贝尔曼福特算法

松弛算法

想像一下图论中的graph 是用毛线和珠子组成的网状结构,两颗珠子之间毛线的长度即edge上的权值,一开始十分松乱的放在桌上。现在你要求SSSP(单源最短路),当发现从源点s到当前点u有两条路径,relax操作可以想象成用力把s和u两点往外撑开。这时候依照生活经验,我们可以很自然的看到s点和u点之间较短的那条边处于紧绷状态,而较长的那条边处于松弛状态。因此非常形象的把这个操作称为松弛操作。

负环路

负环路(环路权重之和为负数)

Bellman Ford算法每次对所有的边进行松弛,每次松弛都会得到一条最短路径,所以总共需要要做的松弛操作是V - 1次。在完成这么多次松弛后如果还是可以松弛的话,那么就意味着,其中包含负环。

思路

  1. 初始化时将起点s到各个顶点v的距离dist(s->v)赋值为∞,dist(s->s)赋值为0

  2. 后续进行最多n-1次遍历操作(n为顶点个数,上标的v输入法打不出来…),对所有的边进行松弛操作,假设:

    所谓的松弛,以边ab为例,若dist(a)代表起点s到达a点所需要花费的总数, dist(b)代表起点s到达b点所需要花费的总数,weight(ab)代表边ab的权重, 若存在:

    (dist(a) +weight(ab)) < dist(b)

    则说明存在到b的更短的路径,s->…->a->b,更新b点的总花费为(dist(a) +weight(ab)),父节点为a

  3. 遍历都结束后,若再进行一次遍历,还能得到s到某些节点更短的路径的话,则说明存在负环路

思路上与狄克斯特拉算法(Dijkstra algorithm)最大的不同是每次都是从源点s重新出发进行”松弛”更新操作,而Dijkstra则是从源点出发向外扩逐个处理相邻的节点,不会去重复处理节点,这边也可以看出Dijkstra效率相对更高点。

Java实现

如下图,求出A到各节点的最短路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
public class BellmanFord {

/**
* 实现思路:
* 1、初始化时将起点起点到各个顶点的距离赋值为(无穷大)∞,当前起点距离赋值为0
* 2、后续进行最多n-1次遍历操作
* @param args
*/
public static void main(String[] args){

//创建图
Edge ab = new Edge("A", "B", -1);
Edge ac = new Edge("A", "C", 4);
Edge bc = new Edge("B", "C", 3);
Edge be = new Edge("B", "E", 2);
Edge ed = new Edge("E", "D", -3);
Edge dc = new Edge("D", "C", 5);
Edge bd = new Edge("B", "D", 2);
Edge db = new Edge("D", "B", 1);

//需要按图中的步骤步数顺序建立数组,否则就是另外一幅图了,
//从起点A出发,步骤少的排前面
Edge[] edges = new Edge[] {ab,ac,bc,be,bd,ed,dc,db};

//存放到各个节点所需要消耗的时间
HashMap<String,Integer> costMap = new HashMap<String,Integer>();
//到各个节点对应的父节点
HashMap<String,String> parentMap = new HashMap<String,String>();


//初始化各个节点所消费的,当然也可以再遍历的时候判断下是否为Null
//i=0的时候
costMap.put("A", 0); //源点
costMap.put("B", Integer.MAX_VALUE);
costMap.put("C", Integer.MAX_VALUE);
costMap.put("D", Integer.MAX_VALUE);
costMap.put("E", Integer.MAX_VALUE);

//进行节点数n-1次循环
for(int i =1; i< costMap.size();i++) {
boolean hasChange = false;
for(int j =0; j< edges.length;j++) {
Edge edge = edges[j];
//该边起点目前总的路径大小
int startPointCost = costMap.get(edge.getStartPoint()) == null ? 0:costMap.get(edge.getStartPoint());
//该边终点目前总的路径大小
int endPointCost = costMap.get(edge.getEndPoint()) == null ? Integer.MAX_VALUE : costMap.get(edge.getEndPoint());
//如果该边终点目前的路径大小 > 该边起点的路径大小 + 该边权重 ,说明有更短的路径了
if(endPointCost > (startPointCost + edge.getWeight())) {
costMap.put(edge.getEndPoint(), startPointCost + edge.getWeight());
parentMap.put(edge.getEndPoint(), edge.getStartPoint());
hasChange = true;
}
}
if (!hasChange) {
//经常还没达到最大遍历次数便已经求出解了,此时可以优化为提前退出循环
break;
}
}

//在进行一次判断是否存在负环路
boolean hasRing = false;
for(int j =0; j< edges.length;j++) {
Edge edge = edges[j];
int startPointCost = costMap.get(edge.getStartPoint()) == null ? 0:costMap.get(edge.getStartPoint());
int endPointCost = costMap.get(edge.getEndPoint()) == null ? Integer.MAX_VALUE : costMap.get(edge.getEndPoint());
if(endPointCost > (startPointCost + edge.getWeight())) {
System.out.print("\n图中存在负环路,无法求解\n");
hasRing = true;
break;
}
}

if(!hasRing) {
//打印出到各个节点的最短路径
for(String key : costMap.keySet()) {
System.out.print("\n到目标节点"+key+"最低耗费:"+costMap.get(key));
if(parentMap.containsKey(key)) {
List<String> pathList = new ArrayList<String>();
String parentKey = parentMap.get(key);
while (parentKey!=null) {
pathList.add(0, parentKey);
parentKey = parentMap.get(parentKey);
}
pathList.add(key);
String path="";
for(String k:pathList) {
path = path.equals("") ? path : path + " --> ";
path = path + k ;
}
System.out.print(",路线为"+path);
}
}
}
}



/**
* 代表"一条边"的信息对象
*
* @author Administrator
*
*/
static class Edge{
//起点id
private String startPoint;
//结束点id
private String endPoint;
//该边的权重
private int weight;
public Edge(String startPoint,String endPoint,int weight) {
this.startPoint = startPoint;
this.endPoint = endPoint;
this.weight = weight;
}
public String getStartPoint() {
return startPoint;
}

public String getEndPoint() {
return endPoint;
}

public int getWeight() {
return weight;
}
}

}

问题

为什么循环n-1次

首先明确,不是这个算法规定了一定要n-1次循环
而是这个算法最坏的情况下需要n-1次循环。

什么情况下最好?
这是我们的图: 很简单的一个图,四个节点,三个边(长度分别是:3 7 12)

1
1 --(3)---2--(7)---3--(12)--4

让你求出,从1出发的最短路径

1 .假如题目这样描述:

1
2
3
1   2   3
2 3 7
3 4 12

能理解吧? (起点,终点, 长度)

关键就在于我们从上到下按顺序,把这三个边, 依次存进了 你的 edge[] 数组里面。
来看看Bellman算法的代码中,我们是咋操作的

  1. 先把dis 数组,起点处设置为 0 , 其它点设置为INF, dis[1] = 0 , 其他的dis[] =INF
  2. 对于每一次循环, 算法要求我们,更新 m 条边。
1
2
3
4
5
6
7
8
for(i=0;i<n;i++){
for(j=0;j<m;j++){
int u=e[j].u,v=e[j].v;
if(d[u]!=INF&&d[u]+w[u][v]<d[v]) {
d[v]=d[u]+w[u][v];
}
}
}

我们来动手做一下

1
2
3
4
5
6
7
8
9
10
11
12
 第一次循环。。。。。。 
dis[1]=0
dis[2] =3 ,因为 dis[1] + map[1][2] < dis[2],也就是 3 + 0 <INF
dis[3] = 10 ,同理
dis[4] = 22 , 同理
第二次循环。。。。。。。
dis[1] =0
dis[2] = 3
dis[3] =10
dis[4] = 22 ,啥都没做
第三次循环。。。。。。
也啥都没做

发现了吗? 我们仅仅一次循环就出结果了,谁还费那劲要做n-1 次循环????
但是,如果是这种呢?

2 .假如题目这样描述:

1
2
3
3   4   12
2 3 7
1 2 3

我们再动手操作一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 第一次循环。。。。。。 
dis[4]=inf
dis[3]=inf ,
dis[2] =3 ,
dis[1]= 0 ,
第二次循环。。。。。。。
dis[4]=inf
dis[3]= 10 ,
dis[2] =3 ,
dis[1]= 0 ,
第三次循环。。。。。。
dis[4]=22
dis[3]=10,
dis[2] =3 ,
dis[1]= 0 ,

我们必须,做n-1次循环,才能出结果

原因就是:

1
2
3
我们按顺序存入边,
但是这个顺序有时候很好,(我们第一次循环就是从前往后循环),导致第一次循环就更新了 所有的节点
这个顺序有时候很差(我们每次都是从路径的后面向前面更新),导致每次循环仅仅更新一个

所以,我们为了应对最差情况,不得不更新n-1次。

现在你懂了为啥循环中间有个剪枝了吧,就是担心情况太好, 导致后面的循环白白浪费时间,早点剪枝跳出循环。