算法思想
核心思想:要求源点到其余顶点的最短路径,根据宽度优先搜索的经验一层一层扩展,这样来考虑,从源点开始经过其相邻的一条边(所有边走一遍)可以得到一个暂时的到各个顶点的最短路径,保存到distance数组中;
然后考虑经过在之前的走过边基础上,再走一条边得到可以得到到各顶点的最短路径,就相当于经过两条边得到的最短路径,这里走第二条边时需要将每条边都走一遍才能确定下来走两条边得到的最短路径;
最多可以走顶点数-1条边,就可以得到最短路径,因为每多一条边得到的都是到各顶点的最短路径,最后得到的也是最短路径。
这个思路和Floyd算法很像,只是floyd是通关过中间点来得到源点到一个点的路径,而bell是通过中间边的数量来得到源点到一个点的更短路径。参考floyd算法,dijkstra也是通过顶点来得到更短路径,但是floyd算法和dijkstra算法区别在dijkstra算法每次都取到源点最短的距离的顶点,是贪心思想,参考dijkstra。
Bellman-Ford算法,通过边的信息来求最短路径,和dijkstra和floyd的通过顶点的信息来找不同,因此该算法适合稀疏图,边的数量较少,稀疏矩阵一般使用邻接表的存储方式。
举例分析
以如下的单向图为例分析
求顶点1到其他顶点的最短路径,将,每走一条边的最短路径结果记录到distance数组中,初始化为
distance[5]={0,1000,1000,1000,1000}//1000为极大值
遍历所有边得到经过一条边后distance数组为
//如果distance[j] > distance[i]+ weight[i][j],则更新distance[i]
distance[5]={0,-3,1000,1000,5}
在遍历一遍所有边,1->3变为-1,1->4变为2,1->5变为即经过2条边之后
distance[5]={0,-3,-1,2,,5}
那么==最多==(可以提前结束)遍历5-1次,就一定可以得到最终结果为
distance[5]={0,-3,-1,2,,4}
算法实现
根据以上分析得到java实现代码
package aha_algorithm;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
public class Bellman {
private static Integer vertexNum;
private static Integer edgeNum;
static ArrayList<ArrayList<Edge>> adjList = new ArrayList<ArrayList<Edge>>() ;
static int[] distance ;
static ArrayList<Edge> edges = new ArrayList<Bellman.Edge>();
static class Edge{
public Edge() {
// TODO Auto-generated constructor stub
}
public Edge(int start,int end ,int weight){
this.start = start;
this.end = end;
this.weight = weight;
}
int start ;
int end;
int weight;
}
/**
* @param args
*/
public static void main(String[] args) {
initAdjacencyList();
BellmanFord();
System.out.println("结果为:");
for (int i = 0; i < distance.length; i++) {
System.out.print(distance[i]+" ");
}
}
static void BellmanFord(){
for (int i = 0; i < vertexNum-1; i++) {//遍历n-1次
//遍历所有边
/* for (int j = 0; j < adjList.size(); j++) {
for (int j2 = 0; j2 < adjList.get(j).size(); j2++) {
int weight = adjList.get(j).get(j2).weight;
int dest = adjList.get(j).get(j2).end;
if(distance[dest] > distance[j]+weight){
distance[dest] = distance[j]+weight;
}
}
}*/
for (int j = 0; j < edges.size(); j++) {
// int weight = adjList.get(j).get(j).weight;
// int dest = adjList.get(j).get(j).end;
int start = edges.get(j).start;
int dest = edges.get(j).end;
int weight = edges.get(j).weight;
if(distance[dest] > distance[start]+weight){
distance[dest] = distance[start]+weight;
}
}
System.out.print("第"+i+"次遍历的distance为:");
for (int i1 = 0; i1 < distance.length; i1++) {
System.out.print(distance[i1]+" ");
}
}
}
public static void initAdjacencyList(){
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
try {
String[] inLine = input.readLine().split(" ");
vertexNum = Integer.valueOf(inLine[0]);
edgeNum = Integer.valueOf(inLine[1]);
//初始化distance
distance= new int[vertexNum];
for (int i = 0; i < vertexNum; i++) {
distance[i] = 1000;
}
distance[0]=0;
//初始化邻接表
for (int i = 0; i < vertexNum; i++) {
ArrayList<Edge> subList = new ArrayList<Bellman.Edge>();
adjList.add(i, subList);
}
// 获取边信息
for (int i = 0; i < edgeNum; i++) {
String[] edge = input.readLine().split(" ");
int start =Integer.valueOf(edge[0]);
int end =Integer.valueOf(edge[1]);
int weight =Integer.valueOf(edge[2]);
Edge tempEdge = new Bellman.Edge(start,end,weight);
adjList.get(start).add(tempEdge);//邻接表
//按边输入顺序存储
edges.add(tempEdge);
}
}catch(Exception e){
}
}
}
用例和结果
5 5
1 2 2
0 1 -3
0 4 5
3 4 2
2 3 3
第0次遍历的distance为:0 -3 1000 1000 5 第1次遍历的distance为:0 -3 -1 2 5 第2次遍历的distance为:0 -3 -1 2 4 第3次遍历的distance为:0 -3 -1 2 4
结果为:
0 -3 -1 2 4
算法时间复杂度
可以看到循环执行O(m*n),m为边数,n为顶点数。上面的算法可以看到,执行完第2次遍历之后结果就得到了,其实如果以邻接表的顶点顺序来遍历边那么,将会是这样的结果
第0次遍历的distance为:0 -3 -1 2 4 第1次遍历的distance为:0 -3 -1 2 4 第2次遍历的distance为:0 -3 -1 2 4 第3次遍历的distance为:0 -3 -1 2 4
结果为:
0 -3 -1 2 4
可见后面的循环都是多余的。
所以可以对算法进行优化:判断源点到某个点的距离是否改变
1、可以判断如果结果distance数组没有变化则表示结果已得到,可以直接中断循环。
2、使用队列,每次取队首顶点,对该顶点的所有出边进行遍历。如果遍历某条边时,改变了distance[end],那么说明源点经过该边的终点(end顶点)到其他点的最短路径可能发生了改变,那么将end顶点加入到队列,等待遍历。将首顶点所有出边遍历完就出队。类似BFS。
这里需要对end顶点在队列中去重,可以使用book数组进行标记。
判断是否存在负环路
判断是否存在负权环路就是判断在执行完遍历完n-1次所有边之后,在遍历一次所有边是否会改变distance数组,如果改变了说明存在负权环路。
因为n个顶点的没有负权环路的图的最短路径最多经过n-1条边。即在if()判断中立一个flag判断是否改变。
三种最短路径算法对比
判断是否存在负权环路就是判断在执行完遍历完n-1次所有边之后,在遍历一次所有边是否会改变distance数组,如果改变了说明存在负权环路。
因为n个顶点的没有负权环路的图的最短路径最多经过n-1条边。即在if()判断中立一个flag判断是否改变。
三种算法对比
floyd | dijkstra | Bellman-ford | |
---|---|---|---|
空间复杂度 | O(N^2) | O(M) | O(M) |
时间复杂度 | O(N3) | O((M+N)logN) | O(MN) |
适用情况 | 稠密图 | 稠密图 | 稀疏图 |
负权边 | 可以 | 不可以 | 可以 |
floyd算法简单,dijkstra不能解决负权边优化后可以得到MLogN的复杂度,bellman可以解决负权边。