Bellman-Ford算法
dijkstra算法的主要思想是每次找到距离起点s最近的点将其加入已确定的集合,不断更新其它点到起点的距离,每次找到1个点,更新1次,因此只需要n次即可确定所有的起点到所有点的最短距离。
松弛函数
对边集合 E 中任意边(x,y),以 w(u,v) 表示顶点 u 出发到顶点 v 的边的权值,以 dis[v] 表示当前从起点 s 到顶点 v 的路径权值。
若存在边 (u,v),使得:dis[v] > dis[u]+w(u,v),则更新 d[v] 值:d[v]=d[u]+w(u,v)
所以松弛函数的作用,就是判断是否经过某个顶点,或者说经过某条边,可以缩短起点到终点的路径权值。
Bellman-Ford算法不是通过扫描顶点到起点s距离选择,而是每次都扫描所有的边,通过松弛函数,进行更新起点s到所有顶点的距离。
可以证明:若图中存在未确认的顶点,则对边集合的一次迭代松弛后,会增加至少一个已确认顶点,即图中某条最短路径长度会至少加一。
则要找到从起点出发到各顶点的最短路径权值
- 极端情况下,图中N个顶点都在一条最短路径上,且松弛边按照最坏情况下进行,即一次只增加一个已确认顶点,则需要执行的迭代次数为N-1次;
- 另一种极端情况,松弛边按照最好情况下进行,则需要执行的迭代次数为 1 次。
松弛边情况的好坏取决于遍历边的顺序,这与输入边的顺序有关;
另一方面还和图的结构有关,若是并查集那样的树结构,1次即可迭代结束。
最短路径长度和最短路径权值的区别:
对于路径 p<s,v1,v2,v>,路径权值为 d[v],路径长度为 3。
算法实现
图中,顶点个数为N,边数为M,自身到自身距离是已知的,为0,因此最多遍历N-1次,每次都要遍历M条边,因此时间复杂度O(NM)。
它也存在提前结束的情况,就是当某次循环时,如果在遍历M条边后,都不进行更新,就说明已经提前结束了;
此外,如果遍历了N-1次仍然会更新,说明存在负权回路。
根据Bellman-ford算法的思想,用其求最短路时,不需要知道图的结构,只需要M条边即可。
题目描述见dijkstra算法
#include <iostream>
#include <cstring>
using namespace std;
const int N=510,M=1e5+10,INF=0x3f3f3f3f;
int dis[N];
int n,m;
struct Edge
{
int a,b,w;
}edges[M]; //a->b的边,权值为w
void bellman_ford(int s)
{
memset(dis,0x3f,sizeof dis);
dis[s]=0;
int num=n-1;//n条边,顶点s距离已经确定,最多循环n-1遍。
bool tag; //判断是否提前结束的标志
while (num--) {
//最坏情况下:最多循环m-1次即可得到1号结点到每个结点的距离,这取决于遍历边的顺序
tag=false;
for (int i=0;i<m;i++) {
int a=edges[i].a,b=edges[i].b,w=edges[i].w;
//a->b 权值为w的边,确认是否修改dis[b]
if(dis[a]!=INF && dis[b]>dis[a]+w) {
dis[b]=dis[a]+w; tag=true;
}如果前驱a都没有访问到,后继b也不用发访问了,因为负权边w可能为负数,导致被替换,dis[b]!=INF(略小于)且不可达
} 若不存在正权边,则不用担心。
if (!tag) break;//当不再更新边时,说明已经得到了所有顶点的最短路径,可以提前退出
}
}
int main()
{
scanf("%d%d",&n,&m);
int a,b,w;
for (int i=0;i<m;i++) {
//循环m次,输入边的信息
scanf("%d%d%d",&a,&b,&w);
edges[i]={
a,b,w}; //重边也进行存储,因为没法覆盖找最小值
}
bellman_ford(1); //求1号结点到所有结点的最短距离
if (dis[n]==INF) puts("-1"); //输出1->n的最短距离
else printf("%d",dis[n]);
return 0;
}
易错理解:
每条边不止只用一次,可能存在用很多次,本来是想增加一个bool数组限制每条边只迭代一次的,后来发现不对,(a,b,w) a->b
dis[b]=min(dis[b],dis[a]+w);
dis[b]是否更新取决于它的前驱dis[a]是否更新,若dis[a]更新,才要更新dis[b],所以边(a,b)可能会重复多次使用。
这正是spfa算法的思想。
适用范围
dijkstra算法适用于边权都是非负数的图,且可以存在回路。
bellman_ford算法适用于有负权边存在,但不存在负权回路的情况。
因为若i->j的路径上存在负权回路的情况,最短路径(权值)就不存在。
最短路径的限制:
i->j的最短路径中可以存在负权边,但不可以存在回路,因为若存在负权回路,则每一次经过一次负权回路,最短路径就降低一次,若走无穷大次,则最短路径最终为负无穷。
在有向图中,负权边不一定会形成负权回路;
在无向图中,负权边就意味着负权回路,所以无向图中不能存在负权边。(有i->j,即有j->i )
因此,bellman-ford算法可以检测 有向带权图 负权回路的存在:
而拓扑排序可以检测有向图中是否存在回路
根据前面对松弛函数执行次数的分析可知,
若图中不存在负权回路,那么即使在最坏情况下,也只需要执行N-1次迭代松弛,即可获得从起点到各顶点的最短路径;
若图中存在负权回路,可以再额外执行一次松弛函数,即在N-1次循环后,再执行一次,如果仍有顶点更新距离,则说明存在负权回路。
题目描述
有边数限制的最短路
给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从1号点到n号点的最多经过k条边的最短距离,如果无法从1号点走到n号点,输出impossible。
注意:图中可能存在负权回路 。
输入格式
第一行包含三个整数n,m,k。
接下来m行,每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示从1号点到n号点的最多经过k条边的最短距离。
如果不存在满足条件的路径,则输出“impossible”。
数据范围
1≤n,k≤500,
1≤m≤10000,
任意边长的绝对值不超过10000。
输入样例:
3 3 1 3个顶点,3条边,最多经过1条边
1 2 1 //1->2
2 3 1 //2->3
1 3 3 //1->3
输出样例:
3
算法实现
注意区分路径长度和路径权值:
该题目要求在最大路径长度为k的情况下,求出1->n的最小路径权值(距离)。
若遍历松弛的边顺序为:(a,b),(b,c),(c,d),(a,c),(a,d)
第一次迭代:
对边 (a,b) 执行松弛函数,则 d[b]=d[a]+w(a,b)=1
对边 (b,c) 执行松弛函数,则 d[c]=d[b]+w(b,c)=3
对边 (c,d) 执行松弛函数,则 d[d]=d[c]+w(c,d)=8
这是最优的遍历的边的顺序,一次就可得到点a到其它所有点的最短距离,但是,一次迭代更新的路径长度却不是1,最大更新的路径长度是3:a->b->c->d
我们这次想要限制它每次迭代只更新路径长度为1的最短距离,所以措施就是本次迭代更新的结果不作用本次迭代,而是作用于下一次。
e.g. 初始时,dis[a]=0,dis[b]=dis[c]=dis[d]=INF,
当边(a,b)松弛后,dis[b]=1,但是这次更新的不作用这次,更新边(b,c)时,dis[b]仍按上次的INF来更新,而是下次更新时再用dis[b]=1。
因此,我们将dis数组分成两类,更新前和更新后。这样可以做到每走一次循环,只让路径长度+1 (路径长度和路径权值无关,路径长度和仅与经过的顶点数有关),这样走k次循环即可满足题意。
而且,这样也可以实现走N-1次循环就可遍历到起点到所有顶点的距离,但是确实必须要走N-1次才行,因为1号顶点和N号顶点之间的路径长度为N-1(无环)。
而题目只要求K次循环即可。
在不存在负权回路的情况下,
若K<N,就可以不用判断 是否需要提前break,退出while循环了,因为dis数组肯定还没更新完。
若K>=N时,当第N次循环时,就不会再更新了,超过N-1的多次循环无效,所以①要么限制n-1次循环,②要么设置标志当其不再更新dis数组时退出。
即在第18行加一句k=min(n-1,k);
即可。
在存在负权回路的情况下,
当K<N时,虽然还没有更新完dis数组,但是有可能 已经 经过负权回路了,也可能没有。
那么K>=N时,就是说求循环负权回路之后的最短路径,每循环一次就变一次,循环次数受限于K。
所以dis[n]数组与K有关,必须要循环K次。
这里K与N的关系是不一定的,理论上应该是K<N,但是也可以K>=N,且图中也可能存在负权回路。
所以,这里一定是经过K次循环,而不是k=min(n-1,k);
#include <iostream>
#include <cstring>
using namespace std;
const int N=510,M=1e4+10,INF=0x3f3f3f3f;
struct Edge
{
int a,b,w;
}edges[M];
int dis[N],d[N];
int n,m,k;
void bellman_ford(int s)
{
memset(dis,0x3f,sizeof dis);
dis[s]=0;
while(k--) {
//最多经过k条边,循环k次
memcpy(d,dis,sizeof dis);//d数组作为旧数组存储,更新结果存在dis数组中
for (int i=0;i<m;i++) {
int a=edges[i].a,b=edges[i].b,w=edges[i].w;
if(d[a]!=INF) dis[b]=min(dis[b],d[a]+w);
} //如果前驱都没有访问到,后继也不用发访问了,避免最后 if (dis[n] == INF ) 出现问题
}
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
int a,b,w;
for (int i=0;i<m;i++) {
scanf("%d%d%d",&a,&b,&w);
edges[i]={
a,b,w};
}
bellman_ford(1);
dis[n]==INF?puts("impossible"):printf("%d",dis[n]);
return 0;
}
对于没有负权回路的图,这个一次只走一步的方法和前面一次走多步的方法都可以实现求起点到所有顶点的最短路,不过前者必须要循环N-1次才行,后者是至多N-1次,可能存在提前终止的情况。
不过前者的好处是可以统计路径长度为k的情况下的最短路径。(特别是有负权回路的)