最短路
一、常见的最短路问题
(规定: n n n为点数, m m m为边数)
- 单源最短路:求一个点到其他所有点的最短距离
- 所有边权都是正数
- 朴素 D i j k s t r a Dijkstra Dijkstra算法 O ( n 2 ) O(n^2) O(n2) 时间复杂度和边数无关,适合稠密图( m m m~ n 2 n^2 n2)
- 堆优化版的 D i j k s t r a Dijkstra Dijkstra算法 O ( m l o g n ) O(mlogn) O(mlogn)
- 存在负权边
- B e l l m a n − F o r d Bellman-Ford Bellman−Ford O ( n m ) O(nm) O(nm)
- S P F A SPFA SPFA 一般 O ( m ) O(m) O(m) 最坏 O ( n m ) O(nm) O(nm)
- 所有边权都是正数
- 多源汇最短路:源点(起点) 汇点(终点) 有多个询问,起点和终点不确定
- F l o y d Floyd Floyd算法 O ( n 3 ) O(n^3) O(n3)
二、朴素 D i j k s t r a Dijkstra Dijkstra算法
//集合S:所有当前已确定了最短距离的点
step1: dist[1]=0,dist[i]=+∞
step2: for i : 1~n {//循环结束之后可以求出每个点到起点的最短路
t <- 不在s中的,距离最近的点 (n^2次)
s <- t
用t来更新其他所有点的距离 //例如用1号点到x的距离 与 1->t->x再加上权重比 较,再用短的那个更新
}
给定一个 nn 个点 mm 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 11 号点到 nn 号点的最短距离,如果无法从 11 号点走到 nn 号点,则输出 −1−1。
输入格式
第一行包含整数 nn 和 mm。
接下来 mm 行每行包含三个整数 x,y,zx,y,z,表示存在一条从点 xx 到点 yy 的有向边,边长为 zz。
输出格式
输出一个整数,表示 11 号点到 nn 号点的最短距离。
如果路径不存在,则输出 −1−1。
数据范围
1≤n≤5001≤n≤500,
1≤m≤1051≤m≤105,
图中涉及边长均不超过10000。输入样例:
3 3 1 2 2 2 3 1 1 3 4
输出样例:
3
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=510;
int n,m;
int g[N][N];//邻接矩阵存储稠密图
int dist[N];//当前点到起点的最短距离
bool st[N];//每个点的最短路是否已经确定
int dijkstra(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=0;i<n;i++){//迭代n次,即可遍历所有的点
int t=-1; //将t设置为-1,因为Dijkstra算法适用于不存在负权边的图
for(int j=1;j<=n;j++){//寻找还未确定最短路的点中路径最短的点
if(!st[j] && (t==-1 || dist[t]>dist[j]))
t=j;
}//最终循环结束后的t是还未确定点中距离最短的
st[t]=true;//把这个点标记
for(int j=1;j<=n;j++)//用t更新剩下未确定点的距离,之前的点即使遍历到了也不会改变其数值
dist[j]=min(dist[j],dist[t]+g[t][j]);
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
cin>>n>>m;
memset(g,0x3f,sizeof g);//初始化图,每个点最初为无限大
while(m--){
int a,b,c;
cin>>a>>b>>c;
g[a][b]=min(g[a][b],c);//重边的处理,如果a、b之间有多条边,则保留长度最短的那一条
}
int t=dijkstra();
printf("%d\n",t);
return 0;
}
二、堆优化版的 D i j k s t r a Dijkstra Dijkstra算法
每一次找不在S中的最短距离的点,可以用堆来寻找,时间复杂度,从 O ( n 2 ) O(n^2) O(n2) -> O ( 1 ) O(1) O(1)
此时更新距离的复杂度由 m m m次到 -> m l o g n mlogn mlogn次。
遍历所有点和所有边,即遍历所有边。
堆
- 手写堆 n个数可任意修改
- s t l stl stl 优先队列 无法修改其中元素,每次修改都是重新插入 一般复杂度变为 O ( m l o g m ) O(mlogm) O(mlogm)
给定一个 nn 个点 mm 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出 11 号点到 nn 号点的最短距离,如果无法从 11 号点走到 nn 号点,则输出 −1−1。
输入格式
第一行包含整数 nn 和 mm。
接下来 mm 行每行包含三个整数 x,y,zx,y,z,表示存在一条从点 xx 到点 yy 的有向边,边长为 zz。
输出格式
输出一个整数,表示 11 号点到 nn 号点的最短距离。
如果路径不存在,则输出 −1−1。
数据范围
1≤n,m≤1.5×1051≤n,m≤1.5×105,
图中涉及边长均不小于 00,且不超过 1000010000。
数据保证:如果最短路存在,则最短路的长度不超过 109109。输入样例:
3 3 1 2 2 2 3 1 1 3 4
输出样例:
3
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
typedef pair<int,int> PII;
const int N=1e6+10;
int n,m;
int h[N],e[N],ne[N],idx;
int w[N];//权重
int dist[N];
bool st[N];
void add(int a,int b,int c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int dijkstra(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
priority_queue<PII,vector<PII>,greater<PII>> heap;
//这一行是定义优先队列,参数vector<PII>表示堆是用vector来实现的,greater是小根堆(小的数在上)
heap.push({0,1});//把1号点放进去来更新剩下所有的点
//堆里第一个元素表示距离dist,第二个元素是其在堆中的编号
while(heap.size()){
auto t=heap.top();
heap.pop();
int ver=t.second;
if(st[ver]) continue;//这个点的最短距离已确定,跳过
st[ver]=true;
for(int i=h[ver];i!=-1;i=ne[i]){
int j=e[i];
if(dist[j]>dist[ver]+w[i]){
dist[j]=dist[ver]+w[i];
heap.push({dist[j],j});
}
}
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);//邻接表表头初始化,使h[i]均指向-1
//邻接表不需要处理重边
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
cout<<dijkstra()<<endl;
return 0;
}
三、 B e l l m a n − F o r d Bellman-Ford Bellman−Ford 算法
存边方式:定义一个结构体即可(未必需要是邻接矩阵/邻接表)
struct{
int a,b,w;
}edge[M];//M条边
for 循环n次
//循环k次后的dist[]数组表示,从1号点,经过不超过k条边,到每个点的最短距离
//若n次时dist[]又进行了更新,即存在一条最短路径,上有n条边,即n+1个点,则路径上存在环,因为更新所以是负环
上一次dist[]备份到backup[],防止串联,用上一次迭代的结果去更新下一次
for 循环所有边 a b w(存在一条a->b)的边,权重为w //第二层循环m次
dist[b]=min(dist[b],backup[a]+w);//更新:比较1->a->b与1->b(松弛操作)
//这里的a的距离,用的是上一次的,不能直接用dist[a],因为每一次迭代后的dist[a]可能不同
循环完之后,所有边一定满足:dist[b]<dist[a]+w (三角不等式)
如果有负权回路,最短路不一定必存在(可能为负无穷)。如果负环不在1->n的路径上,则有最短路径。
给定一个 nn 个点 mm 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 11 号点到 nn 号点的最多经过 kk 条边的最短距离,如果无法从 11 号点走到 nn 号点,输出
impossible
。注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数 n,m,kn,m,k。
接下来 mm 行,每行包含三个整数 x,y,zx,y,z,表示存在一条从点 xx 到点 yy 的有向边,边长为 zz。
点的编号为 1∼n1∼n。
输出格式
输出一个整数,表示从 11 号点到 nn 号点的最多经过 kk 条边的最短距离。
如果不存在满足条件的路径,则输出
impossible
。数据范围
1≤n,k≤5001≤n,k≤500,
1≤m≤100001≤m≤10000,
1≤x,y≤n1≤x,y≤n,
任意边长的绝对值不超过 1000010000。输入样例:
3 3 1 1 2 1 2 3 1 1 3 3
输出样例:
3
//由于已经限制了最多经过k条边,故只能用Bellman-Ford算法
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=510,M=10010;
int n,m,k;
int dist[N],backup[N];
struct Egde{
int a,b,w;
}edges[M];
int bellman_ford(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=0;i<k;i++){
memcpy(backup,dist,sizeof dist);//将上一次迭代的结果去更新下一次
for(int j=0;j<m;j++){
int a=edges[j].a,b=edges[j].b,w=edges[j].w;
dist[b]=min(dist[b],backup[a]+w);
}
}
if(dist[n] > 0x3f3f3f3f/2) return 0;
//为什么不写成dist[n]==0x3f3f3f3f?没有最短路时可能dist[n]=0x3f3f3f3f-w
return dist[n];
}
int main(){
cin>>n>>m>>k;
for(int i=0;i<m;i++){
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
edges[i]={a,b,w}; //结构体的赋值
}
int t=bellman_ford();
if(t==0) puts("impossible");
else printf("%d\n",t);
}
四、 S P F A SPFA SPFA算法
Bellman Ford 每次循环未必会更新距离,而spfa对这里进行了优化(使用宽搜)
因为dist[b]=min(dist[b],dist[a]+w)
所以只有dist[a]减小,dist[b]才会减小
queue <- 1 (变小的节点存入队列)
while queue不空
t <- q.front()
q.pop()
//如果一个点被更新过,再拿他来更新别人
更新t的所有出边 t->b=w
queue <- b
给定一个 nn 个点 mm 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出 11 号点到 nn 号点的最短距离,如果无法从 11 号点走到 nn 号点,则输出
impossible
。数据保证不存在负权回路。
输入格式
第一行包含整数 nn 和 mm。
接下来 mm 行每行包含三个整数 x,y,zx,y,z,表示存在一条从点 xx 到点 yy 的有向边,边长为 zz。
输出格式
输出一个整数,表示 11 号点到 nn 号点的最短距离。
如果路径不存在,则输出
impossible
。数据范围
1≤n,m≤1051≤n,m≤105,
图中涉及边长绝对值均不超过 1000010000。输入样例:
3 3 1 2 5 2 3 -3 1 3 4
输出样例:
2
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
typedef pair<int,int> PII;
const int N=+1e6+10;
int n,m;
int h[N],e[N],ne[N],idx;
int w[N];
int dist[N];
bool st[N];
void add(int a,int b,int c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int spfa(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
queue<int> q;//队列中存的是更新过的点
q.push(1);//第一个点存入队列
st[1]=true;//st[]储存这个点是否在队列中,防止重复添加
while(q.size()){
int t=q.front();
q.pop();
st[t]=false;//取出t
for(int i=h[t];i!=-1;i=ne[i]){//遍历t的所有出边
int j=e[i];
if(dist[j]>dist[t]+w[i]){//如过dist需要改变
dist[j]=dist[t]+w[i];
if(!st[j]){
q.push(j);
st[j]=true;
}
}
}
}
if(dist[n]==0x3f3f3f3f) return 0;
return dist[n];
}
int main(){
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
int t=spfa();
if(t==0) puts("impossible");
else printf("%d\n",t);
return 0;
}
spfa判断负环
dist[x]最短距离
cnt[x]边数
dist[x]=dist[t]+w[t]
cnt[x]=cnt[t]+1
如果cnt[x]>=n 则图中存在负环
给定一个 nn 个点 mm 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。
输入格式
第一行包含整数 nn 和 mm。
接下来 mm 行每行包含三个整数 x,y,zx,y,z,表示存在一条从点 xx 到点 yy 的有向边,边长为 zz。
输出格式
如果图中存在负权回路,则输出
Yes
,否则输出No
。数据范围
1≤n≤20001≤n≤2000,
1≤m≤100001≤m≤10000,
图中涉及边长绝对值均不超过 1000010000。输入样例:
3 3 1 2 -1 2 3 4 3 1 -4
输出样例:
Yes
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
typedef pair<int,int> PII;
const int N=+1e6+10;
int n,m;
int h[N],e[N],ne[N],idx;
int w[N];
int dist[N],cnt[N];
bool st[N];
void add(int a,int b,int c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
bool spfa(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
queue<int> q;//队列中存的是更新过的点
//不能只将1号点存入队列,因为负环未必是从1号点开始的
//所以把所有点放入对列
for(int i=1;i<=n;i++)
q.push(i);
while(q.size()){
int t=q.front();
q.pop();
st[t]=false;//取出t
for(int i=h[t];i!=-1;i=ne[i]){//遍历t的所有出边
int j=e[i];
if(dist[j]>dist[t]+w[i]){//如过dist需要改变
dist[j]=dist[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(){
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
if(spfa()) puts("Yes");
else puts("No");
return 0;
}
五、 F l o y d Floyd Floyd算法
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,j]+d[i,k]+d[k,j]);
循环结束之后。d[i,j]存储的是i到j的最短路
原理:基于动态规划
d[k,i,j] 从i点。只经过1-k这些中间点到达j的最短距离
d[k,i,j]=d[k-1,i,k]+d[k-1,k,j]
给定一个 nn 个点 mm 条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定 kk 个询问,每个询问包含两个整数 xx 和 yy,表示查询从点 xx 到点 yy 的最短距离,如果路径不存在,则输出
impossible
。数据保证图中不存在负权回路。
输入格式
第一行包含三个整数 n,m,kn,m,k。
接下来 mm 行,每行包含三个整数 x,y,zx,y,z,表示存在一条从点 xx 到点 yy 的有向边,边长为 zz。
接下来 kk 行,每行包含两个整数 x,yx,y,表示询问点 xx 到点 yy 的最短距离。
输出格式
共 kk 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出
impossible
。数据范围
1≤n≤2001≤n≤200,
1≤k≤n21≤k≤n2
1≤m≤200001≤m≤20000,
图中涉及边长绝对值均不超过 1000010000。输入样例:
3 3 2 1 2 1 2 3 2 1 3 1 2 1 1 3
输出样例:
impossible 1
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=210,INF = 1e9;
int n,m,Q;
int d[N][N];
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(){
scanf("%d%d%d",&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;
while(m--){
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
d[a][b]=min(d[a][b],w);//若有重边,则保留最短的那一条
}
floyd();
while(Q--){
int a,b;
scanf("%d%d",&a,&b);
if(d[a][b]>INF/2) puts("impossible");
else printf("%d\n",d[a][b]);
}
return 0;
}