最短路总共有四种算法:
Dijkstra算法,Floyd算法,Bellman-ford算法,spfa算法
bellman-ford可以用于边权为负的图中,图里有负环也可以,如果有负环,算法会检测出负环。
时间复杂度O(VE);
dijkstra只能用于边权都为正的图中。
时间复杂度O(n2);
spfa是个bellman-ford的优化算法,本质是bellman-ford,所以适用性和bellman-ford一样。(用队列和邻接表优化)。
时间复杂度O(KE);
floyd可以用于有负权的图中,即使有负环,算法也可以检测出来,可以求任意点的最短路径,有向图和无向图的最小环和最大环。
时间复杂度O(n3);
任何题目中都要注意的有四点事项:图是有向图还是无向图、是否有负权边,是否有重边,顶点到自身的可达性。
例题:洛谷P3371
1.Dijkstra(单源点最短路) :
这个算法只能计算单源最短路,而且不能计算负权值,这个算法是贪心的思想,
dis数组用来储存起始点到其他点的最短路,但开始时却是存的起始点到其他点的初始路程。通过n-1遍的遍历找最短。
每次在剩余节点中找dist数组中的值最小的,加入到s数组中,并且把剩余节点的dist数组更新。
#include<bits/stdc++.h> using namespace std; #define maxn 10005 #define maxm 500005 #define INF 2147483647 inline int read(){ int x=0,k=1; char c=getchar(); while(c<'0'||c>'9'){if(c=='-')k=-1;c=getchar();} while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar(); return x*k; } struct Edge { int u,v,w,next; }e[maxm]; int head[maxm],cnt,n,m,s,vis[maxn],dis[maxn]; #define P pair<long long,int> priority_queue<P,vector<P>,greater<P> >q; //把最小的元素放在队首的优先队列 inline void add(int u,int v,int w) { e[++cnt].u=u; //这句话对于此题不需要,但在缩点之类的问题还是有用的 e[cnt].v=v; e[cnt].w=w; e[cnt].next=head[u]; //存储该店的下一条边 head[u]=cnt; //更新目前该点的最后一条边(就是这一条边) } //链式前向星加边 void dijkstra() { for(int i=1;i<=n;i++) { dis[i]=INF; } dis[s]=0; //赋初值 q.push(make_pair(0,s)); while(!q.empty()) //堆为空即为所有点都更新 { int x=q.top().second; q.pop(); //记录堆顶并将其弹出 if(!vis[x]) //没有遍历过才需要遍历 { vis[x]=1; for(int i=head[x];i;i=e[i].next) //搜索堆顶所有连边 { int v=e[i].v; dis[v]=min(dis[v],dis[x]+e[i].w); //松弛操作 q.push(make_pair(dis[v],v)); } } } } int main() { n=read(),m=read(),s=read(); for(int i=1;i<=m;i++) { int x,y,z; x=read(),y=read(),z=read(); add(x,y,z); } dijkstra(); for(int i=1;i<=n;i++) { printf("%d ",dis[i]); } return 0; }
2.Floyd:
不少人可能刚接触floyd的时候非常容易把它写错,错误的写法就是三层循环的从外到内的变量分别为i,j,k
正确的写法应该是k,i,j。写错的原因是不理解floyd算法造成的,那么为什么从顺序是k,i,j呢?
其实floyd的算法本质是个动态规划!
dp[k][i][j]代表i到j的中间节点(不包括i和j)都在区间[1,k]时,i到j的最短路。算法的最外层循环是个从小到大枚举k的过程,
当最外层刚刚进入第k次循环的时候,我们已经得到了所有点对的dp[k-1][][]的值,也就是所有点对(i,j)的i到j的中间节点都在[1,k-1]区间的i到j的最短路。
那么对任意的点对(i,j),如果他俩的最短路经过k点,则dp[k][i][j]=dp[k-1][i][k]+dp[k-1][k][j];
如果不经过k点,则dp[k][i][j]=dp[k-1][i][j]。所以当我们求dp[k][][]的时候,要保证所有的dp[i-1][][]都求出来了,
因此,最外层循环是k。
每一层都是有上一层决定,不会受这一层影响,所以可以利用滚动数组优化内存空间,将k去除掉
任意节点i到j的最短路径不外乎两种可能:1、直接从i到j;2、从i经过若干个节点k到j。
Dis(i,j)表示节点i到j最短路径的距离,对于每一个节点k,检查Dis(i,k)+Dis(k,j)小于Dis(i,j),如果成立,
Dis(i,j) = Dis(i,k)+Dis(k,j);遍历每个k,每次更新的是除第k行和第k列的数。
floyd能做很多事情,下面简单说两个:
求有向图的最小环或者最大环(顶点数>=2),求无向图的最小环(顶点数>=3)。
先说求有向图最小环(最大环略)。有两种方法可以求,一种是设定g[i][i]为无穷大
这样最后找所有的g[i][i]里的最小值就行;另一种是正常做floyd,然后对每个点对(i,j)
求g[i][j]+dp[n][j][i]的最小值,这样的原理是最小环如果存在的话,那么可以枚举一个这个环里的边i->j,那么包含这条边的最小的环一定是i->j和dp[n][j][i]构成的最短路。
无向图的最小环做法和有向图不一样,是因为无向边可能会被用两次导致出错,
举例说就是:枚举了一条边i->j,然后其与dp[n][j][i]的和作为一个结果,但是如果j到i的最短路就是边j->i的话,
那么我们找的环其实只是一条边而已,这样的结果显然是错误的。正确的做法是先判断最小环再更新最短路.
因为会出现重边的现象,所以一个环一定至少有三个点,所以每次先判断最小环,
因为k必须是未用过的,此时的dist[i][j]是遍历了k-1次的最短路,用完判断了再去更新k对应的最短路。
每次比较dist[i][j]+ g[i][k]+g[k][j]的最小值。k—>i—>j—>k;这样一直保证环是三个点。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; #define inf 2147483647 #define maxn 10005 inline int read(){ //快读 int x=0,k=1; char c=getchar(); while(c<'0'||c>'9'){if(c=='-')k=-1;c=getchar();} while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar(); return x*k; } int a[maxn][maxn],n,m,s; inline void floyd() { for(int k=1;k<=n;k++) //这里要先枚举k(中转点) { for(int i=1;i<=n;i++) { if(i==k||a[i][k]==inf) { continue; } for(int j=1;j<=n;j++) { a[i][j]=min(a[i][j],a[i][k]+a[k][j]); //松弛操作,即更新每两个点之间的距离 //松弛操作有三角形的三边关系推出 } } } } int main(){ n=read(),m=read(),s=read(); for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { a[i][j]=inf; } } //初始化,相当于memset(a,inf,sizeof(a)) for(int i=1,u,v,w;i<=m;i++) { u=read(),v=read(),w=read(); a[u][v]=min(a[u][v],w); //这种方法可以对付重边 } floyd(); a[s][s]=0; for(int i=1;i<=n;i++) { printf("%d ",a[s][i]); } return 0; }
3.Bellman-Ford
适用范围:
1、单源最短路径(从源点到其他所有点v);
2、有向图&无向图;
3、边权可正可负
4、差分约束系统
图G(v,e),源点s,数组Distant[i]记录了从源点s到顶点i的路径长度,循环执行至多n-1次,n为顶点数。
对每一条边e(u,v),如果Distant[u]+w[u,v]小于Distant[v],则Distant[v] = Distant[u]+w(u,v);
每一次循环都会至少更新一个点,所以才会出现至多循环n-1次,一次更新是指用所有节点进行一次松弛操作
因为:
1、将所有节点分为两类:已知最短距离的节点和剩余节点。
2、这两类节点满足这样的性质:已知最短距离的节点的最短距离值都比剩余节点的最短路值小。
3、易知到剩余节点的路径一定会经过已知节点。
4、而从已知节点连到剩余节点的所以边中的最小的那个边,这条边所更新后的剩余节点就一定是确定的最短距离,从而多找到了一个能确定最短距离的节点(不用知道它是哪个节点)。
实现过程的三步:
1、初始化所有的点,每一个点保存一个值,表示源点到这个点的距离其他点的值设为无穷大。
2、进行循环,从1到n-1,进行松弛计算。
3、遍历所有边,如果的d[v]大于d[u]+w(u,v)存在,则有从源点可达的权为负的回路。
#include<iostream> using namespace std; const int maxx=10001; int n,m,s,dis[maxx],w[500001],num[maxx],f[maxx][maxx/10][2],a=0; int main(){ ios::sync_with_stdio(false); cin>>n>>m>>s; for(int i=1;i<=n;i++) dis[i]=400; for(int i=1;i<=m;i++) for(int j=1;j<=m;j++) w[i]=400; for(int i=1;i<=m;i++){ int x,y,v; cin>>x>>y>>v; f[x][++num[x]][0]=y; f[x][num[x]][1]=i; w[i]=v; } dis[s]=0; while(a<=50){ //循环大法好 for(int i=1;i<=n;i++) for(int j=1;j<=num[i];j++) dis[f[i][j][0]]=min(dis[f[i][j][0]],dis[i]+w[f[i][j][1]]); a++; } for(int i=1;i<=n;i++) if(dis[i]==400) cout<<2147483647<<' '; else cout<<dis[i]<<' '; return 0; }
4.SPFA
spfa其实是Bellman-Ford的优化
它和Bellman-Ford一样可以处理负边权
定理: 只要最短路径存在,上述SPFA算法必定能求出最小值。
运用从不证明思想,所以把它背下来就好
其次SPFA无法处理带负环的图
下面为例题代码:
//SPFA #include<cstdio> #include<cstring> using namespace std; const int M=5e5+200; const int N=1e5+100; #define ll long long #define init memset(head,0,sizeof(head)) #define inf 2147483647 int head[N],tot(0),n,m,s; struct edge { ll to,next,dis; }e[M]; ll dis[N]; inline void add(int u,int v,int dis) { e[++tot]=(edge){v,head[u],dis}; head[u]=tot; } inline void input() { int f,g,w; scanf("%d%d%d",&n,&m,&s); for(int i=1;i<=m;i++) { scanf("%d%d%d",&f,&g,&w); add(f,g,w); } } inline void spfa() { int queue[M<<1],t(1),h(0); for(int i=1;i<=n;i++) dis[i]=inf; bool vis[N]; memset(vis,true,sizeof(vis)); vis[s]=false; dis[s]=0; queue[0]=s; while(t>h) { int node=queue[h++]; for(int i=head[node],x=e[i].to,w=e[i].dis;i;i=e[i].next,x=e[i].to,w=e[i].dis) { if(dis[node]+w<dis[x]) { dis[x]=dis[node]+w; if(vis[x]) queue[t++]=x,vis[x]=false; } } vis[node]=true; } } inline void output() { for(int i=1;i<=n;i++) { printf("%lld ",dis[i]); } } int main() { init; input(); spfa(); output(); return 0; }