文章目录
1、Floyd算法
多源最短路问题:求图中任意两点之间的距离
1.1 核心代码:
for(k=1;k<=n;k++)//通过前k号节点中转
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
e[i][j]=min(e[i][j],e[i][k]+e[k][j]);
1.2 基本思想
最开始只允许经过1号顶点进行中转,接下来只允许1号和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短路径。用一句话概括就是:从i号顶点到j号顶点只经过前k号 节点的最短路径。
1.3 完整代码
// 算法:Floyd-Warshall
// 时间:2021.11.3 22点41分
// 目的:求解多源路最短路径问题(任意两个点之间的最短路径)
#include<stdio.h>
int main()
{
int e[10][10], k, i, j, n, m, t1, t2, t3;
int inf = 99999999;//用inf(infinity的缩写)存储一个我们认为的正无穷值
//读入n和m,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;
}
//Floyd-Warshall算法核心语句
for (k = 1; k <= n; k++)
for (i = 1; i <= n; i++)
for (j = 1; j <= n; j++)
if (e[i][j] > e[i][k] + e[k][j])
e[i][j] = e[i][k] + e[k][j];
//输出最终的结果
for (i = 1; i <= n; i++)
{
for (j = 1; j <= n; j++)
{
printf("%10d", e[i][j]);
}
printf("\n");
}
return 0;
}
执行结果:
2、Dijkstra算法(朴素版)
单源最短路径:指定一个点(源点)到其余各个顶点的最短路径
2.1 算法思想
基本思想:每次找到离源点(这里以1号顶点为源点
)最近的一个顶点,然后以该顶点为中心进行扩展,最终得到源点到其余所有点的最短路径。基本步骤如下:
- 将所有顶点分为两部分:已知最短路径的顶点集合P和未知最短路径的顶点集合Q。最开始,已知最短路径的顶点集合P中只有源点一个顶点。我们这里用一个book数组来记录哪些点在集合P中,如果book[i]为0则表示这个顶点在集合Q中。
- 设置源点s到自己的最短路径为0即dis[s]=0。若存在有源点能直接到达的顶点i,则把dis[i]=e[s][i].同时把所有其他(源点不能直接到达的)顶点的最短路径设为无穷大(这里我们一般把无穷大具体定义为99999999)。
- 在集合Q的所有顶点中选择一个离源点s最近的顶点u(即dis[u]最小)加入到集合P。并考察所有以u为起点的边,对每一条边进行松弛操作。(如果有一条从u到v的边,dis[v]=min(dis[v],dis[u]+e[u][v])。
- 重复第三步,如果集合P为空,算法结束。最终dis数组中的值就是源点到所有顶点的最短路径。
2.2 完整代码
// 时间:2021.11.3 23点11分
// 算法:Dijkstra算法
// 目的:求解单源最短路径(指定一个点(源点)到其余各个顶点的最短路径)
#include<stdio.h>
int main()
{
int e[10][10], dis[10], book[10], i, j, n, m, t1, t2, t3, u, v, min;
int inf = 99999999;//用inf(infinity的缩写)存储一个我们认为的正无穷值
//读入n和m,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数组
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++)//一共n个顶点,1号本身已经在p集合里面了,所以
{ //只需要循环n-1次即可
//找到离1号顶点最近的顶点
min = inf;
for (j = 1; j <= n; j++)
{
if (book[j] == 0 && dis[j] < min)
{
min = dis[j];
u = j;
}
}
book[u] = 1;
for (v = 1; v <= n; v++)
{
if (e[u][v] < inf)
{
if (dis[v] > dis[u] + e[u][v])
dis[v] = dis[u] + e[u][v];
}
}
}
//输出最后的结果
for (i = 1; i <= n; i++)
printf("%d ", dis[i]);
getchar(); getchar();
return 0;
}
执行结果:
3、Bellman-Ford算法-解决负权边
3.1 核心代码
for(k=1;k<=n-1;k++)
for(i=1;i<=m;i++)
//下面这行代码的意思是:看看能否通过u[i]->v[i](权值为w[i])的这条边,使得一号顶点到v[i]号顶点的距离变短。
dis[v[i]]=min(dis[v[i]],dis[u[i]]+w[i]);
3.2 基本思想
因为最短路径上最多有n-1条边,因此Bellman-Ford算法最多有n-1个阶段。在每一个阶段,我们对每一条边都要进行松弛操作。其实每实施一次松弛操作,就会有一些顶点已经求得最短路,即这些顶点的最短路的“估计值”变为“确定值”。此后这些顶点的最短路的值就会一直保持不变,不再受后续操作的影响(但是,每次还是会判断是否需要松弛,这里浪费了时间,后续会使用队列优化)。在前k个阶段结束后,就已经找出了从源点发出“最多经过k条边”到达各个顶点的最短路。直到进行完n-1个阶段后,便得到了最多经过n-1条边的最短路。
3.3 完整代码
// 时间:2021.11.14 22点08分
// 算法:Bellman-Ford
// 目的:解决负权边得问题
#include<stdio.h>
int main()
{
int dis[10], bak[10], i, k, n, m, u[10], v[10], w[10], check, flag;
int inf = 99999999;//用inf存储一个我们认为得正无穷值
//读入n和m,n表示顶点个数,m表示边的个数
scanf("%d %d", &n, &m);
//读入边
for (i = 1; i <= m; i++)
scanf("%d %d %d", &u[i], &v[i], &w[i]);
//初始化dis数组,这里是1号顶点到其余各个顶点得初始路程
for (i = 1; i <= n; i++)
dis[i] = inf;
dis[1] = 0;
//Bellman-Ford算法核心语句
for (k = 1; k <= n - 1; k++)
{
//将dis数组备份到bak数组中
for (i = 1; i <= n; i++) bak[i] = dis[i];
//进行一轮松弛
for (i = 1; i <= m; i++)
if (dis[v[i]] > dis[u[i]] + w[i])
dis[v[i]] = dis[u[i]] + w[i];
//松弛完毕后检测dis是否有更新
check = 0;
for (i = 1; i <= n; i++) if (bak[i] != dis[i]) { check = 1; break; }
if (check == 0) break;//如果dis数组没有更新,提前退出循环结束算法
}
//检测负权回路
flag = 0;
for (i = 1; i <= m; i++)
if (dis[v[i]] > dis[u[i]] + w[i]) flag = 1;
if (flag == 1) printf("此图含有负权回路");
else
{
//输出最终的结果
for (i = 1; i <= n; i++)
printf("%d ", dis[i]);
}
getchar(); getchar();
return 0;
}
3.4 扩展功能:判断图是否含有负权回路
基本原理:
在进行n-1轮松弛之后,如果依然可以继续松弛,那么此图必然存在负权回路。因为如果存在负权回路,那么每多走一次负权回路就可以得到更短的路径。如果一个图没有负权回路,那么最短路径所包含的边最多为n-1条,即进行n-1轮松弛之后最短路径不会再发生变化。如果在n-1轮松弛之后最短路径依然会发生变化,则该图必然存在负权回路。
核心代码:
//Bellman-Ford算法核心语句
for(k=1;k<=n-1;k++)
for(i=1;i<=m;i++)
dis[v[i]]=min(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("此图含有负权回路");
4、Bellman-Ford算法的队列优化
4.1 优化思路
上面介绍Bellman-Ford算法的时候我们已经了解到松弛n-1轮是最大值,往往不需要n-1轮即可成功。所以每次仅对最短路径发生变化了的点的相邻边执行松弛操作。但是如何知道当前哪些点的最短路程发生了变化呢??我们可以用一个队列来维护这些点。每次选取队首顶点u,对顶点u的所有出边进行松弛操作。例如有一条u->v的边,如果通过u->v这条边使得源点到顶点v的最短路径变短(dis[u]+e[u][v]<dis[v]),且顶点v不在当前的队列中,就将顶点v放入队尾。需要注意的是,这里我们需要一个数组来判重(判断哪些点已经在队列中)。在对顶点u的所有边松弛完毕后,就将顶点v出队。接下来不断从队列中取出新的队首顶点再进行如上操作,直至队列空为止。
4.2 完整代码
// 时间:2021.11.14 22点44分
// 算法:Bellman-Ford 算法队列优化
#include<stdio.h>
int main()
{
int n, m, i, j, k;
//u,v和w的数组大小要根据实际情况来设置,要比m的最大值要大一
int u[8], v[8], w[8];
//first要比n的最大值大1,next要比m的最大值要大一
int first[6], next[8];
int dis[6] = { 0 }, book[6] = { 0 };//book数组用来记录那些顶点已经在队列中了
int que[101] = { 0 }, head = 1, tail = 1;//定义一个队列,并初始化队列
int inf = 99999999;//用inf来存储一个我们认为的正无穷值
//读入n,m
scanf("%d %d", &n, &m);
//初始化dis数组,这里是1号顶点到其余各个顶点的初始化路程
for (i = 1; i <= n; i++)
dis[i] = inf;
dis[1] = 0;
//初始化book数组,初始化为0,刚开始都不在队列中
for (i = 1; i <= n; i++) book[i] = 0;
//初始化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;
}
//1号顶点入队
que[tail] = 1; tail++;
book[1] = 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];//更新顶点1到顶点v[k]的路程
//这里的book数组用来判断顶点v[k]是否在队列中
//如果不使用一个数组来标记的话,判断一个顶点是否在队列中每次都需要
//从队列的head到tail扫一遍,很浪费时间
if (book[v[k]] == 0)//0表示不在队列中,将顶点v[k]加入队列中
{
//下面两句是入队操作
que[tail] = v[k];
tail++;
book[v[k]] = 1;//同时标记顶点v[k]已经入队
}
}
k = next[k];
}
//出队
book[que[head]] = 0;
head++;
}
//输出1号顶点到其余各点的最短路径
for(i = 1; i <= n; i++)
printf("%d ", dis[i]);
getchar(); getchar();
}
运行结果: