Floyd算法的产生背景
Robert W. Floyd ,在学习数据结构的过程中,我们总会了解到各种各样的顶级天才,但Floyd却不同于大多数一开始就研究计算机相关领域的天才,他属于"自学成才",这么说是因为他本科毕业拿到的是文学学位(值得一提的是,Floyd出生于1936年,而他在1953年就从芝加哥大学本科毕业了,这个时候他才17岁)。
本科毕业后,由于专业限制,找不到对口工作的Floyd只能去当计算机操作员(这个职位并不需要掌握专业知识,属于难度不高的职位),但是对Floyd这样的天才而言,光干这种没啥技术含量的活没意思,所以他利用值夜班的空闲时间自学并程序员请教,白天跑到母校去旁听相关课程,凭借这样的毅力在1958年(此时他22岁)又拿到了物理学的第二学位。
从那以后,Floyd便正式进入了计算机这个行业,1962年被Computer Associates公司聘为分析员,1965年应聘成为卡内基---梅隆大学的副教授,3年后转至斯坦福大学。1970年,Floyd被聘任为教授,而后于1978年获图灵奖。而Floyd-Warshall算法就是Floyd在1962年结合Stephen Warshall在传递闭包问题上的研究工作后演化出了如今的版本
Floyd算法的概念
小故事讲完,我们来看看Floyd算法的概念:Floyd算法,又称Floyd-Warshall算法或插点法,是一种利用动态规划思想 来求解加权图中任意两个节点之间最短路径问题的算法
那么Floyd是怎么应用动态规划思想的呢?我们先来看问题,要求解加权图中任意两个节点间最短路径,显然当起点无法直接到达终点时,应该从图中另外的顶点中转,想要找出最短路径的话 就应该遍历所有可以中转的顶点。
下面我们来看一个例子

对于这样一个有向加权图,我们使用邻接矩阵存储它的结果如下:
|-------|-------|-------|-------|-------|
| | 1 | 2 | 3 | 4 |
| 1 | 0 | 2 | 3 | ∞ |
| 2 | ∞ | 0 | 3 | ∞ |
| 3 | 5 | ∞ | 0 | 4 |
| 4 | 4 | ∞ | 5 | 0 |
规定自己到自己的路径长度为0,从某一顶点无法直接到达另一顶点则规定路径长度为无穷大
然后我们来模拟中转的过程:
1.允许通过顶点1进行中转:更新路径3->1->2、4->1->2
|-------|-------|-------|-------|-------|
| | 1 | 2 | 3 | 4 |
| 1 | 0 | 2 | 3 | ∞ |
| 2 | ∞ | 0 | 3 | ∞ |
| 3 | 5 | 7 | 0 | 4 |
| 4 | 4 | 6 | 5 | 0 |
2.允许通过顶点1和顶点2进行中转:通过顶点2没有能缩短的路径,矩阵信息不变
|-------|-------|-------|-------|-------|
| | 1 | 2 | 3 | 4 |
| 1 | 0 | 2 | 3 | ∞ |
| 2 | ∞ | 0 | 3 | ∞ |
| 3 | 5 | 7 | 0 | 4 |
| 4 | 4 | 6 | 5 | 0 |
3.允许通过顶点1、2、3中转:更新路径1->3->4、2->3->4
|-------|-------|-------|-------|-------|
| | 1 | 2 | 3 | 4 |
| 1 | 0 | 2 | 3 | 7 |
| 2 | ∞ | 0 | 3 | 7 |
| 3 | 5 | 7 | 0 | 4 |
| 4 | 4 | 6 | 5 | 0 |
4.允许通过顶点1、2、3、4中转:更新路径2->4->1
|-------|-------|-------|-------|-------|
| | 1 | 2 | 3 | 4 |
| 1 | 0 | 2 | 3 | 7 |
| 2 | 8 | 0 | 3 | 7 |
| 3 | 5 | 7 | 0 | 4 |
| 4 | 4 | 6 | 5 | 0 |
所有中转结束后的邻接矩阵变为这样:
|-------|-------|-------|-------|-------|
| | 1 | 2 | 3 | 4 |
| 1 | 0 | 2 | 3 | 7 |
| 2 | 8 | 0 | 3 | 7 |
| 3 | 5 | 7 | 0 | 4 |
| 4 | 4 | 6 | 5 | 0 |
此时图中任意两个顶点间最短路径就是上面表格中的值
Floyd算法代码实现
刚才我们通过更新邻接矩阵这样的可视化方式演示了Floyd算法的逻辑,那怎么用代码实现它呢?我们可以使用一个三维数组dist[k][i][j] 来表示只允许经过顶点0 到k 中转时,从起点i 到终点j的最短路径长度
k=0 只允许经过顶点0中转
k=1 允许经过顶点0和1中转
k=2 允许经过顶点0,1,2中转
......
k=n-1 允许经过所有顶点中转
显然当k的值更新后,我们需要比较不经过第k个顶点的路径长度与经过第k个的顶点中转后的路径长度的大小,并取更小的值 作为新的最短路径长度,即比较 dist[k-1][i][j]和 dist[k-1][i][k] + dist[k-1][k][j] 的大小(dist[k-1][i][k] + dist[k-1][k][j] 表示从 i 到 k 的路径 + 从 k 到 j 的路径,拼起来就变成了经过顶点 k 从 i 到 j 的路径),于是我们便有了如下的方程:
学过动态规划的小伙伴肯定认识,这就是动态规划里的状态转移方程,也是Floyd算法的核心部分。为了不跑题,动态规划的知识点在此就不详细展开了,我们接着往下看。
有的朋友也许会好奇,为什么定义dist数组的时候要把k放到最前面,定义为dist[k][i][j] ,而不定义为dist[i][j][k] 呢?这是因为dist数组中的 k 表示的是允许使用的中转节点的范围,也就是说,当计算经过第 k 个顶点时,前面 k-1 个顶点的状态必须完全计算好并且保持不变(这个原因 还是出于动态规划的思想)
要想找到所有的最短路径,肯定要遍历整个图,而Floyd算法的遍历采用的是三重循环,最外层枚举可用的中转顶点,中间层枚举起点,最内层枚举终点,写成代码就是这样:
cpp
for(int k=1; k<=n; k++){
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
}
}
}
至此我们便可以写出Floyd算法核心逻辑的代码(C++):
cpp
//Floyd算法
void Floyd(vector<vector<int>> &Graph){
int n = Graph.size()-1;//顶点数
//定义三维数组dist,初始全为极大值
vector<vector<vector<int>>> dist(n + 1, vector<vector<int>>(n + 1, vector<int>(n + 1, INF)));
//初始化dist[0],即不通过任何中转顶点的邻接矩阵
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dist[0][i][j] = Graph[i][j];
}
}
//三重循环
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dist[k][i][j] = min(dist[k-1][i][j], dist[k-1][i][k] + dist[k-1][k][j]);//状态转移方程
}
}
}
//将结果复制回Graph
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
Graph[i][j] = dist[n][i][j];
}
}
}
可以看到,使用三维数组的话能直观感受到k的变化,但是这样的空间复杂度 会变成O(n^3) ,能不能进一步降低空间复杂度呢?答案是可以的,既然我们已经了解了Floyd算法的思想,由于在计算 dist[i][j] 时,它的值在这一次循环中要么不变,要么被覆盖为更小的值,省略对状态的记录也不改变这个性质,那我们就可以不再保留形式上的历史状态dist[k-1],直接在原数组上进行更新,这样的话dist就变成了二维数组 ,空间复杂度也降到了O(n^2),函数代码如下:
cpp
//Floyd算法(二维数组版)
void Floyd(vector<vector<int>> &dist) {
int n = dist.size() - 1;//顶点数
// 三重循环
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);//状态转移方程
}
}
}
}
到这里Floyd算法的基本内容就完结了,感谢大家阅读!下面附上三维数组和二维数组两个版本的完整代码:
三维数组版
cpp
//三维数组版
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int INF = 0x3f3f3f3f;//定义一个无穷大值
//Floyd算法三维数组版
void Floyd(vector<vector<int>> &Graph) {
int n = Graph.size() - 1;//顶点数
//定义三维数组dist
vector<vector<vector<int>>> dist(n + 1, vector<vector<int>>(n + 1, vector<int>(n + 1, INF)));
//初始化dist[0],即不通过任何中转顶点的邻接矩阵
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
dist[0][i][j] = Graph[i][j];
}
}
//三重循环
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
// 状态转移方程
dist[k][i][j] = min(dist[k-1][i][j], dist[k-1][i][k] + dist[k-1][k][j]);
}
}
}
//将结果复制回Graph
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
Graph[i][j] = dist[n][i][j];
}
}
}
//输出结果
void Print(vector<vector<int>> &dist) {
int n = dist.size() - 1;//顶点数
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dist[i][j] == INF) {
cout << i << "到" << j << "的最短距离是:无穷大" << endl;
} else {
cout << i << "到" << j << "的最短距离是:" << dist[i][j] << endl;
}
}
}
}
int main() {
int n, m;//顶点数 边数
cin >> n >> m;
//定义二维邻接矩阵
vector<vector<int>> Graph(n + 1, vector<int>(n + 1, INF));
//初始化自己到自己的距离为0
for (int i = 1; i <= n; i++) {
Graph[i][i] = 0;
}
//读边
int u, v, w;
for (int i = 0; i < m; i++) {
cin >> u >> v >> w;
Graph[u][v] = w;
}
Floyd(Graph);
Print(Graph);
return 0;
}
/*
4 7
1 2 2
1 3 3
2 3 3
3 1 5
3 4 4
4 1 4
4 3 5
1到1的最短距离是:0
1到2的最短距离是:2
1到3的最短距离是:3
1到4的最短距离是:7
2到1的最短距离是:8
2到2的最短距离是:0
2到3的最短距离是:3
2到4的最短距离是:7
3到1的最短距离是:5
3到2的最短距离是:7
3到3的最短距离是:0
3到4的最短距离是:4
4到1的最短距离是:4
4到2的最短距离是:6
4到3的最短距离是:5
4到4的最短距离是:0
*/
二维数组版
cpp
//二维数组版
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int INF = 0x3f3f3f3f;//定义一个无穷大值
//Floyd算法二维数组版
void Floyd(vector<vector<int>> &dist) {
int n = dist.size() - 1;//顶点数
//三重循环
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
//状态转移方程
//直接修改dist
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
}
//输出结果
void Print(vector<vector<int>> &dist) {
int n = dist.size() - 1;//顶点数
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dist[i][j] == INF) {
cout << i << "到" << j << "的最短距离是:无穷大" << endl;
} else {
cout << i << "到" << j << "的最短距离是:" << dist[i][j] << endl;
}
}
}
}
int main() {
int n, m;//顶点数 边数
cin >> n >> m;
//定义二维邻接矩阵
vector<vector<int>> Graph(n + 1, vector<int>(n + 1, INF));
//初始化自己到自己的距离为0
for (int i = 1; i <= n; i++) {
Graph[i][i] = 0;
}
//读边
int u, v, w;
for (int i = 0; i < m; i++) {
cin >> u >> v >> w;
Graph[u][v] = w;
}
Floyd(Graph);
Print(Graph);
return 0;
}
/*
4 7
1 2 2
1 3 3
2 3 3
3 1 5
3 4 4
4 1 4
4 3 5
1到1的最短距离是:0
1到2的最短距离是:2
1到3的最短距离是:3
1到4的最短距离是:7
2到1的最短距离是:8
2到2的最短距离是:0
2到3的最短距离是:3
2到4的最短距离是:7
3到1的最短距离是:5
3到2的最短距离是:7
3到3的最短距离是:0
3到4的最短距离是:4
4到1的最短距离是:4
4到2的最短距离是:6
4到3的最短距离是:5
4到4的最短距离是:0
*/