Floyd求最短路
用邻接矩阵表示图,如果 V i V_i Vi和 V j V_j Vj有点可到达,那么更新 G [ i ] [ j ] G[i][j] G[i][j],否则 G [ i ] [ j ] G[i][j] G[i][j]为无穷大(一般设为 0 x 3 f 3 f 3 f 3 f 0x3f3f3f3f 0x3f3f3f3f,这样两个无穷大相加也不会溢出)。另外需要注意的是 G [ i ] [ i ] = 0 G[i][i]=0 G[i][i]=0,因为我们要默认一个点到本身的最短路为 0 0 0,如果忘记有的题目会卡。
算法思想
动态规划,对于任何两个节点 i i i和 j j j而言, i i i到 j j j的最短距离不外乎存在 i i i与 j j j之间经过 k k k和不经过 k k k两种情况,这样我们设 G [ i ] [ j ] [ k ] G[i][j][k] G[i][j][k]为 i i i与 j j j之间经过 k k k和不经过 k k k两种情况,枚举时就先枚举 i i i再枚举 j j j再枚举 k k k,但是这样的空间开销大。于是可以省去数组的第三维,改为先枚举 k k k然后分别枚举 i , j i,j i,j,这样 D P DP DP数组就定义为 G [ i ] [ j ] G[i][j] G[i][j]表示 i i i到 j j j的最短距离。
松弛时比较 G ( i , j ) G(i,j) G(i,j)与 G ( i , k ) + G ( k , j ) G(i,k)+G(k,j) G(i,k)+G(k,j)的大小。在此 G ( i , k ) + G ( k , j ) G(i,k)+G(k,j) G(i,k)+G(k,j)分别是目前为止所知道的 i i i到 k k k与 k k k到 j j j的最短距离,因此 G ( i , k ) + G ( k , j ) G(i,k)+G(k,j) G(i,k)+G(k,j)就是 i i i到 j j j经过 k k k的最短距离。所以若有 G ( i , j ) > G ( i , k ) + G ( k , j ) G(i,j)>G(i,k)+G(k,j) G(i,j)>G(i,k)+G(k,j),就表示从 i i i出发经过 k k k再到 j j j的距离要比原来的 i i i到 j j j距离短,自然把 i i i到 j j j的 G ( i , j ) G(i,j) G(i,j)重写为 G ( i , k ) + G ( k , j ) G(i,k)+G(k,j) G(i,k)+G(k,j),每当一个 k k k比较完了, G ( i , j ) G(i,j) G(i,j)就是目前的i到j的最短距离。重复这一过程,最后当遍历完所有的 k k k时,对于任意的 i , j i,j i,j, G ( i , j ) G(i,j) G(i,j)里面存放的就是 i i i到 j j j之间的最短距离。
时间复杂度 O ( n 3 O(n^3 O(n3),空间复杂度 O ( V 2 ) O(V^2) O(V2),应用于求多源最短路(可以含有负权但不能含有负环)
代码实现:
int G[505][505]; //邻接矩阵
int n; //n是节点个数
void init() {
memset(G, 0x3f, sizeof(G));
for (int i = 1; i <= n; i++) G[i][i] = 0; //每个节点到自身的最短路为0
}
void Floyd(int n) {
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (G[i][j] > G[i][k] + G[k][j])
G[i][j] = G[i][k] + G[k][j];
}
}
}
}
Floyd打印最短路径
我们用 P a t h Path Path数组保存路径, P a t h [ i ] [ j ] Path[i][j] Path[i][j]代表从 i i i到 j j j的下一个节点。初始肯定是假设每个 i i i都直接和 j j j相连,那么 P a t h [ i ] [ j ] Path[i][j] Path[i][j]就是 j j j。
如果某一点 k k k能够在 i , j i,j i,j之间松弛,那么令 P a t h [ i ] [ j ] = P a t h [ i ] [ k ] Path[i][j]=Path[i][k] Path[i][j]=Path[i][k],这样,再加上 P a t h [ k ] [ j ] Path[k][j] Path[k][j]刚好是最短路径 i → k → j i \rightarrow k \rightarrow j i→k→j。
因此我们得到如下代码:
int G[505][505], Path[505][505];
int n;
void init() {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
G[i][j] = (i == j ? 0 : inf);
Path[i][j] = j;
}
}
void Floyd() {
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (G[i][j] > G[i][k] + G[k][j]) {
G[i][j] = G[i][k] + G[k][j];
Path[i][j] = Path[i][k];
}
}
void Print(int u, int v) {
if (u == v) {
printf("%d\n", v); //最后一个点
return;
}
printf("%d ", u); //因为考虑到先打印起始节点,所以先打印后递归
Print(Path[u][v], v);
}
求最小字典序的路径
我们知道两个节点之间的最短路径不一定唯一,那么如何求打印的字典序最小的路径?
只需要在 F l o y d Floyd Floyd的三重循环里添加一个特判:
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
if (G[i][j] > G[i][k] + G[k][j]) {
G[i][j] = G[i][k] + G[k][j];
Path[i][j] = Path[i][k];
} else if (G[i][j] == G[i][k] + G[k][j] && Path[i][j] > Path[i][k]) //即只要最短路径相同我们只保存序号最小的节点
Path[i][j] = Path[i][k];
}
Floyd求最小环
最小环
从起点出发,经过一条简单路径回到起点成为环。图的最小环就是所有环中长度最小的。有向图的环至少含 2 2 2个顶点,而无向图的环至少含 3 3 3个顶点。
怎么求最小环
无向图:
F l o y d Floyd Floyd保证了最外层循环到 k k k 时所有顶点间已求得以 [ 1 , k − 1 ] [1,k-1] [1,k−1]为中间点的最短路径。一个环至少有 3 3 3 个顶点,设某环编号最大的顶点为 k k k,在环中直接与之相连的两个顶点编号分别为 i i i 和 j j j ( i , j , k i,j,k i,j,k两两不同且 i , j < k i,j<k i,j<k),则最大编号为 k k k 的最小环长度即为 G ( j , k ) + G ( k , i ) + d ( i , j ) G(j,k) + G(k,i) +d(i,j) G(j,k)+G(k,i)+d(i,j),其中 d ( i , j ) d(i,j) d(i,j)表示以 [ 1 , k − 1 ] [1,k-1] [1,k−1] 号顶点为中间点时的最短路径,刚好符合 F l o y d Floyd Floyd 最外层循环到 k k k 时的情况,则经过最外层 k k k 次循环,即可找到整个图的最小环。
注意下面是先找[1,k-1]的最小环,再求经过k点松弛的最短路
有向图:
由于此时最小环至少包含两个节点,那么进行一次 F l o y d Floyd Floyd就得到最小环 d [ i ] [ j ] d[i][j] d[i][j]。
#define INF 10000000 //因为下面涉及到三个数相加,那么0x3f3f3f3f会溢出int
int G[505][505], d[505][505];
int n;
void init() {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
G[i][j] = d[i][j] = (i == j ? 0 : INF);
}
}
}
int Floyd() {
int ans = INF;
for (int k = 1; k <= n; k++) {
for (int i = 1; i < k; i++) {
for (int j = i + 1; j < k; j++) { //保证3个不相同的点
ans = min(ans, G[i][k] + G[k][j] + d[i][j]); //取最小的环
}
}
//正常的求i,j经过点k松弛的最短路
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
}
}
return ans;
}
求出最短路的数量
无向图
不难想到最终从终点到起点的所有最短路构成的是一颗搜索树,那么我们只需沿着终点开始搜到起点。对于边 u − v u - v u−v,如果起点到 u u u的最短路等于起点到 v v v的最短路加上 w ( u , v ) w(u,v) w(u,v),那么就可以继续搜索,每搜到一次起点答案加一。
有向图
思想和上面类似,只需再反向建图。
int s, e, cnt; //起点,终点,答案
int g[505][505], G[505][505]; //g数组为任意两点间的最短路,G数组为初始的图
void dfs(int u) {
if (u == s) {
cnt++;
return;
}
for (int i = 1; i <= n; i++) {
if (i != u && G[u][i] < inf && g[s][u] == g[s][i] + G[u][i]) {
dfs(i);
}
}
}
void solve () {
dfs(e);
cout << cnt << endl;
}
Floyd求传递闭包
若 a > b , b > c a>b,b>c a>b,b>c能够推出 a > c a>c a>c,那么 a , c a,c a,c关系的确定就是一个传递闭包。对于这样的给定大小、排名关系的问题,只需要当成图然后跑 f l o y d floyd floyd算法,常见模板如下:
int vis[505][505];
int n;
void Floyd() {
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (vis[i][k] && vis[k][j])
vis[i][j] = 1;
}
}
}
}