引言
本文整理了常见的图的最短距离相关算法,方便以后查阅。更多相关文章和其他文章均收录于贝贝猫的文章目录。
BellmanFord
package bbm.graph;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/**
* 解决单源最短路径问题, 本例中使用的 Bellman-Ford 算法能够处理有负权重的问题,它的思想是通过对每一条边进行松弛化,重复遍历 V-1 次所有边
* (V = 节点数)。
* 这里的松弛化过程就是:我们用 d 来描述从源点出发到某一节点的最短路径估计,当我们要对一条 s->v 的边进行松弛化时,我们需要
* 比较 v.d 和 s.d + w<s, v>,如果 v.d 更大则说明如果想要到哒 v 通过 s 再到 v 能更短,这时 s 将成为 v 的前缀路径,v 的 d 修改为
* s.d + w<s, v>
* 因为每次对所有边进行松弛化后,总会有至少一个节点的 d 达到最优值,所以重复遍历 V-1 次所有边之后,就能得到最终结果
*
* @author bbm
*/
public class BellmanFord {
public static class Node {
String name;
int d = 10000;
Node pre = null;
public Node(String name) {
this.name = name;
}
}
public static class Line {
int w;
Node[] points;
public Line(Node node1, Node node2, int w) {
this.points = new Node[] {node1, node2};
this.w = w;
}
}
private static void relax(Node s, Node v, int w) {
if (v.d > s.d + w) {
v.d = s.d + w;
v.pre = s;
}
}
public static void main(String[] args) {
Node s = new Node("s");
Node t = new Node("t");
Node x = new Node("x");
Node z = new Node("z");
Node y = new Node("y");
List<Node> nodes = Arrays.asList(s, t, x, z, y);
List<Line> lines = new LinkedList<>();
lines.add(new Line(s, t, 6));
lines.add(new Line(s, y, 7));
lines.add(new Line(y, y, 8));
lines.add(new Line(z, s, 2));
lines.add(new Line(t, x, 5));
lines.add(new Line(x, t, -2));
lines.add(new Line(t, z, -4));
lines.add(new Line(y, x, -3));
lines.add(new Line(y, z, 9));
lines.add(new Line(z, x, 7));
s.d = 0;
for (int i = 0; i < nodes.size() - 1; i++) {
for (Line line : lines) {
// 松弛化过程
relax(line.points[0], line.points[1], line.w);
}
}
for (Line line : lines) {
if (line.points[1].d > line.points[0].d + line.w) {
System.out.println("Has cycle");
}
}
for (Node node : nodes) {
System.out.println(node.name + ":" + node.d + " pre:" + (node.pre != null ? node.pre.name : ""));
}
}
}
无环图单源最短路径
package bbm.graph;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import static bbm.graph.ShortestPathWithoutCycle.Color.GRAY;
import static bbm.graph.ShortestPathWithoutCycle.Color.WHITE;
/**
* 解决无环图单源最短路径
*
* 在无环图中,我们可以按照拓扑排序的节点顺序,依次处理各个节点的所有边,当处理完最后一个节点时,整个图中的所有节点的最短路径就确定下来了
*
* 由于拓扑排序的性质,当在图中不存在从 B 到 A 的路径,则 A 在序列中排在 B 的前面,每当我们按照拓扑排序处理一个节点的所有边时,就会有至少一个
* 节点的最短路径确定下来
*
* @author bbm
*/
public class ShortestPathWithoutCycle {
public static class Node {
String name;
Color color = WHITE;
int startTime;
int endTime;
Node pre = null;
int d = 10000;
List<Next> next = new LinkedList<>();
public Node(String name) {
this.name = name;
}
}
public static class Next {
Node node;
int w;
public Next(Node node, int w) {
this.node = node;
this.w = w;
}
}
public enum Color {
WHITE,
GRAY,
BLACK
}
public static int time = 0;
public static void dfs(List<Node> graph) {
graph.forEach(node -> {
if (node.color.equals(WHITE)) {
visit(node);
}
});
}
private static void visit(Node node) {
time += 1;
node.startTime = time;
node.color = GRAY;
node.next.forEach(next -> {
if (next.node.color.equals(WHITE)) {
visit(next.node);
}
});
node.color = Color.BLACK;
time += 1;
node.endTime = time;
}
public static void topologicalSort(List<Node> nodes) {
dfs(nodes);
nodes.sort((o1, o2) -> o2.endTime - o1.endTime);
for (Node node : nodes) {
System.out.print(node.name + " ");
}
}
private static void relax(Node s, Node v, int w) {
if (v.d > s.d + w) {
v.d = s.d + w;
v.pre = s;
}
}
public static void main(String[] args) {
Node s = new Node("s");
Node r = new Node("r");
Node t = new Node("t");
Node x = new Node("x");
Node y = new Node("y");
Node z = new Node("z");
r.next.add(new Next(s, 5));
r.next.add(new Next(t, 3));
s.next.add(new Next(t, 2));
s.next.add(new Next(x, 6));
t.next.add(new Next(x, 7));
t.next.add(new Next(y, 4));
t.next.add(new Next(z, 2));
x.next.add(new Next(z, 1));
x.next.add(new Next(y, -1));
y.next.add(new Next(z, -2));
List<Node> nodes = Arrays.asList(s, r, z, y, x, t);
topologicalSort(nodes);
System.out.println();
s.d = 0;
for (Node node : nodes) {
if (!node.next.isEmpty()) {
for (Next next : node.next) {
relax(node, next.node, next.w);
}
}
}
for (Node node : nodes) {
System.out.println(node.name + ":" + node.d + " pre:" + (node.pre != null ? node.pre.name : ""));
}
}
}
Dijkstra
package bbm.graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
/**
* 解决非负值权重的单源最短路径
*
* Dijkstra 将节点分为两个集合,一个是处理完的节点集合,一个时待处理集合,每次从待处理集合中选取 d 最小的节点,然后松弛化该节点的所有边,
* 重复该过程直到处理集合为空
*
* @author bbm
*/
public class Dijkstra {
public static class Node {
String name;
int d = 10000;
Node pre = null;
List<Next> next = new LinkedList<>();
public Node(String name) {
this.name = name;
}
}
public static class Next {
Node node;
int w;
public Next(Node node, int w) {
this.node = node;
this.w = w;
}
}
private static void relax(Node s, Node v, int w) {
if (v.d > s.d + w) {
v.d = s.d + w;
v.pre = s;
}
}
public static void main(String[] args) {
Node s = new Node("s");
Node t = new Node("t");
Node x = new Node("x");
Node y = new Node("y");
Node z = new Node("z");
s.next.add(new Next(t, 10));
s.next.add(new Next(y, 5));
t.next.add(new Next(y, 2));
t.next.add(new Next(x, 1));
y.next.add(new Next(t, 3));
y.next.add(new Next(x, 9));
y.next.add(new Next(z, 2));
x.next.add(new Next(z, 4));
z.next.add(new Next(x, 6));
z.next.add(new Next(s, 7));
s.d = 0;
List<Node> nodes = new ArrayList<>(Arrays.asList(s, t, x, y, z));
List<Node> result = new ArrayList<>();
while (!nodes.isEmpty()) {
nodes.sort(Comparator.comparingInt(o -> o.d));
Node node = nodes.remove(0);
result.add(node);
for (Next next: node.next) {
relax(node, next.node, next.w);
}
}
for (Node node: result) {
System.out.println(node.name + ":" + node.d + " pre:" + (node.pre != null ? node.pre.name : ""));
}
}
}
Floyd-Warshall
package bbm.graph;
/**
* 解决所有节点对的最短路径问题,要求不存在负权重环路
*
* 假设图的所有节点为 1-n, 现在我们只考虑其中一个子集,1-k,这时候我们假设对于任何节点 i 和 j 它们的路径 p 属于 1-k,并且 p 为权重最小的路径
* 本算法利用了 i 到 j 的中间节点均取自 1-(k-1) 的路径 p' 和上述路径 p 的关系来实现。现在我们有两条路径,其中 p 的中间节点来自于 1-k,
* p' 的中间结点来自于 1-(k-1),要认清它们之间的关系,我们需要分两种情况:
* 1. k 不是路径 p 的中间节点,也就是说路径 p 实际上中间结点只在 1-(k-1) 中,也就是 p = p'
* 2. k 是路径 p 的中间结点,那么路径可以分解为 i -> k -> j,其中 ik 和 kj 路径中的节点均来自于 1-(k-1) ,因为我们要求图中没有负环路,
* 这时候 p = ik + kj
*
*
* @author bbm
*/
public class FloydWarshall {
private static void floyd(int[][] weight) {
int[][][] d = new int[6][5][5];
Integer[][][] pre = new Integer[6][5][5];
d[0] = weight;
pre[0] = new Integer[5][5];
for (int i = 0; i < weight.length; i++) {
for (int j = 0; j < weight.length; j++) {
if (i == j || weight[i][j] == 10000) {
pre[0][i][j] = null;
} else {
pre[0][i][j] = i + 1;
}
}
}
System.out.println("Pre" + 0 + ":");
for (int i = 0; i < pre[0].length; i++) {
for (int j = 0; j < pre[0].length; j++) {
System.out.print(pre[0][i][j] + " ");
}
System.out.println();
}
for (int k = 1; k <= weight.length; k++) {
d[k] = new int[5][5];
pre[k] = new Integer[5][5];
for (int i = 0; i < d[k].length; i++) {
for (int j = 0; j < d[k].length; j++) {
d[k][i][j] = Math.min(d[k - 1][i][j], d[k - 1][i][k - 1] + d[k - 1][k - 1][j]);
if (d[k - 1][i][j] <= (d[k - 1][i][k - 1] + d[k - 1][k - 1][j])) {
pre[k][i][j] = pre[k-1][i][j];
} else {
pre[k][i][j] = pre[k-1][k-1][j];
}
}
}
System.out.println("D" + k + ":");
for (int i = 0; i < d[k].length; i++) {
for (int j = 0; j < d[k].length; j++) {
System.out.print(d[k][i][j] + " ");
}
System.out.println();
}
System.out.println("Pre" + k + ":");
for (int i = 0; i < pre[k].length; i++) {
for (int j = 0; j < pre[k].length; j++) {
System.out.print(pre[k][i][j] + " ");
}
System.out.println();
}
}
}
public static void main(String[] args) {
int[][] weight = new int[][] {
{0, 3, 8, 10000, -4},
{10000, 0, 10000, 1, 7},
{10000, 4, 0, 10000, 10000},
{2, 10000, -5, 0, 10000},
{10000, 10000, 10000, 6, 0}
};
System.out.println("D" + 0 + ":");
for (int i = 0; i < weight.length; i++) {
for (int j = 0; j < weight.length; j++) {
System.out.print(weight[i][j] + " ");
}
System.out.println();
}
floyd(weight);
}
}
Johnson
package bbm.graph;
import java.util.HashSet;
import java.util.Set;
/**
* Johnson 算法主要用于求稀疏图上的所有节点对的最短路径。其主体思想是利用重新赋予权值的方法把一个原问题带负权的图转化为权值非负的图。
* 然后再对每个节点使用一次 Dijkstra 算法以求出所有节点对的最短路径。
*
* @author bbm
*/
public class Johnson {
private static int[] bellmanFord(int[][] newWeight) {
int[] h = new int[newWeight.length];
h[0] = 0;
for (int i = 1; i < h.length; i++) {
h[i] = 10000;
}
for (int k = 0; k < newWeight.length - 1; k++) {
for (int i = 0; i < newWeight.length; i++) {
for (int j = 0; j < newWeight.length; j++) {
h[j] = relax(h[j], h[i], newWeight[i][j]);
}
}
}
for (int i = 0; i < newWeight.length; i++) {
for (int j = 0; j < newWeight.length; j++) {
if (h[i] + newWeight[i][j] < h[j]) {
throw new RuntimeException("Has cycle");
}
}
}
return h;
}
private static int relax(int dv, int ds, int w) {
if (dv > ds + w) {
return ds + w;
} else {
return dv;
}
}
private static void dijkstra(int index, int[][] weight) {
System.out.println("Start from " + (index + 1));
int[] length = new int[weight.length];
length[index] = 0;
for (int i = 0; i < length.length; i++) {
if (i != index) {
length[i] = 10000;
}
}
Set<Integer> handled = new HashSet<>();
while (handled.size() < weight.length) {
int min = 10000;
int minIndex = -1;
for (int i = 0; i < length.length; i++) {
if (!handled.contains(i) && length[i] < min) {
min = length[i];
minIndex = i;
}
}
handled.add(minIndex);
for (int i = 0; i < weight.length; i++) {
if (weight[minIndex][i] != 10000 && i != minIndex) {
length[i] = relax(length[i], length[minIndex], weight[minIndex][i]);
}
}
}
for (int i = 0; i < length.length; i++) {
System.out.print(length[i] + " ");
}
System.out.println();
}
private static void johnson(int[][] weight) {
int[][] newWeight = new int[weight.length + 1][weight.length + 1];
for (int i = 0; i < newWeight.length; i++) {
newWeight[0][i] = 0;
}
for (int i = 1; i < newWeight.length; i++) {
newWeight[i][0] = 10000;
}
for (int i = 0; i < weight.length; i++) {
for (int j = 0; j < weight.length; j++) {
newWeight[i + 1][j + 1] = weight[i][j];
}
}
System.out.println("New weight:");
for (int i = 0; i < newWeight.length; i++) {
for (int j = 0; j < newWeight.length; j++) {
System.out.print(newWeight[i][j] + " ");
}
System.out.println();
}
int[] h = bellmanFord(newWeight);
System.out.println("H:");
for (int i = 0; i < h.length; i++) {
System.out.print(h[i] + " ");
}
System.out.println();
for (int i = 0; i < weight.length; i++) {
for (int j = 0; j < weight.length; j++) {
if (weight[i][j] != 10000) {
weight[i][j] += h[i+1] - h[j+1];
}
}
}
System.out.println("Weight:");
for (int i = 0; i < weight.length; i++) {
for (int j = 0; j < weight.length; j++) {
System.out.print(weight[i][j] + " ");
}
System.out.println();
}
for (int i = 0; i < weight.length; i++) {
dijkstra(i, weight);
}
}
public static void main(String[] args) {
int[][] weight = new int[][] {
{0, 3, 8, 10000, -4},
{10000, 0, 10000, 1, 7},
{10000, 4, 0, 10000, 10000},
{2, 10000, -5, 0, 10000},
{10000, 10000, 10000, 6, 0}
};
System.out.println("Weight:");
for (int i = 0; i < weight.length; i++) {
for (int j = 0; j < weight.length; j++) {
System.out.print(weight[i][j] + " ");
}
System.out.println();
}
johnson(weight);
}
}
参考内容
[1] 《算法导论》