最短路问题难点在于建图,如何把原问题抽象成最短路问题,如何定义点和边
m和n^2差不多时就是稠密图
1.朴素版Dijkstra算法
O(n*n)
思路
s
i
s_i
si:当前已确定最短路距离的点
1.初始化 dis[1]=0 , dis[i]=INF
只有起点是确定的
2. for i : 1~n //每次循环都可以确定一个点到源点的最短距离
t <- 不在s[i]中的,距离最近的点
s <- t
用t来更新其他所有点的距离(dis[x]>dis[t]+w)
例题1
给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。
由数据范围,500个点1e5条边知这是一个稠密图,用邻接矩阵来存储
输入
3 3
1 2 2
2 3 1
1 3 4
输出
3
代码
#include <iostream>
#include <cstring>
#include <queue>
const int maxn=1e3+5;
const int INF=0x3f3f3f3f;
using namespace std;
int n,m;
int mp[maxn][maxn],dis[maxn];
bool st[maxn];//某个点是否已经更新过其他点,而不是它的最短距离是否已经确定
int dijkstra()
{
memset(dis,0x3f,sizeof(dis));
dis[1]=0;
for(int i=1;i<=n;i++)//还剩下n-1个点未处理,所以循环n-1次以后就可以得到所有点的最短路了
{
int t=-1;//Dij的使用情况是均为正权边
for(int j=1;j<=n;j++)//编号从1开始遍历
if(!st[j]&&(t==-1||dis[t]>dis[j]))//刚开始,或当前不是最短路
t=j;//寻找还未确定最短路的点中 路径最短的点
if(t==n)//如果提前找到了可以直接break
break;
st[t]=true;//找到了剩余未确定的最短路中路径最短的点
for(int j=1;j<=n;j++)//用这个点去更新其他点
dis[j]=min(dis[j],dis[t]+mp[t][j]);
}
if(dis[n]==INF)//注意如果不存在最短路情况的特判
return -1;
return dis[n];
}
int main()
{
cin>>n>>m;
int a,b,c;
memset(mp,0x3f,sizeof(mp));
for(int i=0;i<m;i++)
{
cin>>a>>b>>c;
mp[a][b]=min(mp[a][b],c);//处理重边的情况
}
cout<<dijkstra()<<endl;
return 0;
}
扩展
如果是问编号a到b的最短距离,初始化的时候改为 dis[a] = 0 ,并且return dis[b]
2.堆优化版Dijkstra算法
O(mlogn)
dij用binary-heap是 O( (N+M)logN )的,即使是fibo堆也是 O( M + NlogN )的,稠密图的 M 能到 N2
遍历所有点的所有边,就是遍历这个图的所有边
堆的实现方式
1.手写堆 优点:支持直接修改某个数 其中存储的最多就是n个数
2.优先队列 缺点:不支持修改任意一个元素,只能通过不断插入新的数来实现,所以堆里总共的个数可能会有m个,时间复杂度就会变成O(mlogm)
但是,一般来说 m ≤ n n , l o g m ≤ l o g n n = 2 l o g n m≤n^n,logm≤logn^n=2logn m≤nn,logm≤lognn=2logn,所以log m和log n是一个级别的,所以一般可以不用手写堆。
但是使用优先队列,有可能会存在冗余数据,因为修改是通过不断加数进去的。
例题 (上题变形)
采用邻接表来存图
代码
#include <iostream>
#include <cstring>
#include <queue>
#include <vector>
const int maxn=1e6+5;
const int INF=0x3f3f3f3f;
using namespace std;
int n,m,idx;
int h[maxn],w[maxn],e[maxn],nex[maxn],dis[maxn];
bool st[maxn];//某个点是否已经更新过其他点,而不是它的最短距离是否已经确定
typedef pair<int,int> pp;
void add(int a,int b,int c)
{
e[idx]=b;
w[idx]=c;
nex[idx]=h[a];
h[a]=idx++;
}
int dijkstra()
{
memset(dis,0x3f,sizeof(dis));
dis[1]=0;
priority_queue<pp,vector<pp>,greater<pp> > heap;//定义一个小根堆
//注意默认是按first来进行排序的,所以把dis放在前面
heap.push({0,1});
while(heap.size())
{
auto t=heap.top();
heap.pop();
int vi,vdis;
vi=t.second;
vdis=t.first;
if(st[vi])//已经访问过了
continue;
st[vi]=true;
for(int i=h[vi];i!=-1;i=nex[i])
{
int j=e[i];
if(dis[j]>dis[vi]+w[i])
{
dis[j]=dis[vi]+w[i];
heap.push({dis[j],j});
}
}
}
if(dis[n]==INF)
return -1;
return dis[n];
}
int main()
{
cin>>n>>m;
int a,b,c;
memset(h,-1,sizeof(h));
for(int i=0;i<m;i++)
{
cin>>a>>b>>c;
add(a,b,c);
}
cout<<dijkstra()<<endl;
return 0;
}
3.Bellman-Ford算法
O(nm)
基本思路
注意:
备份是因为在遍历边进行更新时,要用之前的dis
for 1~n 迭代n次
备份一下之前的dis//保证只用上一次迭代的结果
for 所有边 a,b,w //这里不一定要用邻接表存,只要能遍历到所有边即可 可以用struct
dis[b]=min(dis[b],dis[a]+w) //1 -> b 和 1 -> a -> b 哪条路径最短
//松弛操作
该算法完成之后,就一定有dis[b]≤dis[a]+w
(三角不等式)
如果有负权回路
,最短路就不一定存在了,当这个环在1->n的最短路径上时,就不存在,如果不在,则可以存在最短路。
判有负环,即出现一个dis[i]=-∞
,则说明存在负环
B-F算法可以求出是否存在负权回路
for n次
迭代k次之后,dis表示的是,从1号点经过不超过k条边,到达所有点的最短距离
当迭代第n次时还有更新,说明 1->...->x 有n条边构成的最短路,而n条边意味着有n+1个点,
但实际上由只有n个点,说明一定有重复的编号,就一定有环,另外这里更新了,所以就一定是负环。
但是一般找负环不用S-F算法来做,常用SPFA来做
例题
一般B-F能做的SPFA都能做,但是有边数限制的题目只能用B-F来做
输入
3 3 1
1 2 1
2 3 1
1 3 3
输出
3
本题只能用B-F算法,因为 1.存在负边权(不能用dijstra) 2.存在负权回路(不能用SPFA)
本题如果限制了k步,则不会出现无限次转负环的情况,所以会存在最短路。
#include <iostream>
#include <cstring>
#include <algorithm>
const int maxn=1e4+5;
const int INF=0x3f3f3f3f;
using namespace std;
struct node{
int a,b,c;
}edges[maxn];
int n,m,k;
int dis[510],pre[510];
void bellman_ford()
{
memset(dis,0x3f,sizeof(dis));
dis[1]=0;//不要忘记初始化
for(int i=0;i<k;i++)
{
memcpy(pre,dis,sizeof(dis));//效率非常高
for(int j=0;j<m;j++)//遍历每条边
{
node e;
e=edges[j];
dis[e.b]=min(dis[e.b],pre[e.a]+e.c);//注意这里的pre
}
}
}
int main()
{
cin>>n>>m>>k;
for(int i=0;i<m;i++)
{
int a,b,c;
cin>>a>>b>>c;
edges[i]={a,b,c};//注意这种使用方式
}
bellman_ford();
if(dis[n]>INF/2)
cout<<"impossible"<<endl;
else
cout<<dis[n]<<endl;
return 0;
}
4.SPFA算法
O(m) 最坏:O(nm)
要求这个图中不含负环
一般最短路问题中都不会有负环出现
B-F算法是通过遍历所有边来更新dis,但是每一次迭代并不一定都会更新更小的dis,所以这里可优化。
一个点如果没有被更新,那么由它所更新的那些点都不会有变化,所以可略过
SPFA优化:对于dis[b]=min(dis[b],dis[a]+w)
来说,dis[b]
变小,一定是因为dis[a]变小
,可以通过宽搜来优化
使用的工具:队列、优先队列等
queue <- 1
while(queue不为空)
1. t <- q.front
q.pop() w
2.更新t的所有出边 t-->b //更新过谁,就拿谁来更新别人
queue <- b
我们也用SPFA来做正权图的问题,并且可能比堆优化的dij块,but...能用堆优化的dij就不用spfa,spfa很有可能被出题人构造的数据卡成O(nm),这个效率就很低了
网格形状的图很容易被卡spfa
是个稀疏图,边数是点数的2~3倍
dijkstra和spfa相比
spfa算法的期望的时间复杂度为O(ke) k<=2
-
DIJ的堆优化可以做到O((V+E)LOG2(V))
近似为 O(E LOG2(V))
SPFA的时间复杂度为O(KE),好坏视数据RP而定,在大多数平均情况下为O(2E),为了近似最快可以随机化输入数据 -
根据用不用堆,dij的复杂度可以是O(mlogn)和O(n2),前者不一定总是比后者快,因为有时候m=O(n^2)
- Floyd是先枚举转接点,从而达到更新最小值的目的。到后期好像O(n^3) 像闹着玩一样,但在一些n<=100的环境下还是很好用的
- Dijkstra在图论问题中主要的优点是较稳定,不会被特殊设计的测试点卡掉,还可以记录路径
- Spfa是笔者比较青睐的算法。Spfa可以在压队的过程中判断是否存在环,还可以处理负环。
例题1 模板
输入
3 3
1 2 5
2 3 -3
1 3 4
输出
2
代码
#include <iostream>
#include <queue>
#include <cstring>
#include <algorithm>
const int maxn=1e5+5;
const int INF=0x3f3f3f3f;
using namespace std;
int n,m;
int h[maxn],w[maxn],e[maxn],nex[maxn],idx;
int dis[maxn];
bool st[maxn];//st数组是用来标记这个数是否存在在队列中,不影响算法正确性,只用于提高算法效率
void add(int a,int b,int c)
{
e[idx]=b;
w[idx]=c;
nex[idx]=h[a];
h[a]=idx++;
}
int spfa()
{
memset(dis,0x3f,sizeof(dis));
dis[1]=0;
queue<int> q;
q.push(1);//存入点的编号
st[1]=true;//当前点是不是在队列当中,避免队列中存储重复的点
while(!q.empty())
{
int t=q.front();
q.pop();
st[t]=false;
for(int i=h[t];i!=-1;i=nex[i])
{
int j=e[i];
if(dis[j]>dis[t]+w[i])
{
dis[j]=dis[t]+w[i];
if(!st[j])//把更新后的这个点加入队列
{
q.push(j);
st[j]=true;
}
}
}
}
return dis[n];
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof(h));//初始化链表头
for(int i=0;i<m;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
int ans=spfa();
if(ans==INF)
cout<<"impossible"<<endl;
else
cout<<ans<<endl;
return 0;
}
这里可以使用 ans==INF 作为判断条件是因为 spfa只会更新所有能从起点走到的点(dis[j]>dis[t]+w[i]),所以如果无解,那么起点就走不到终点,那么终点的距离就是INF
例题2 spfa判负环
dis[x]:1~x 的最短距离
cnt[x]:当前最短路的边数
更新操作:
dis[x]=dis[t]+w[i]
cnt[x]=cnt[t]+1
一旦出现cnt[x]≥n
,即1->x至少经过了n条边,如果经过了n条边,则说明1->x中至少经过了n+1个点,但是最多只有n个点,由抽屉原理知就一定有两个点是一样的。路径上存在一个环,这个环又一定是更新的时候能够使得最短路变小,所以一定是负边权的环。
输入
3 3
1 2 -1
2 3 4
3 1 -4
输出
Yes
代码
#include <iostream>
#include <queue>
#include <cstring>
#include <algorithm>
const int maxn=1e5+5;
const int INF=0x3f3f3f3f;
using namespace std;
int n,m;
int h[maxn],w[maxn],e[maxn],nex[maxn],idx;
int dis[maxn],cnt[maxn];
bool st[maxn];//st数组是用来标记这个数是否存在在队列中,不影响算法正确性,只用于提高算法效率
void add(int a,int b,int c)
{
e[idx]=b;
w[idx]=c;
nex[idx]=h[a];
h[a]=idx++;
}
int spfa()
{
//这里也可以不要初始化,因为你不关心最短距离是多少
queue<int> q;
//注意这里要判断的是负环,而不是从1开始的负环了
//这个途中有可能存在负环,但是从1开始到不了
//q.push(1);//存入点的编号
//st[1]=true;//当前点是不是在队列当中,避免队列中存储重复的点
//所以应该把所有点放到队列中
for(int i=1;i<=n;i++)
{
q.push(i);
st[i]=true;
}
while(!q.empty())
{
int t=q.front();
q.pop();
st[t]=false;
for(int i=h[t];i!=-1;i=nex[i])
{
int j=e[i];
if(dis[j]>dis[t]+w[i])
{
dis[j]=dis[t]+w[i];
cnt[j]=cnt[t]+1;
if(cnt[j]>=n)
return true;
if(!st[j])//把更新后的这个点加入队列
{
q.push(j);
st[j]=true;
}
}
}
}
return false;
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof(h));//初始化链表头
for(int i=0;i<m;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
if(spfa())
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
return 0;
}
5.Floyd算法
求多源汇最短路
基本思路 (基于动态规划)
可以处理负权,但不能有负权回路
用邻接矩阵存储所有的边
d[k,i,j]=d[k-1,i,k]+d[k-1,k,j]
第一维可以去掉
dp[k,i,j]
表示只经过1~k这些中间点,到达j的最短距离
d[i][j]存储图的边权
for (k=1;k<=n;k++)
for (i=1;i<=n;i++)
for (j=1;j<=n;j++)
d[i][j]=min(d[i][j],d[i][k]+d[k][j])
算法完成之后,d[i][j]中存储的就是i到j的最短路长度
如果了解算法原理,可以看算法导论or百度。
例题
题目中保证不存在负权回路,所以自环的权值一定是正的,在求最短路时没有用,可直接删除
输入
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出
impossible
1
代码
#include <iostream>
#include <queue>
#include <cstring>
#include <algorithm>
const int maxn=205;
const int INF=0x3f3f3f3f;
using namespace std;
int n,m,q;
int d[maxn][maxn];
void floyd()
{
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
int main()
{
cin>>n>>m>>q;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(i==j)
d[i][j]=0;
else
d[i][j]=INF;
for(int i=0;i<m;i++)
{
int a,b,c;
cin>>a>>b>>c;
d[a][b]=min(d[a][b],c);
}
floyd();
while(q--)
{
int a,b;
cin>>a>>b;
if(d[a][b]>INF/2)
cout<<"impossible"<<endl;
else
cout<<d[a][b]<<endl;
}
return 0;
}
使用INF
还是INF/2
作为判断是否存在最短路,需要具体情况具体分析:
本题中可能存在如下情况:不能走到终点,但由于负数边权的存在,终点的距离可能被其他长度是正无穷的距离更新,此时会稍小于INF。INF/2
比较可以处理这种情况。
- 假设d[i][j]=7,d[i][k]=5,d[k][j]=8,在第k层时会因为d[i}[j]小于后者,而不更新d[i][j]。但万一此时第k+1个点与点k还有点j之间有负权边(点k+1与点i不直接相连),或者因为各种原因d[i][k]+d[k][k+1]+d[k][j]小于d[i][j],floyd算法会不会因为没有选择走点k而错失i->k->k+1->j这条最短路的机会?
floyd本质上是个动态规划,每次循环完第k层后,会将所有中间点的编号不超过k的最短路径全部找出来。d[i][j]
如果在第k层没有被更新,这也是合理的,因为在从i到j的最短路径中需要经过k+1这个点,那么这条最短路径要在第k+1层循环完之后才可能被找到,即d[i][j]
是会被d[i][k+1] + d[k+1][j]
来更新,那么此时就会找到d[i][k] + d[k][k+1] + d[k+1][j]
这条路径了。
- 为什么计算状态的时候 for (int k = 1; k <= n; k++)要放到最外层呢?
floyd本身是个动态规划算法,在代码实现的时候省去了一维状态。原状态是:f[i, j, k]表示从i走到j的路径上除了i, j以外不包含点k的所有路径的最短距离。那么f[i,j,k] = min(f[i,j,k-1),f[i,k,k-1]+f[k,j,k-1]
。因此在计算第k层的f[i, j]的时候必须先将第k - 1层的所有状态计算出来,所以需要把k放在最外层。