转自gzr的博客:https://www.cnblogs.com/TFLS-gzr/p/10381849.html,https://www.cnblogs.com/TFLS-gzr/p/10387463.html
感觉这是我目前浏览找到的最短路问题讲解中比较通俗易懂的,转载只为方便自己在遗忘的时候可以及时查阅,不必再去找,感谢作者gzr!
1 floyed算法
1)明确思想及功效:在图中求最短路还是要分开说的,分别是单源最短路和多源最短路,而floyed算法是求多源最短路的,什么是多源最短路呢?简单来说就是用完算法之后能直接写出任意两点间的最短路径长度。floyed算法在本质上是动态规划思想,不断更新最短路径的值,主要思想就是不断判断两个点是否可以通过一个点中继以刷新当前两个点最短路径的估计值,直到每两个点都判断完成。很容易理解,就不详细图解了。
2)代码如下:
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(a[i][j]>a[i][k]+a[k][j])
{
a[i][j]=a[i][k]+a[k][j];
}
}
3)时间复杂度:O(n^3)
2 dijkstra算法
1)思想及其功效:dijkstra算法属于单源最短路算法,简单说就是用过之后,只能直接写出一点到其他任意一点的最短路径长度。这个算法类似于prim算法,找出刷新点所能到达的点,然后再继续刷新,写起来太难理解,自己都不会说了,那就具体以一幅图来讲解。
如果这个图是这样的:
当然,图要用矩阵来存储。因为是单源最短路,只能求一个点到其他各点的最短距离,所以我们可以假设是求1号点到其他各点的最短距离。此时我们需要一个数组dis[ ]来存储1到5个顶点分别的最短路径长度。那么,在最开始时,数组里初始化为估计值,凡是1能到达的点,相应路径的长度会直接存在数组中,而不能直接到达的点则被当做正无穷,可以赋值成999999或0x3f3f3f3f等尽可能大的数,具体是什么样呢?如图所示:
思考几个问题:
- 为什么dis[1]的值是0?
answer:因为dis[ ]数组所存的是1到各点的最短路径长度,1到1不用走,直接就到了呀,所以是0;
- OO是什么?
answer:因为画图软件不会画无限符号,就用这个代替正无穷吧,写代码时可以赋成尽可能大的值。
- 为什么有的赋值了而有的没有?
answer:如上面所说,比如2和5,从图上看它们都和1有直接连边,可以赋进dis里,但是3和4与1无直接连边,只能通过其他点间接到达,所以先赋估计值正无穷。
接着就把刚才刷新的2和5作为中继看一看是否可以刷新其他点的值。
先来看一看通过2所能刷新的点,分别是3和5,dis[3]原本是正无穷,现在通过2只需要权值5就能到达,比正无穷小,就更新它,像这种操作,专业术语叫做“松弛”,同理,继续松弛操作,dis[5]的值本来为8,因为通过2来中继,3+4<8,所以更新它,继续这样操作不断循环下去,直到所有未知点全部被访问并进行松弛操作。
//蓝边表示中继,红边代表这条边不能用,绿边表示可以通过这条边来刷新。
刚才刷新了3和4,现在通过3和4来中继。
通过3无法继续刷新,继续看4。
依旧没点刷新,此时,所有点都访问过了,并进行了松弛操作所有的dis[ ]全部从估计值一步步变成了确定值。
算法演示结束。
2)主要问题
- 怎么确定所有顶点被访问过?
answer:可以定义一个vis[ ]数组,最开始全是0,每选出一个点,就把vis的值更新为1。
- 先找哪个点为中继?
answer:先选一条没有访问过且dis值最小的。
- 有代码吗?
answer:有,如下:
void dijkstra()
{
vis[1]==0;dis[1]=0;
for(int i=1;i<=n-1;i++)
{
minn=inf;
for(int j=1;j<=n;j++)
{
if(vis[j]==0&&dis[j]<minn)
{
k=j;
minn=dis[j];
}
}
vis[k]=1;
for(int j=1;j<=n;j++)
{
if(dis[j]>dis[k]+map[k][j]&&map[k][j]<inf)
dis[j]=dis[k]+map[k][j];
}
}
}
3 SPFA算法
1 原理介绍
SPFA算法和dijkstra算法特别像,总感觉自己讲的不行,同学说我的博客很辣鸡,推荐一个视频讲解,想看点这里,算法思路如下:
1)和dijkstra一样初始化,定义一个dis[ ]数组,除了源点赋成0之外其它点都赋成正无穷,然后定义一个队列q。
2)把队列q的队首元素取出,标志为不在队中,将其作为中继点对这个队首元素的所有出边进行松弛操作(不知道松弛操作请看这里),修改完dis值后,判断每一个修改过dis值的元素是否在队列q中,如果不在,就放入队尾;然后判断这个数入队的次数,如果大于n(n为点的个数),那就说明出现了负权回路,算法结束,否则继续。
3)不断循环,直到队列为空。
2 实现过程中的一些问题
- question:怎么标志出队?
answer:可以定义一个vis[ ]数组,最开始全部为0,表示都不在队列中,每入队一个元素x,就把vis[x]赋成1,每出队一个元素就赋值成0。
- question:怎么判断一个数入队次数?
answer:可以定义一个num[ ]数组,每入队一个元素x,就num[x]++;这个可以不写,因为题目一般不会出现负权回路。
- question:怎么判断队列为空?
answer:最流行的写法是while(!q.empty()),但是不太好理解,我一般会写成while(s.size()),和前一句意思相同。
3 图解演示
4 代码:
void SPFA()
{
for(int i=1;i<=n;i++)
dis[i]=inf;
queue<int>q;
q.push(1);vis[1]=1;dis[1]=0;
while(q.size())
{
x=q.front();q.pop();vis[x]=0;
for(int i=head[x];i;i=a[i].next)
{
int s=a[i].to;
if(dis[s]>dis[x]+a[i].cost)
{
dis[s]=dis[x]+a[i].cost;
if(vis[s]==0)
{
vis[s]=1;
q.push(s);
}
}
}
}
}