简单起见(这回只写伪代码好了), 对于图的定义如下:
node index = {1,2,...,n}
e[u,v] = distance of edge(u,v);
if (u,v) is not an edge, e[u,v]=INF- 令N=点的数量, M=边的数量
文章目录
任意两点最短路径: Floyd-Warshall
Floyd算法解决的问题是: 对于任意两个节点s(source)和t(target), 求s到达t的最短路径.
Floyd算法的思想是动态规划:
- 定义
d(i,j,k)
为点i到点j之间, 只允许借道节点1…k的最短路径 - 初始化:
d(i,j,0)=e[i,j]
(即i到j之间不经由其他任何中转节点的最短路径) - 更新dij的公式就是:
d(i,j,k)=min( d(i,j,k-1), d(i,k-1,k-1)+e[k,j])
- 更新n次
每次更新dij的意思就是: 现在从i到j可以经过节点k了, 那么看一下ij之间从k这个点经过的话(i → k → j 这条路)能不能缩短dij.
i到j最短路径最终就是: d(i,j,n)
即i到j的路程可以经过1~n中的任何中转节点.
伪代码
for all [i,j]: // initialize
d[i,j] = e[i,j]
for k = 1 ~ n: // relax for k times:
for all [i,j]:
d[i,j] = min(d[i,j], d[i,k] + e[k,j])
完整代码
void Floyd(int[][] e, int n) {
// 记录任意两点间的最短距离
int[][] d = Arrays.copyOf(e, e.length);
// 记录最短距离路径
int[][] path = new int[n + 1][n + 1];
// 三重循环
// 选中的中间值
for (int k = 1; k <= n; k++) {
// 数组横坐标
for (int i = 1; i <= n; i++) {
// 数组纵坐标
for (int j = 1; j <= n; j++) {
// 如果以k中间点为中间点检测到路径更短
if (d[i][j] > d[i][k] + e[k][j]) {
// 更新路径值
d[i][j] = d[i][k] + e[k][j];
// 更新要经过的中间点
path[i][j] = k;
}
}
}
}
}
核心代码只有最后三行… 运行结束后d[i,j]就保存着任意i和j之间的最短路径长度.
程序主循环n次, 每次要处理遍历所有的ij组合, 所以复杂度是O(N^3).
单源最短路径: Dijkstra
Dijkstra算法解决的问题是: 没有负权边的情况下, 从源节点s到其他任意节点t的路径长度.
维护一个dist
数组, dist[i]
就表示(目前为止) s
到i
的最短距离.
对于每个元素, 标记是否其dist
是否已经确定不再更改(或者说维护两个集合: 一个集合的dist
确定, 另一个未确定).
Dijkstra算法是一种贪心策略:
每次在未确定最短路径的节点里挑选距离s最近的那个点, 把这个点标记为已经确定dist
, 然后对从这个点出发的边进行松弛.
为了标记每个点, 这里用一个bool数组表示: determined[i]
为true
表示i
的dist
已经是最短路程, 为false
表示还不确定.
算法如下:
- 初始化
dist[i] = e[s,i]
,determined[i]
全为false - 在dist未确定的元素里(
determined[i]==false
)寻找一个dist最小的节点u: - 标记u的dist已经确定
determined[i]=true
- 用u的所有出边进行松弛:
s
到i
如果经过(u,i)
这条边会不会变近?dist[i] = min(dist[i], dist[u]+e[u,i])
- 重复循环直到所有的点都确定
dist
(or 重复N遍即可: 每次只会确定一个新的节点的距离)
伪代码
for i in 1~n:
dist[i] = e[s,i]
determined[i] = false
loop N times:
u = argmin(dist[i]) among all i that determined[i]==false
determined[u] = true // determine one node at each loop
for v such that e[u,v]<INF:
dist[v] = min( dist[v], dist[u]+e[u,v] )
完整代码
int[] Dijkstra(int e[][], int N) {
int min, pos = 1;
int[] dist = new int[N + 1];
boolean[] determined = new boolean[N + 1];
// 初始化所有节点的dist[]数组为其到源点的距离
for (int i = 1; i <= N; i++) {
dist[i] = e[1][i];
determined[i] = false;
}
determined[1] = true;
for (int i = 1; i <= N; i++) {
// 在dist未确定的元素里(determined[i]==false)寻找一个dist最小的节点u
min = Integer.MAX_VALUE;
for (int j = 1; j <= N; j++) {
// 找到一个距离源点最近的节点
if (!determined[j] && dist[j] < min) {
pos = j;
min = dist[j];
}
}
// 表示源点到所有其他节点都不可达
if (min == Integer.MAX_VALUE) {
break;
}
// 标记pos节点为已访问的节点
determined[pos] = true;
for (int j = 1; j <= N; j++) {
// 如果i节点经由pos节点到达j节点的距离小于i节点直接到j节点的距离(即i->pos->j的距离小于i->j的距离),则更新j节点的dist[j]值
if (!determined[j] && dist[pos] + e[pos][j] < dist[j]) {
dist[j] = dist[pos] + e[pos][j];
}
}
}
return dist;
}
以上代码的复杂度为O(N^2), 不过如果用堆来优化寻找最近的u的距离, 复杂度可以变得更低.
有负权边的单源最短路径: Bellman-Ford
Dijkstra算法的缺点在于不能处理边长为负数的情况, 而这就是Bellman-Ford算法解决的.
Bellman算法也是一种动态规划(动态规划这个东西就是Bellman提出来的):
- 定义
d(i,k)
为源点s
到i
最多经过k
条边的最短距离 - 初始化:
d(i,1)=e[s,i]
- 每次更新
di
的公式:for all (u,v): d(v,k)=min( d(v,k-1), d(u,k-1)+e[u,v] )
- 更新n-1次
为什么是更新n-1次? 因为s到i的最短路径至多只有n-1条边(即s到i的路径经过了所有n个点).
注意每次更新, 需要把所有的边试一遍: 看看用每条边(u,v
)能不能松弛dv
.
伪代码
for i in 1~n:
dist[i] = e[s,i]
loop n-1 times:
for all (u,v) such that e[u,v]<INF:
dist[v] = min( dist[v], dist[u]+e[u,v] )
核心代码也是最后三行… 太tm精妙了!!
外层循环N-1次, 内层循环M次, 所以代码的复杂度是O(NM).