前言
从上篇博客的,Bellman-Ford算法介绍的最后一部分,我们指出,其实,可以对该算法进行进一步的优化。原因是因为:其实在每一轮松弛操作结束后,就会有一些顶点已经求得其最短路径。此后这些顶点的最短路径的估计值就会一直保持不变,但是每一次都还要对其进行判断。这里浪费了时间,这就启发了我们每次仅对最短路估计值发生了变化的顶点的所有出边执行松弛操作。所以下面我们学习Bellman-Ford算法的队列优化。而这篇文章,我们就来介绍一下其队列优化,以及几种最短路径算法的对比。
Bellman-Ford算法的队列优化
这个优化算法大致如下: 每次选取队列首顶点u,对顶点u的所有出边进行松弛操作,例如有一条u—>v的边,如果通过u—>v这条边使得源点到v点的最短路程变短,且顶点v不在当前队列中,就将顶点放入队尾。需要注意的是,同一个顶点同时在队列中出现多次是毫无意义的,所以我们需要一个数组来判重。在对顶点u的所有出边松弛完毕后,就将顶点u出队。接下来不断从队列中取出新的队首顶点再进行如上的操作,直到队列空为止。
下面我们用一个例子来说明:
最短路径图如上。
具体过程分析 1、首先我们初始化一个数组dis = {0,999999,999999,999999,999999},表示源点1号点,到1、2、3、4、5点的距离,其中1号点到1号的距离初始化为0,到其余各点初始化为无穷(999999) 2、然后需要一个记录队列que来存放各点 3、首先我们来看1号点的各个边,能否让1号点到其余点的距离变短,首先是1—>2 边,可以让dis[2] = 2,所以松弛成功,看队列que中是否有2号点,发现没有,那么将2号点添加到队列que中。 现在数组dis = {0,2,999999,999999,999999} 队列que = { 1 ,2},然后看1—>5边,是否可以让dis[5]变小,我们发现能让dis[5]变小,然后看队列que中是否有5号点,没有 那么添加进去,现在的数组 dis= { 0,2,999999,999999,10} 队列que = {1,2 , 5}。然后1号点没有其他出边了,我们将1号点从que中移除出去,第一个点松弛完毕的结果是 dis = {0,2,999999,999999,10} que = {2,5}。 4、然后,我们继续从que队列的队首中取出顶点,并且判断这个顶点的各个出边是否能让源点1到其余个点的距离缩短。首先看顶点2的第一条出边 2—>3能否让顶点1到顶点3的路径变短,也就是dis[2] + 2—>3 ? dis[3]。我们发现dis[2] + 3 < dis[3] = 999999,所以松弛成功,然后看顶点3是否在队列中,不在,所以,现在的dis = {0,2,5,999999,10},队列 que = {2,5,3 },然后看顶点2的第二条出边2—> 5 (上面的图漏了一个2—>5的边的距离是7),能否让源点1到顶点5的距离变短,我们发现dis[2] + 2 —>5 = 2 + 7 < dis[5] = 10,所以松弛成功,然后看顶点5是否在队列que中,发现已经存在了,所以不继续往里面添加了,所以顶点2的各个出边的松弛结果是 dis = {0,2,5,999999,9} que = { 5,3}。 5、依次继续,直到队列que中没有了顶点。这样就完成了Bellman-Ford算法的队列优化。
代码编写
首先,我们初始化dis数组和队列que
int[] dis = new int[6];
for (int i = 1; i <= 5; i++) {
if (i == 1){
dis[i] = 0;
}else {
dis[i] = 999999;
}
}
Queue<Integer> que = new LinkedList<>();//记录最短路径有过变化的点
que.add(1);//先增加一个源点,算法需要从源点开始
因为java中已经有实现好的队列LinkedList了,所以我们这里就不手动实现队列这种数据结构了。dis数组的dis[1] 初始化为0,其余点初始化为无穷,上面已经解释过了,就不再多解释。这里的que.add(1)是提前往队列中增加一个点,因为我们需要一个起始点。 然后是初始化路径图
//初始化路径图
int[] u = new int[8];//边的起点
int[] v = new int[8];//边的终点
int[] w = new int[8];//边的权值
u[1] = 1; v[1] = 2; w[1] = 2;//各条边的初始化
u[2] = 1; v[2] = 5; w[2] = 10;
u[3] = 2; v[3] = 3; w[3] = 3;
u[4] = 2; v[4] = 5; w[4] = 7;
u[5] = 3; v[5] = 4; w[5] = 4;
u[6] = 4; v[6] = 5; w[6] = 5;
u[7] = 5; v[7] = 3; w[7] = 6;
//我们使用邻接表来完成图的记录
int[] first = new int[8];
int[] next = new int[8];
for (int i = 1; i <= 7; i++) {
first[i] = -1;
}
for (int i = 1; i <= 7; i++) {
next[i] = first[u[i]];
first[u[i]] = i;
}
这里不多说,u,v,w跟以前一样记录了一条边的完整信息,u记录的是边的起点,v记录的是边的终点,w记录的是边的权值。然后我们初始化了两个数组first和next,这是因为该算法中用到了遍历一个顶点的所有出边。所以我们使用了邻接表来存储边的信息,能优化一下算法的执行时间。
然后就是算法的主体
while (!que.isEmpty()){
//如果队列不是空的
//拿到队列中的第一个点
Integer remove = que.remove();
//遍历所有的顶点remove的出边
int k = first[remove];
while (k != -1){
if (dis[v[k]] > dis[u[k]] + w[k]){
//松弛成功
dis[v[k]] = dis[u[k]] + w[k];
if (!que.contains(v[k])){
que.add(v[k]);
}
}
k = next[k];
}
}
//算法松弛完毕,打印结果
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Bellman-Ford的队列优化结果-dis = {");
for (int i = 1; i <=5 ; i++) {
if (i == 5){
stringBuilder.append(dis[i]+"}");
}else {
stringBuilder.append(dis[i]+",");
}
}
Log.e("hero","--"+stringBuilder.toString());
执行结果
到这里,Bellman-Ford算法的队列优化也差不多结束了。但是我们还有一个点需要注意一下,第一就是,这种优化我们如何判断一个图是否有负权回路呢?其实我们可以通过一个点如果进入了队列que中n次,说明他肯定是有负权回路的。该优化的核心就是,只有那些在前一遍松弛中丐帮了最短路径估计值的点,才可能引起其他领接点的最短路程的估计值发生变化。因此使用一个队里que记录被成功松弛的点,之后只对队列中的点进行处理,这降低了算法的时间复杂度。
最短路径算法对比分析
上面,我们学习了Floyd、Dijkstra、Bellman-Ford以及Bellman-Ford的队列优化等求解最短路径的算法。他们的一些数据对比如下
具体的分析这里就不再进行了,因为我们已经分别深入的学习了这四种算法的核心思想和实现。
总结
这篇文章到这里,也就结束了。最短路径的几种常见和简单的算法,我们也都已经学习了一遍。当然这个问题,我们只是猜入门,对于更加深入的知识还有很多,有兴趣的朋友,可以继续搜索相关资料进行学习。 ps:最近看了浮生六记这本书,不得不说,以前的社会生产力低下,卫生医疗条件落后。能安稳一生,最后善终的人,已是上天赐予的莫大幸福了。所以,珍惜如今的生活吧。