摘自《啊哈!算法》 第六章
5行求最短路径,时间复杂度O(n^3)
Floyd-Warshall 算法用来找出每对点之间的最短距离。它需要用邻接矩阵来储存边,这个算法通过考虑最佳子路径来得到最佳路径。
可以在任何图中使用,包括有向图、带负权边的图。
for(k = 1;k <= n; k++){
for(i = 1; i<= n; i++){
for(j = 1; j<=n;j++){
if(e[i][k] + e[k][j] < e[i][j]){
e[i][j] = e[i][k] + e[k][j];
}
}
}
}
如果时间复杂度要求不高,使用Floyd-Warshall来求指定两点之间的最短路径或者指定一个点到其余各个顶点的最短路径也是可行的。
单源最短路径:从指定的一个点(源点)到其余各个顶点的最短路径。
从一个顶点到其余各顶点的最短路径算法,解决的是有向图中最短路径问题。Dijkstra算法主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。
与Floyd-Warshall算法一样,这里也用邻接矩阵存储顶点之间的关系。
e:
1 2 3 4 5 6
1 0 1 12 # # #
2 # 0 9 3 # #
3 # # 0 # 5 #
4 # # 4 0 13 15
5 # # # # 0 4
6 # # # # # 0
用一个一位数组dis存储1号顶点到其余各个顶点的初始路程:
1 2 3 4 5 6
dis 0 1 12 # # #
先找一个离1号顶点最近的顶点,这里是2号顶点,即dis[2]确定为1号顶点到2号顶点的最短路程。
然后比较dis[3]=12 和 dis[2] + e[2][3]=10
dis[4]=# 和 dis[2] + e[2][4]=4
的距离,通过松弛来更新路程。然后再选取一个最小的边作为确定值,接着松弛…
即Dijkstra算法的主要思想是:通过边来松弛1号顶点到其余各个顶点的路程。每次找到离源点最近的一个顶点,然后以该点为中心进行扩展,最终得到源点到其余所有点的最短路径。
#include<stdio.h>
int main(){
int e[10][10], dis[10], book[10];
int n, m, inf = 99999999; // n:顶点个数 m:边的条数
int i, j, t1, t2, t3;
int min, min_index;
printf("输入顶点个数n和边的条数m: ");
scanf("%d %d",&n,&m);
//初始化
for(i = 1; i <= n; i++){
for(j = 1;j <= n; j++){
if(i == j){
e[i][j] = 0;
}
else e[i][j] = inf;
}
}
//输入边的信息
for(i = 1; i<=m; i++){
scanf("%d %d %d",&t1, &t2,&t3);
e[t1][t2] = t3;
}
//初始化dis数组,这里是1号顶点到其他点的初始距离
for(i = 1; i<=n; i++){
dis[i] = e[1][i];
}
//book数组初始化
for(i = 1;i <= n;i++){
book[i] = 0;
}
book[1] = 1;
//Dijkstra算法核心
for(i = 1;i <= n - 1; i++){
min = inf;
for(j = 1; j <= n; j++){
if(book[j] == 0 && dis[j] < min){
min = dis[j];
min_index = j;
}
}
book[min_index] = 1;
for(v = 1; v <= n; v++){
if(e[min_index][v] < inf){
if(dis[v] > dis[min_index] + e[min_index][v]){
dis[v] = dis[min_index] + e[min_index][v];
}
}
}
}
//输出最后结果
for(i = 1; i<=n; i++){
printf("%d ",dis[i]);
}
getchar();
return 0;
}
Dijkstra算法的时间复杂度为O(N^2),每次找到离源点最近的顶点的时间复杂度为O(N),用堆优化后可为O(logN)。
对于边m远小于N^2的稀疏图,可以用邻接表来存储稀疏图,可以将整个时间复杂度优化到O(M+N)logN,最坏情况下M就是N^2.
4 5
1 4 9
2 4 6
1 2 5
4 3 8
1 3 7
// 4个顶点,5条边
int n, m, i;
// u, v, w 的数组大小要根据实际情况来设置,要比m的最大值要大1
int u[6], v[6], w[6]
// first, next的数组大小要根据实际情况来设置,要比n的最大值大1
int first[5], next[5];
scanf("%d %d", &n, &m);
//初始化first数组下标1-n的值为-1, 表示1-n顶点暂时都没有边
for(i = 1; i <= n; i++){
first[i] = -1;
}
for( i = 1; i <= m; i++){
scanf("%d %d %d", &u[i], &v[i], &w[i]);
//关键
next[i] = first[u[i]];
first[u[i]] = i;
}
first数组的[1-n]号单元格分别用来存储1-n号顶点的第一条边的编号;
next数组存储“编号为i的边”的“下一条边”的编号。
有点绕口,就是,first数组存的是每个顶点出发的第一条边的编号,next数组存的是某条边的下一条边。
first = {-1}
4 5 4个顶点 5条边
1 4 9 next[1] = first[u[1]] = first[1] = -1; first[1] = 1;
2 4 6 next[2] = first[u[2]] = first[2] = -1; first[2] = 2;
1 2 5 next[3] = first[u[3]] = first[1] = 1 ; first[1] = 3;
4 3 8 next[4] = first[u[4]] = first[4] = -1; first[4] = 4;
1 3 7 next[5] = first[u[5]] = first[1] = 3 ; first[1] = 5;
所以first的结果是:
0 1 2 3 4
# 5 2 -1 4
遍历每条边:
for(i = 1; i<= n; i++){
k = first[i];
while(k != -1){
printf("%d %d %d\n",u[k], v[k], w[k]);
k = next[k]
}
}
Bellman - ford算法是求含负权图的单源最短路径算法,效率很低,但代码很容易写。其原理为持续地进行松弛(原文是这么写的,为什么要叫松弛,争议很大),在每次松弛时把每条边都更新一下,若在n-1次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。Bellman - ford算法有一个小优化:每次松弛先设一个标识flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出。Bellman-ford算法浪费了许多时间去做没有必要的松弛,而SPFA算法用队列进行了优化,效果十分显著,高效难以想象。SPFA还有SLF,LLL,滚动数组等优化。
——百度百科
//对单个源点来说
for(k = 1; k <= n - 1; k++){ // n:顶点数
for(i = 1; i <= m; i++){ // m:边的条数
if(dis[v[i]] > dis[[u[i]] + w[i]){
// dis[v[i]]: 源点到顶点v[i]的距离
// dis[u[i]] + w[i]: 源点到顶点u[i]的距离 + u[i] 到 v[i]的距离
dis[v[i]] = dis[u[i]] + w[i];
}
}
}
#include<stdio.h>
int main(){
int dis[10], u[10], v[10], w[10];
int inf = 99999999;
int n,m;
int i,k;
scanf("%d %d",&n,&m); // n:顶点数 m: 边的条数
for(i=1; i<= m; i++){
scanf("%d %d %d",&u[i], &v[i], &w[i]);
}
for(i = 1; i <= n; i++){
dis[i] = inf;
}
dis[1] = 0;
// Bellman-Ford算法核心语句
for(k = 1; k<= n - 1; k++){
for(i = i; i <= m; i++){
if(dis[v[i]] > dis[[u[i]] + w[i]){
dis[v[i]] = dis[u[i]] + w[i];
}
}
}
flag = 0;
//检测是否是负权回路
for(i = 1; i <=m; i++){
if(dis[v[i]] > dis[u[i]] + w[i])
flag = 1;
}
if(flag == 1){
printf("此图含有负权回路");
}
for( i = 1; i <= n; i++){
printf("%d ", dis[i]);
}
getchar();
return 0;
}
对Bellman-Ford的优化:每次仅对最短路程发生变化了的点的相邻边执行松弛操作,但是如何知道当前哪些点的最短路径发生了变化呢?可以用一个队列来维护这些点。
数据:
5 7
1 2 2
1 5 10
2 3 3
2 5 7
3 4 4
4 5 5
5 3 6
#include<stdio.h>
int main(){
int n, m, i, j, k;
int u[8], v[8], w[8];
int first[6] = {-1}; //每个节点的第一条边 >= n+1
int next[8]; //每条边的下一条边 >= m+1
int dis[6] = {0}, book[6] = {0}; // book数组用来记录哪些顶点已经在队列中
int que[101] = {0}, head = 1, tail = 1;
int inf = 99999999;
//初始化图的邻接表存储
scanf("%d %d", &n, &m); //5 7
for(i = 1; i <= m; i++){
scanf("%d %d %d",&u[i], &v[i], &w[i]);
next[i] = first[u[i]];
first[u[i]] = i;
}
// 1号顶点入队
que[tail] = 1;
tail++;
book[1] = 1
while(head < tail){
k = first[que[head]]; // 当前需要处理的队首顶点
while(k != -1){
if(dis[v[k]] > dis[u[k]] + w[k]){ //判断是否松弛成功
dis[v[k]] = dis[u[k]] + w[k];
//book数组用来判断顶点v[k]是否在队列中,
//如果不使用数组来标记,则每次判断都要从队列的head到tail扫一遍,很浪费时间
if(book[v[k]] == 0){
que[tail] = v[k];
tail++;
book[v[k]] = 1;
}
}
k = next[k];
}
book[que[head]] = 0;
head++;
}
//输出结果
for(i = 1; i<=n; i++){
printf("%d ", dis[i]);
}
getchar();
return 0;
}
- | Floyd | Dijkstra | Bellman-Ford | 队列优化的Bellman-Ford |
---|---|---|---|---|
空间复杂度 | O(N^2) | O(M) | O(M) | O(M) |
时间复杂度 | O(N^3) | O((M+N)logN) | O(NM) | 最坏也是O(NM) |
适用情况 | 稠密图,和顶点关系密切 | 稠密图,和顶点关系密切 | 稀疏图,和边关系密切 | 稀疏图,和边关系密切 |
负权 | 可以解决负权 | 不能解决 | 可以解决 | 可以解决 |