bellman-ford
Bellman-Ford算法优点
Bellman-Ford算法是用来解决单源最短路问题的。
在现实生活旅游途中,我们通常想知道一个景点到其他所有景点的最短距离,以方便我们决定去哪些比较近的景点。而这时候,Bellman-Ford算法就有用了。
Bellman-Ford算法的优点是可以发现负圈,缺点是时间复杂度比Dijkstra算法高。
而SPFA算法是使用队列优化的Bellman-Ford版本,其在时间复杂度和编程难度上都比其他算法有优势。
虽然题目规定了不存在「负权边」,但我们仍然可以使用可以在「负权图中求最短路」的 Bellman Ford 进行求解,该算法也是「单源最短路」算法,复杂度为 O(VE)。
通常为了确保O(VE) ,可以单独建一个类代表边,将所有边存入集合中,在n次松弛操作中直接对边集合进行遍历(代码见p1 )。
由于本题边的数量级大于点的数量级,因此也能够继续使用「邻接表」的方式进行边的遍历,遍历所有边的复杂度的下界为 O(V),上界可以确保不超过 O(E)(代码见 p2)
算法流程
(1)初始化:将除起点s外所有顶点的距离数组置无穷大 dist[vs] = N, dist[vs] = 0,prev[] = -1;
(2)迭代:遍历图中的每条边,对边的两个顶点分别进行一次松弛操作,直到没有点能被再次松弛
(3)判断负圈:如果迭代超过V-1次,则存在负圈
代码实现
(1) 类实现
class Edge {
int a;
int b;
int c;
public Edge(int a, int b, int c) {
this.a = a;
this.b = b;
this.c = c;
}
}
//n为顶点数量,N为Interger.MAX_VALUE
public static void bellmanClass(int vs) {
List<Edge> edges = new ArrayList<>();
int[] prev = new int[n];
int dist[] = new int[n];
Arrays.fill(dist, N);
Arrays.fill(prev, -1);
dist[vs] = 0;
//改为类存储
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (i == j || matrix[i][j] == N) continue;
edges.add(new Edge(i, j, matrix[i][j]));
}
}
for (int i = 1; i < n; i++) {
boolean hasChange = false;
for (Edge edge : edges) {
int a = edge.a, b = edge.b, c = edge.c;
if (dist[a] == N) continue;
if (dist[b] > dist[a] + c) {
dist[b] = dist[a] + c;
prev[b] = a;
hasChange = true;
}
}
if (!hasChange) break;
}
//判断负环
boolean hasRing = false;
for (Edge edge : edges) {
int a = edge.a, b = edge.b, c = edge.c;
if (dist[a] == N) continue;
if (dist[b] > dist[a] + c) {
hasRing = true;
System.out.println("存在负环");
return;
}
}
// 打印bellman-ford最短路径的结果
System.out.printf("bellman(%c): \n", vexs[vs]);
for (int i = 0; i < vexs.length; i++) {
System.out.printf(" shortest(%c, %c)=%d , p =%d \t", vexs[vs], vexs[i], dist[i], prev[i]);
StringBuilder sb = new StringBuilder();
sb.append(i);
for (int j = i; j != vs && prev[j] != -1; j = prev[j]) {
sb.append(">--");
sb.append(prev[j]);
}
System.out.println(sb.reverse());
}
}
(2) 邻接链表实现
private static int[] head = new int[n];
//edge
private static int[] to = new int[edgNum];
private static int[] w = new int[edgNum];
private static int[] next = new int[edgNum];
public static void adjacency() {
int idx = 0;
Arrays.fill(head, -1);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (i == j) continue;
if (matrix[i][j] == N) continue;
to[idx] = j;
w[idx] = matrix[i][j];
next[idx] = head[i];
head[i] = idx;
idx++;
}
}
}
public static void bellman(int vs) {
adjacency();
int[] dist = new int[n];
int[] prev = new int[n];
Arrays.fill(dist, N);
Arrays.fill(prev, -1);
dist[vs] = 0;
for (int i = 1; i < n; i++) {
//提前结束
boolean hasChange = false;
//遍历边
for (int j = 0; j < n; j++) {
for (int a = head[j]; a != -1; a = next[a]) {
if (dist[j] == N) continue;
int id = to[a];
if (dist[id] > w[a] + dist[j]) {
dist[id] = w[a] + dist[j];
prev[id] = j;
hasChange = true;
}
}
}
if (!hasChange) break;
}
//判断负环
boolean hasRing = false;
for (int j = 0; j < n; j++) {
for (int a = head[j]; a != -1; a = next[a]) {
if (dist[j] == N) continue;
int id = to[a];
if (dist[id] > w[a] + dist[j]) {
hasRing = true;
System.out.println("存在负环");
return;
}
}
}
// 打印bellman-ford最短路径的结果
System.out.printf("bellman(%c): \n", vexs[vs]);
for (int i = 0; i < vexs.length; i++) {
System.out.printf(" shortest(%c, %c)=%d , p =%d \t", vexs[vs], vexs[i], dist[i], prev[i]);
StringBuilder sb = new StringBuilder();
sb.append(i);
for (int j = i; j != vs && prev[j] != -1; j = prev[j]) {
sb.append(">--");
sb.append(prev[j]);
}
System.out.println(sb.reverse());
}
}
SPFA
SPFA算法是求解单源最短路径问题的一种算法,由理查德·贝尔曼(Richard Bellman) 和 莱斯特·福特 创立的。有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为 Edward F. Moore 也为这个算法的发展做出了贡献。它的原理是对图进行V-1次松弛操作,得到所有可能的最短路径。其优于迪科斯彻算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达 O(VE)。但算法可以进行若干种优化,提高了效率。
SPFA (shortest path faster algorithm) 是一个单源最短路径算法,与另一个单源最短路算法dijkstra不同的是(什么你还不知道dijkstra?),SPFA可以用来处理含有负权的图,并且也可以判断图中是否存在负权回路。
实际上,SPFA是Bellman-Ford算法的一种队列实现,减少了不必要的冗余计算。SPFA的时间复杂度可以达到,k是每一个节点的平均进队次数,可以证明k不大于2。
算法的思路:
我们用数组dist记录每个结点的最短路径估计值,用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止
我们要知道带有负环的图是没有最短路径的,所以我们在执行算法的时候,要判断图是否带有负环,方法有两种:
- 开始算法前,调用拓扑排序进行判断(一般不采用,浪费时间)
- 如果某个点进入队列的次数超过N次则存在负环(N为图的顶点数)
SPFA算法流程
首先说一下我们都需要哪些东西:
一个储存节点的队列 queue
一个储存当前从原点到每一个节点的最短路径长度的数组dist[n],prev[n](与dijkstra相同)
一个标记此节点是否在队列中的数组visit[n]
一个记录每个节点入队次数的数组count[n](可选,根据图中是否有负权回路判断)
Step1:初始化
与dijkstra类似,首先需要将dist[]数组中每一个元素初始化为,这个我习惯设成0x3f3f3f3f。其次,将visited中的每一个元素设为。将源点的dist值dist[]设为0,将visited[]设为。
Step2:入队
更新与当前节点(如果是第一次循环就是)相连,且在以为弧尾的边上的节点 的最短距离(也就是dist数组的值),如果dist[] + < dist[],那么dist[] = dist[] + 。(weight是两个节点之间的权值)然后,如果节点不在队列中,就将其入队,并记录每一个节点的入队次数,如果次数大于了节点总数,那么就说明这个图中存在回路。
Step3:重复Step2直至队列为空
代码实现
public static void spfa(int vs) {
adjacency();
int[] dist = new int[n];
int[] prev = new int[n];
boolean[] visit = new boolean[n];
int[] count = new int[n];
Arrays.fill(visit,false);
Arrays.fill(dist, N);
Arrays.fill(prev, -1);
dist[vs] = 0;
visit[vs] = true;
Deque<Integer> queue = new LinkedList<>();
queue.offer(vs);
while (!queue.isEmpty()) {
int poll = queue.poll();
visit[poll] = false;
for (int j = head[poll]; j != -1; j = next[j]) {
int id = to[j];
if (dist[poll] == N) continue;
if (dist[id] > dist[poll] + w[j]) {
if(visit[id]) continue;
dist[id] = dist[poll] + w[j];
prev[id] = poll;
if(++count[id] > n){
System.out.println("存在负环");
return;
}
visit[id] = true;
queue.offer(id);
}
}
}
// 打印dijkstra最短路径的结果
System.out.printf("bellman(%c): \n", vexs[vs]);
for (int i = 0; i < vexs.length; i++) {
System.out.printf(" shortest(%c, %c)=%d , p =%d \t", vexs[vs], vexs[i], dist[i], prev[i]);
StringBuilder sb = new StringBuilder();
sb.append(i);
for (int j = i; j != vs && prev[j] != -1; j = prev[j]) {
sb.append(">--");
sb.append(prev[j]);
}
System.out.println(sb.reverse());
}
}