图的遍历,通过算法优雅实现。
上次使用遍历的方法求得最短路径(图的遍历。-CSDN博客),这样虽然可以解决问题,但还是不够优雅,有一些弊端,时间复杂度和空间复杂度都比较高。本博客主要描述三种求最短路径的办法:Floyd-Warshall、Dijkstra、Bellman-Ford。
这些算法本质上还是实现了图的遍历,但更加优雅、简便、高效,我认为这就是算法的意义。
Floyd-Warshall
Floyd-Warshall可以用来解决多源最短路径和判断负权回路问题。核心代码只有五行,比较精简,虽然用到了动态规划的思想,但理解起来不算困难。
我们有如图所示的两个有向图,求1->5的最短路径。每个有向图有三条路径:1->5、1->2->5、1->3->4->5;观察可见,a图中1->3->4->5最短,b图中1->5最短,那么我们如何让计算机知道哪一个路径最短呢?
首先我们达成一个共识,两点之间的距离,通过引入(插入)n个点,可以使路径缩短,如a->b,我们可以通过引入一个点(a->k->b),引入两个点(a->k1->k2->b)来缩短。
我们先定义两个点不经过另外任意顶点的路径为初始路径。通过在两点之间插入其它顶点,来达到缩小距离的目的,
那么我们如何插入呢?我们可以先试一试每个顶点中间插入一个顶点看看有没有缩短,再试试插入两个顶点有没有缩短,一直到插入n个顶点有没有缩短,每一轮都在上一次插入得到的最短路径基础上继续插入,这样插入n次便得到了任意两个顶点之间的最短路径。
因为我们如图所示,并不是每次插入顶点之后,两点之间的路径都会缩短,如果没有缩短的路径,我们就不需要在他的基础上继续插入了;那么有没有可能插入一个点距离没有缩短,在这个基础上再插入一个点距离就缩短了呢?
答案是肯定有的,比如如图a,我们只经过3到5,3和5之间没有路,距离无穷大,但加上4之后,总路径就会缩短;那么这种情况有没有考虑呢?肯定也是有的,1->4->5这条路径,其中1->4已经包含了3顶点,其实就是1->3->4->5。我们得到的这条路径,就等于1->3、3->4、4->5这三条最短路径相加的结果。
这就是动态规划的思想,通过不断优化每两个顶点的间的最短距离来实现任意两点之间的最短距离。
例题
将本图中,顶点和边的关系,存入邻接矩阵,若两个点之间没有路径,距离为正无穷,点到其本身的路径为0.,我们便得到了如图所示的初始路径map[n+1][n+1](从1开始)。
通过以上方法来设计本题程序:因为是有向图,我们不仅要找从i到j的路径,还要找j到i的路径,他们的路径都有可能通过插入一个点从而距离缩短:
只通过e寻找任意两点路径的代码如下:
for (int i = 1; i <= n; i++)for (int j = 1; j <= n; j++)if (map[i][j]>map[i][e]+map[e][j])map[i][j]=map[i][e]+map[e][j];
只通过1:
在此基础上通过2(即只允许通过1和2):
在此基础上通过3(即只允许通过1和2和3):
在此基础上通过4(即允许通过所有顶点),得到最终结果:
核心代码:
for (int e = 1; e <= n; e++)for (int i = 1; i <= n; i++)for (int j = 1; j <= n; j++)if (map[i][j]>map[i][e]+map[e][j])map[i][j]=map[i][e]+map[e][j];
输入输出部分;
输入:输入m+1行,第一行输入n和m代表顶点和边,接下来的m行每行包含三个变量x,y,z表示从x到y的距离为z。
4 8
1 2 2
1 3 6
1 4 4
2 3 3
3 1 7
3 4 1
4 1 5
4 3 12
输出:n*n的邻接矩阵
0 2 5 4
9 0 3 4
6 8 0 1
5 7 10 0
完整代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 0x3f3f3f3fint** init(int n,int m);
void free_output(int **map,int n);int main()
{int n,m;scanf("%d%d",&n,&m);int** map=init(n+1,m); for (int e = 1; e <= n; e++)for (int i = 1; i <= n; i++)for (int j = 1; j <= n; j++)if (map[i][j]>map[i][e]+map[e][j])map[i][j]=map[i][e]+map[e][j]; free_output(map,n);return 0;
}int** init(int n,int m)
{/*****二维数组动态分配内存和初始化************************ */int **map;map=(int **)malloc(sizeof(int*)*n);//二级指针分配n个一级指针for (int i = 0; i < n; i++)map[i]=(int *)malloc(sizeof(int)*n);//一级指针开辟空间for (int i = 0; i < n; i++)memset(map[i],inf,sizeof(int)*n);//二维数组初始化/******可用map[100][100]={0}代替************************ */for (int i = 0,x=0,y=0,flag; i < m; i++){scanf("%d %d %d",&x,&y,&flag);map[x][y]=flag;}for (int i = 0; i <= n-1; i++)map[i][i]=0;return map;
}void free_output(int **map,int n)
{for (int i = 1; i <= n; i++){for (int j = 1; j <= n; j++)printf("%5d",map[i][j]);printf("\n");}/*****释放空间****************************** */for (int i = 0; i < n+1; i++){free(map[i]);}free(map);/***** ******************************** */return ;
}
代码采用动态分配内存和模块化,对于宏定义0x3f3f3f,为无穷大,可以通过memset来定义,具体详情参考博客:【C语言中如何表示无穷大】_c语言的无穷大如何表示-CSDN博客
Dijkstra
Dijkstra可以用来解决单源最短路径,但不能判断负权回路问题。基于「贪心」、「广度优先搜索」、「动态规划」。
我们可以确定,与顶点1直接连接的点的所有边中,一定有一个是最短路径,即两点之间没有办法通过插入其它顶点来缩短路径。因为插入的点也是要连接1的,如果说插入过后路径变短,那这个点就不是与1连接的最短路径。(a)图中1顶点与2、3、5都是直接连接。
我们确定与1连接的最短路径的顶点后,如图(a)是3,那么1到3便是一条最短路径,我们通过这条最短路径来更新1到其它顶点的路径(1通过3),再通过3顶点重复上边操作,这样每次确定的都是最短路径,我们就可以得到1顶点到其它任意顶点的最短路径。
那么我们如何设计这个程序呢?
因为是单源最短路径,所以我们可以用一个一维数组dis来存储一个顶点到其它顶点的最短路径。
例题
求1号顶点到2、3、4、5、6号顶点的最短路径。邻接矩阵如下:
初始化dis:
我们发现最短的为1到2,那么这个就是这一轮中的最短路径,可以用book数组标记,
防止下一轮中被算为新的最短路径 (1到1已经被标记),那么我们就再更新1通过2到各个顶点的最短路径,我们发现3和4的被更新的,这个过程叫做松弛。
接下来我们在没有被book标记的其余路径下继续寻找最短路径,然后继续更新,最短路径为1->4(其实是2->4):
这一轮最短路径为8,继续更新,因为从3出发的只有3-5,所以本轮中,值更新了1-5这一条路线,1-6不属于更新范围内:
这一轮最短路径为5,松弛完毕后的dis数组:
最后剩余一条,即是最短路径:
按着红线走经过的顶点便是最短路径,我们会发现每个点只会延伸出一条红线。
核心代码:
for (int j = 1,min,min_u; j <= n-1 ; j++){min=inf;min_u=0;for (int i = 1; i <= n; i++)/*选出未更新中最小的*/{if (book[i]==0&&dis[i]<min){min=dis[i];min_u=i;}}book[min_u]=1;for (int i = 1; i <= n; i++)/*松弛*/{if (dis[i]>dis[min_u]+map[min_u][i]){dis[i]=dis[min_u]+map[min_u][i];}} }
再加上输入输出和初始化模块,得到完整代码。
输入:输入m+1行,第一行输入n和m代表顶点和边,接下来的m行每行包含三个变量x,y,z表示从x到y的距离为z。
6 9
1 2 1
1 3 12
2 3 9
2 4 3
3 5 5
4 3 4
4 5 13
4 6 15
5 6 4
输出:dis数组
0 1 8 4 13 17
完整代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 0x3f3f3f3f/*无穷大*/int** memory_two(int n);/*二维数组分配*/
int* memory_one(int n);/*一维数组分配*/
void init(int **map,int *dis,int n,int m);/*dis和map初始化*/
void free_output(int **map,int *dis,int n);/*释放内存和输出结果*/int main()
{int n,m;scanf("%d%d",&n,&m);int* dis=memory_one(n+1);/*不用0,故传n+1*/int* book=memory_one(n+1);/*不用0,故传n+1*/int** map=memory_two(n+1); /*不用0,故传n+1*/init(map,dis,n,m);book[1]=1;for (int j = 1,min,min_u; j <= n-1 ; j++){min=inf;min_u=0;for (int i = 1; i <= n; i++)/*选出未更新中最小的*/{if (book[i]==0&&dis[i]<min){min=dis[i];min_u=i;}}book[min_u]=1;for (int i = 1; i <= n; i++)/*松弛*/{if (dis[i]>dis[min_u]+map[min_u][i]){dis[i]=dis[min_u]+map[min_u][i];}} }free_output(map,dis,n);return 0;
}int** memory_two(int n)
{/*****二维数组动态分配内存和初始化************************ */int **map;map=(int **)malloc(sizeof(int*)*n);//二级指针分配n个一级指针for (int i = 0; i < n; i++)map[i]=(int *)malloc(sizeof(int)*n);//一级指针开辟空间for (int i = 0; i < n; i++)memset(map[i],inf,sizeof(int)*n);//二维数组初始化/******可用map[100][100]={0}代替************************ */return map;
}int* memory_one(int n)
{int *a=(int *)malloc(n*sizeof(int));memset(a,0,sizeof(int)*n);return a;
}void init(int **map,int *dis,int n,int m)
{for (int i = 0,x=0,y=0,flag; i < m; i++){scanf("%d %d %d",&x,&y,&flag);map[x][y]=flag;}for (int i = 0; i <= n; i++)/*顶点i到i为0*/map[i][i]=0;for (int i = 0; i <= n; i++)/*dis初始化*/dis[i]=map[1][i];return ;
}void free_output(int **map,int *dis,int n)
{for (int j = 1; j <= n; j++)printf("%5d",dis[j]);printf("\n");/*****释放空间****************************** */for (int i = 0; i < n+1; i++){free(map[i]);}free(map);free(dis);/***** ******************************** */return ;
}
这个算法的时间复杂度为O(N^2),其中每次找到离1号顶点最近的顶点的时间复杂度为O(N),这里我们可以用“堆”来优化,使得这一部分时间复杂度降到log(N),另外对于边数M少于N^2的稀疏图来说(我们把M远小于N^2的图叫做稀疏图,而M较大的图称为稠密图),我们可以用邻接表来代替邻接矩阵,使得时间复杂度优化到O(N+M)logN。最坏的情况下M就是N^2,这样的话(M+N)logN要比N^2还要大。但是大多数情况下并不会有那么多边,因此(M+N)logN要比N^2小得多。
邻接表 (Adjacency List)
邻接表是是一种顺序分配和链式分配相结合的存储结构,这里只介绍在有向图中的使用。
这里我们用数组来实现链表,我们首先创建五个数组:first,next,u,v,w;first可以理解为是头节点,next存储下一条边的编号。u,v,w表示从u到v的边的权重为w。
我们从两个方面来理解邻接表,一方面是邻接表的创建,一方面是邻接表的读取
我们存储一组数据来模拟:
4 5
1 4 9
2 4 6
1 2 5
4 3 8
1 3 7
邻接表的创建核心代码:
for (int i = 1; i <= m; i++){scanf("%d%d%d",&u[i],&v[i],&w[i]);next[i]=first[u[i]];first[u[i]]=i; }
将每条边的信息存储到u、v、w中,first的初始值为-1(表示该顶点没有边),
读入第一条边,u[i]为1,故我们先将first[1]里面的值存入next[i],再将next[i]的下标i存入first[u[i]],其中next[i]的下标i对应u[i]、v[i]、w[i];
读入第二条边,同样的操作。
读入第三条边,注意此时的u[i]为1,虽然还是进行了同样的操作,first的值更新为了3,对应的next[3](这里的3代表第三次读入的,而不是顶点3)存储的是前一个first[1]=1(这里的1也代表第一次读入,同时也和next[1]有关系。),
读入第四次和第五次,也是同样的操作。
这样我们便读入了所有的边,邻接表的创建已经完成了,如果还有点不理解,先看邻接表的读入,接着往下看。
图片显示了与1直接先连的边, 5、3、1表示第5次读入的边的信息、第3次读入的边的信息,第1次读入边的信息。对应边:1->3、1->2、1->4,所以邻接表里面并没有直接存储对应的边,而是存储的第几次读入。
要找与1顶点直接连接的边,就找first[1];通过k=first[i]中的k,k有两个含义,一个含义表示一条与1顶点直接连接的边是第k次读入的,其中u[k]为1,v[k]为对应的边,权重为w[k];另一个含义:通过k=next[k],这又得到一条与1顶点直接连接的边是第k次读入的,其中u[k]为1,v[k]为对应的边,权重为w[k];直到k为-1,表示所有与1相连的边都已经找到。
邻接表的读取核心代码:
int k=first[1];/*顶点*/while (k!=-1){printf("%d %d %d",u[k],v[k],w[k]);k=next[k];}
我们会发现,遍历的顺序和读入时候的顺序相反。因为在为每个顶点插入边的时候都是直接插入"链表"的首部而不是尾部。
使用邻接表来存储图的时间空间复杂度为O(M),遍历每一条边的时间复杂度也是O(M),如果一个图是稀疏图的话,M要远小于N^2。
邻接表实现Dijkstra
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 0x3f3f3f3f/*无穷大*/int* memory_one(int n,int x);/*一维数组分配*/
void init(int *u,int *v,int *w,int *first,int *next,int *dis,int m);/*dis和map初始化*/int main()
{int n,m;scanf("%d%d",&n,&m);int *u,*v,*w;/*从u到v,权重为w*/int *first,*next;/*邻接表*/int* dis=memory_one(n+1,0x3f3f3f3f);/*不用0,故传n+1*/int* book=memory_one(n+1,0);/*不用0,故传n+1*/u=memory_one(m+1,0);/*不用0,比m个数多一*/v=memory_one(m+1,0);/*不用0,比m个数多一*/w=memory_one(m+1,0);/*不用0,比m个数多一*/first=memory_one(n+1,-1);next=memory_one(n+1,0);// int** map=memory_two(n+1); /*不用0,故传n+1*/init(u,v,w,first,next,dis,m);book[1]=1;for (int j = 1,min,min_u; j <= n-1 ; j++){min=inf;min_u=0;for (int i = 1; i <= n; i++)/*选出未更新中最小的*/{if (book[i]==0&&dis[i]<min){min=dis[i];min_u=i;}}book[min_u]=1;int k=first[min_u];/*顶点*/while (k!=-1){if (dis[v[k]]>dis[u[k]]+w[k]){dis[v[k]]=dis[u[k]]+w[k];}k=next[k];}}for (int j = 1; j <= n; j++)printf("%5d",dis[j]);printf("\n");free(u);free(v);free(w);free(first);free(next);free(dis);free(book);return 0;
}int** memory_two(int n)
{/*****二维数组动态分配内存和初始化************************ */int **map;map=(int **)malloc(sizeof(int*)*n);//二级指针分配n个一级指针for (int i = 0; i < n; i++)map[i]=(int *)malloc(sizeof(int)*n);//一级指针开辟空间for (int i = 0; i < n; i++)memset(map[i],inf,sizeof(int)*n);//二维数组初始化/******可用map[100][100]={0}代替************************ */return map;
}int* memory_one(int n,int x)
{int *a=(int *)malloc(n*sizeof(int));memset(a,x,sizeof(int)*n);return a;
}void init(int *u,int *v,int *w,int *first,int *next,int *dis,int m)
{ for (int i = 1; i <= m; i++){scanf("%d%d%d",&u[i],&v[i],&w[i]);next[i]=first[u[i]];first[u[i]]=i; }for (int k=first[1]; k!=-1;)/*dis初始化*/{dis[v[k]]=w[k];k=next[k];}dis[1]=0; return ;
}
负权边问题
Dijkstra是先找到直接相连的对短边(假设为A)来松弛的,但如果有负边的情况下,松弛过后不能保证A还是最短边,故不能解决负权边问题。
Bellman-Ford
Bellman-ford 算法也是解决单源最短路径,但Dijkstra 算法比
更具普遍性,Dijkstra是先找最短边,而Bellman-Ford是通过对每条边直接遍历。它对边没有要求,可以处理负权边与负权回路。缺点是时间复杂度过高,高达O(NM)。
核心代码:
for (int i = 1; i <= n-1; i++) for (int j = 1; j <= m; j++)if (dis[v[j]]>dis[u[j]]+w[j])dis[v[j]]=dis[u[j]]+w[j];
因为也是解决单源最短路径,故还是建立dis,外循环n-1层,内循环m层,我们直接上案例:
例题
先初始化dis数组:
第一轮对所有边进行松弛操作,更新了2和5:
第二轮对所有边进行松弛操作,更新了3和4:
这里注意,如果我们调换 “3 4 3”和”2 3 2“的读入顺序,先读入“3 4 3”,这里就会只更新3而不更新4了。“dis[v[j]]>dis[u[j]]+w[j]”中,如过dis[u[j]]为inf,那么就肯定不会更新,通过这个规则我们就得到了一个规律:
第1轮在对所有的边进行松弛后,得到的是源点最多经过一条边到达其他顶点的最短距离;第2轮在对所有的边进行松弛后,得到的是源点最多经过两条边到达其他顶点的最短距离;第3轮在对所有的边进行松弛后,得到的是源点最多经过三条边到达其他顶点的最短距离......。当然这句话有一个bug(是算法程序的bug,不是算法的bug,如果用队列优化就不会出现,完全遵循该规律),就是和读入顺序相关,在第二轮松弛中,如果先读入“2 3 2”,那么就会更新了dis[3],那么在读入“3 4 3”的时候就会更新经过三个边的顶点,而读入顺序调返边不会发生。
还有一个问题,对所有边遍历几轮结束呢?这里我们选的是n-1轮,是因为在含有n个顶点的图中,任意两点之间的最短路径最多包含n-1条边,最坏的情况下,n个顶点连成一条直线,那我们只需要n-1次循环就可以完成。
我们直接给出第三轮和第4轮松弛,这里我们发现第4轮松弛可以省略,因为距离1顶点最远的顶点4也只有3条边。对于这种情况,我们可以加一个check来判断dis有没有变化,从而提早结束循环。
负权回路问题
负权回路是指图中存在至少一个环,使得经过该环的路径权重之和为负值。负权回路的存在可能导致在求解最短路径的过程中出现问题。例如,对于一般的最短路径算法(如 Dijkstra 或 Bellman-Ford 算法),它们无法处理图中存在负权回路的情况,因为在存在负权回路的图中,最短路径可能是无限小的。
Bellman-Ford可以判断负权回路,如果在进行n-1轮松弛后,仍然存在:
if (dis[v[j]]>dis[u[j]]+w[j])dis[v[j]]=dis[u[j]]+w[j];
也就是进行n-1轮松弛后,仍然可以松弛成功那就代表存在负权回路。判断代码如下:
int flag=0;for (int i = 1; i <= m; i++){if (dis[v[i]]>dis[u[i]]+w[i])flag=1;}if (flag==1){printf("This diagram has a negative weight loop!");/*负权回路是指图中存在至少一个环,使得经过该环的路径权重之和为负值*/}
再加上输入输出和初始化模块,得到完整代码。
输入:输入m+1行,第一行输入n和m代表顶点和边,接下来的m行每行包含三个变量x,y,z表示从x到y的距离为z。
5 5
2 3 2
1 2 -3
1 5 5
4 5 2
3 4 3
输出:dis数组
0 -3 -1 2 4
完整代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 0x3f3f3f3f/*无穷大*/int* memory_one(int n,int x);/*一维数组分配*/
void init(int *u,int *v,int *w,int m);/*dis和map初始化*/int main()
{int n,m;scanf("%d%d",&n,&m);int *u,*v,*w;/*从u到v,权重为w*/int* dis=memory_one(n+1,0x3f3f3f3f);/*不用0,故传n+1*/int* bak=memory_one(n+1,0x3f3f3f3f);/*不用0,故传n+1*/dis[1]=0;u=memory_one(m+1,0);/*不用0,比m个数多一*/v=memory_one(m+1,0);/*不用0,比m个数多一*/w=memory_one(m+1,0);/*不用0,比m个数多一*/// int** map=memory_two(n+1); /*不用0,故传n+1*/init(u,v,w,m);for (int i = 1,check; i <= n+2; i++){for (int i = 1; i <= n ; i++)bak[i]=dis[i];for (int j = 1; j <= m; j++){if (dis[v[j]]>dis[u[j]]+w[j]){dis[v[j]]=dis[u[j]]+w[j];}} /*检查是否更新*/check=0;for (int i = 1; i <= n ; i++)if (bak[i]!=dis[i]){check=1;break;}if (check==0)break; /*********** */}/*****判断负权回路*****/int flag=0;for (int i = 1; i <= m; i++){if (dis[v[i]]>dis[u[i]]+w[i])flag=1;}if (flag==1){printf("This diagram has a negative weight loop!");/*负权回路是指图中存在至少一个环,使得经过该环的路径权重之和为负值*/}/***************** */for (int j = 1; j <= n; j++)printf("%5d",dis[j]);printf("\n");free(u),free(v),free(w);free(dis);return 0;
}int** memory_two(int n)
{/*****二维数组动态分配内存和初始化************************ */int **map;map=(int **)malloc(sizeof(int*)*n);//二级指针分配n个一级指针for (int i = 0; i < n; i++)map[i]=(int *)malloc(sizeof(int)*n);//一级指针开辟空间for (int i = 0; i < n; i++)memset(map[i],inf,sizeof(int)*n);//二维数组初始化/******可用map[100][100]={0}代替************************ */return map;
}int* memory_one(int n,int x)
{int *a=(int *)malloc(n*sizeof(int));memset(a,x,sizeof(int)*n);return a;
}void init(int *u,int *v,int *w,int m)
{ for (int i = 1; i <= m; i++){scanf("%d%d%d",&u[i],&v[i],&w[i]); } return ;
}
在每一次松弛操作后,就会有一些顶点已经求得其最短路,但是每次还要判断是否需要松弛,我们可以用队列优化这一问题,而且可以解决上述所说的“BUG”。
Bellman-Ford 队列优化
采用队列,可以实现每次仅对上一轮最短路更新的点进行松弛,不需要遍历所有边。
队列初始化:
head=1;tail=1;que[tail]=1;tail++;
核心代码:
book[1]=1;while (head<tail){int k=first[que[head]];/*顶点*/while (k!=-1){if (dis[v[k]]>dis[u[k]]+w[k]){dis[v[k]]=dis[u[k]]+w[k];if (book[v[k]]==0){que[tail]=v[k];/*入队*/tail++;book[v[k]]=1;}}k=next[k];}book[que[head]]=0;head++;/*出队*/}
其它部分一样,不再叙述例题。
完整代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 0x3f3f3f3f/*无穷大*/int* memory_one(int n,int x);/*一维数组分配*/
void init(int *u,int *v,int *w,int *first,int *next,int *dis,int m);/*dis和map初始化*/int main()
{int n,m;scanf("%d%d",&n,&m);int *u,*v,*w;/*从u到v,权重为w*/u=memory_one(m+1,0);/*不用0,比m个数多一*/v=memory_one(m+1,0);/*不用0,比m个数多一*/w=memory_one(m+1,0);/*不用0,比m个数多一*/int* first=memory_one(n+1,-1);/*邻接表*/int* next=memory_one(n+1,0);/*邻接表*/int* dis=memory_one(n+1,0x3f3f3f3f);/*不用0,故传n+1*/int* book=memory_one(n+1,0);/*不用0,故传n+1*/int *que=memory_one(n+n,0);;/*队列*/int head,tail;init(u,v,w,first,next,dis,m);/*队列初始化*/head=1;tail=1;que[tail]=1;tail++;/********** */book[1]=1;while (head<tail){int k=first[que[head]];/*顶点*/while (k!=-1){if (dis[v[k]]>dis[u[k]]+w[k]){dis[v[k]]=dis[u[k]]+w[k];if (book[v[k]]==0){que[tail]=v[k];/*入队*/tail++;book[v[k]]=1;}}k=next[k];}book[que[head]]=0;head++;/*出队*/}for (int j = 1; j <= n; j++)printf("%5d",dis[j]);printf("\n");free(u);free(v);free(w);free(first);free(next);free(dis);free(book);return 0;
}int* memory_one(int n,int x)
{int *a=(int *)malloc(n*sizeof(int));memset(a,x,sizeof(int)*n);return a;
}void init(int *u,int *v,int *w,int *first,int *next,int *dis,int m)
{ for (int i = 1; i <= m; i++){scanf("%d%d%d",&u[i],&v[i],&w[i]);next[i]=first[u[i]];first[u[i]]=i; }dis[1]=0; return ;
}
《啊哈,算法!》勘误注意:
由于用的是邻接表,真正读入的顺序是倒序的,而书中给的例子入队顺序是正序的:
正确的应该是:1、5、2、3、5、4
最短路径算法对比分析
图片来源《啊哈!算法》
Flody算法总体复杂度高,但它求的是多源最短路径,其它三个均为单源最短路径,但你也可以再嵌套一层循环来实现(我没有尝试)。如果要求所有点对间的最短路径或者数据范围比较小,可以用Flody算法,Dijkstra算法无法解决负权边,但具有拓展性(怎么拓展呢?),堆优化的Dijkstra的时间复杂度达到O(MlogN)(学完堆回来补),当有负权边时,使用Bellman-Ford来实现。因此我们选择最短路径算法时,要根据实际需求和每一种算法的特性,选择合适的算法。
参考文献
《啊哈!算法》
图详解第六篇:多源最短路径--Floyd-Warshall算法(完结篇)-腾讯云开发者社区-腾讯云
Dijkstra算法详解 通俗易懂 - 知乎
Bellman-ford 算法 - 知乎
什么是负权回路 - AcWing