最短路径之Floyd算法(数据结构)

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] 来表示只允许经过顶点0k 中转时,从起点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] 表示从 ik 的路径 + 从 kj 的路径,拼起来就变成了经过顶点 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
*/
相关推荐
小O的算法实验室2 小时前
2026年SEVC,直觉模糊不确定环境下求解绿色多物品固定费用五维运输问题的多目标进化算法,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
SilentSlot2 小时前
【数据结构】红黑树定义及基本操作
数据结构
海海不瞌睡(捏捏王子)2 小时前
Unity A*寻路算法
算法·unity
jaysee-sjc2 小时前
【项目三】用GUI编程实现局域网群聊软件
java·开发语言·算法·安全·intellij-idea
wangchunting3 小时前
数据结构-树
java·数据结构
DC...3 小时前
【力控】混合位置 / 力控制
算法·机器人·力控
Rabitebla3 小时前
归并排序(MergeSort)完全指南 —— 从原理到非递归实现
c语言·数据结构·c++·算法·排序算法
寒秋花开曾相惜3 小时前
(学习笔记)3.9 异质的数据结构(3.9.1 结构)
c语言·网络·数据结构·数据库·笔记·学习
WBluuue3 小时前
Codeforces Educational 188(ABCDEF)
c++·算法