图论·最短路径
最短路径
一、Dijkstra 单源最短路径
Dijkstra是在非负权图上求单源最短路径的方法,复杂度 \(O(m\) \(log_{2}\) \(n+nk\) \(log_{2}\) \(n)\)。
当一个点\(u\)的最短路被松弛过后,与该点相连的点\(v\)也有可能需要松弛,所以遍历一遍所有与\(u\)相连的点\(v\)并松弛\(dis_{v}\),若\(v\)被松弛,那么放入队列重复上述过程。
贪心地想,对于被影响的点\(v\)一定是被\(dis_{u}\)较小的\(u\)松弛,所以用小根堆维护目前最小的\(dis_{u}\)即可。
\(Dijkstra\)优就优在若点\(u\)成为堆顶,那么此时\(dis_{u}\)一定是最优的,不会再被松弛,这样也就保证了复杂度。
但是,dij不能跑带负环的也不能跑最长路。
板子代码:
void dijkstra()
{
memset(dis,0x3f,sizeof dis);
dis[s]=0;
q.push({0,s});
while (!q.empty())
{
int u=q.top().second;
q.pop();
if (vis[u]) continue;
vis[u]=true;
int _size=e[u].size();
for (int i=0;i<_size;i++)
{
int v=e[u][i].nxt,w=e[u][i].val;
if (dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
q.push({dis[v],v});
}
}
}
}
二、SPFA 单源最短路径——用队列优化的Bellman-Ford
与\(dij\)不同的是,它每次不需要特别记录点 \(u\) 是否确定了最短路径,因为跑完若干轮后肯定可以确定最短路。它在一般情况下和\(dijkstra\) 的复杂度一样好,但有些时候会退化到 \(O(nm)\)
SPFA的优势是边权可以为负,也可以判负环
板子代码↓
bool spfa()
{
memset(dis,0x3f,sizeof dis);
memset(neq,0,sizeof neq);
memset(inq,0,sizeof inq);
queue <int> q;
q.push(1),neq[1]++,dis[1]=0,inq[1]=true;
while (!q.empty())
{
int u=q.front(),q.pop();
inq[u]=false;
int _size=e[u].size();
for (int i=0;i<_size;i++)
{
int v=e[u][i].nxt,w=e[u][i].val;
if (dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
if (inq[v]) continue;
q.push(v),inq[v]=true;
neq[v]++;
if (neq[v]>n) return true;//有负环
}
}
}
return false;
}
还有一个最优比率环,不会待补
三、Floyd 多源最短路
十分暴力的最短路方法,代码简单但效率不高,在某些场景下有自己的优势。
用滚动数组将dp方程优化到二维\(f_{i,j}\),表示点对 \(i,j\) 之间的最短路径长度。时间复杂度为 \(O(n^{3})\),只适用于 \(n<300\) 的小规模图。
判断负环:若存在 \(f_{i,i}<0\) ,那么一定有负环。(呃呃 绕一圈之后边权和是负数)
传递闭包:\(f_{i,j}\) 表示点对 \(i,j\) 是否连通,然后跑Floyd就行。可以 bitset 优化,不会待补。
板子代码:
for (int k=1;k<=n;k++)//一定要在外层循环k,因为它是逐步递推的
{
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
}
}
四、差分约束系统
这个东西是一种特殊的 \(n\) 元一次不等式组,包括 \(n\) 个变量, \(m\) 个约束条件。
将约束条件 \(x_{i}-x_{j}\leqslant c_{k}\) 转化为 \(x_{i}\leqslant x_{j}+c_{k}\) ,没错这仍很像最短路中的松弛操作。对于每个约束条件,从节点 \(j\) 向节点 \(i\) 连一条权值为 \(c_{k}\) 的边,再建一个虚点 \(0\) ,跑一遍SPFA就能跑出来了。若有负环,那么差分约束无解。
【YbtOj】例题
A.单源最短路径
\(Dijkstra\)板子题,秒了
贴
#incIude <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=1e5+5;
int n,m,s;
struct node{
int nxt,val;
};
vector <node> e[N];
priority_queue < pii,vector <pii>,greater<pii> > q;
int dis[N];
void dijkstra()
{
memset(dis,0x3f,sizeof dis);
dis[s]=0;
q.push({0,s});
while (!q.empty())
{
int u=q.top().second;
q.pop();
int _size=e[u].size();
for (int i=0;i<_size;i++)
{
int v=e[u][i].nxt,w=e[u][i].val;
if (dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
q.push({dis[v],v});
}
}
}
}
signed main()
{
scanf("%lld%lld%lld",&n,&m,&s);
for (int i=1;i<=m;i++)
{
int u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
e[u].push_back({v,w});
}
dijkstra();
for (int i=1;i<=n;i++) printf("%lld ",dis[i]);
return 0;
}
B.负环判断
\(SPFA\)板子题,秒了
贴
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e3+5;
int T;
int n,m;
struct node{
int nxt,val;
};
vector <node> e[N];
int inq[N];
int dis[N];
int cnt[N];
queue <int> q;
bool spfa()
{
memset(inq,0,sizeof inq);
memset(dis,0x3f,sizeof dis);
memset(cnt,0,sizeof cnt);
while (!q.empty()) q.pop();
q.push(1);
inq[1]=1;
dis[1]=0;
cnt[1]++;
while (!q.empty())
{
int u=q.front();
q.pop();
inq[u]=0;
int _size=e[u].size();
for (int i=0;i<_size;i++)
{
int v=e[u][i].nxt,w=e[u][i].val;
if (dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
if (inq[v]==0)
{
inq[v]=1;
q.push(v);
cnt[v]++;
if (cnt[v]>=n) return true;
}
}
}
}
return false;
}
signed main()
{
scanf("%lld",&T);
while (T--)
{
scanf("%lld%lld",&n,&m);
for (int i=1;i<=n;i++) e[i].clear();
for (int i=1;i<=m;i++)
{
int u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
if (w<0) e[u].push_back({v,w});
else
{
e[u].push_back({v,w});
e[v].push_back({u,w});
}
}
if (spfa()) printf("YE5\n");
else printf("N0\n");
}
return 0;
}
C.最优贸易
显然,我们需要找到一条路径,在前面某段找到点权最小值,在后面某段找到点权最大值,这两个值的差值就是答案。再看这句话,可以转化为求源点到某点\(u\)的最小值与\(u\)到终点的最大值,所以正反建图、分别跑两边\(Dijkstra\)求出答案即可。
以上方法有误,\(dijkstra\)不能跑“最长路”,待补
(错误代码)
#include <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=1e5+5;
int n,m;
int c[N];
vector <int> e1[N],e2[N];
priority_queue < pii > q1;
priority_queue < pii,vector <pii>,greater<pii> > q2;
bool vis1[N],vis2[N];
int dis1[N],dis2[N];
int ans;
void dijkstra1()//最大的
{
memset(dis1,0xc1,sizeof dis1);
dis1[n]=max(dis1[n],c[n]);
q1.push({dis1[n],n});
while (!q1.empty())
{
int u=q1.top().second;
q1.pop();
if (vis1[u]) continue;
vis1[u]=true;
int _size=e2[u].size();
for (int i=0;i<_size;i++)
{
int v=e2[u][i];
if (max(dis1[u],c[v])>dis1[v])
{
dis1[v]=max(dis1[u],c[v]);
q1.push({dis1[v],v});
}
}
}
}
void dijkstra2()//最大的
{
memset(dis2,0x3f,sizeof dis2);
dis2[1]=min(dis2[1],c[1]);
q2.push({dis2[1],1});
while (!q2.empty())
{
int u=q2.top().second;
q2.pop();
if (vis2[u]) continue;
vis2[u]=true;
int _size=e1[u].size();
for (int i=0;i<_size;i++)
{
int v=e1[u][i];
if (min(dis2[u],c[v])<dis2[v])
{
dis2[v]=min(dis2[u],c[v]);
q2.push({dis2[v],v});
}
}
}
}
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1;i<=n;i++) scanf("%lld",&c[i]);
for (int i=1;i<=m;i++)
{
int x,y,z;
scanf("%lld%lld%lld",&x,&y,&z);
if (z==1) e1[x].push_back(y),e2[y].push_back(x);
else
{
e1[x].push_back(y),e1[y].push_back(x);
e2[x].push_back(y),e2[y].push_back(x);
}
}
dijkstra1();
dijkstra2();
for (int i=1;i<=n;i++) ans=max(ans,dis1[i]-dis2[i]);
printf("%lld",ans);
return 0;
}
D.汽车加油
注意到\(n\)的范围并不大,于是我们可以设状态 \(f_{i,j,k}\) 表示到 \((i,j)\) 还可以走 \(k\) 步的所需最小代价。这是好转移的,先考虑两种加油的情况,再考虑移动的情况,按照跑\(Dijkstra\)的方式跑一遍即可。
贴
#incIude <bits/stdc++.h>
#define int long long
#define pin pair<int,node>
using namespace std;
const int N=105;
const int K=12;
int n,k,a,b,c;
int mp[N][N];
struct node{
int u,v,k,w;
bool operator < (const node &x) const{
return x.w<w;
}
};
priority_queue <node> q;
int vis[N][N][K];
int f[N][N][K];//f[i][j][k]:走到i,j还剩k步的最小费用
int dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
int ans=9e18;
void dijkstra()
{
memset(f,0x3f,sizeof f);
f[1][1][k]=0;
q.push((node){1,1,k,0});
while (!q.empty())
{
int x=q.top().u,y=q.top().v,cost=q.top().k;
q.pop();
if (vis[x][y][cost]) continue;
vis[x][y][cost]=true;
if (mp[x][y]&&cost!=k)
{
if (f[x][y][k]>f[x][y][cost]+a)
{
f[x][y][k]=f[x][y][cost]+a;
q.push((node){x,y,k,f[x][y][k]});
}
continue;
}else{
if (f[x][y][k]>f[x][y][cost]+a+c){
f[x][y][k]=f[x][y][cost]+a+c;
q.push((node){x,y,k,f[x][y][k]});
}
}
if (cost>0)
{
for (int i=0;i<4;i++)
{
int xx=x+dx[i],yy=y+dy[i];
if (xx<1||xx>n||yy<1||yy>n) continue;
int w;
if (i<=1) w=0;
else w=b;
if (f[xx][yy][cost-1]>f[x][y][cost]+w)
{
f[xx][yy][cost-1]=f[x][y][cost]+w;
q.push({xx,yy,cost-1,f[xx][yy][cost-1]});
}
}
}
}
}
signed main()
{
scanf("%lld%lld%lld%lld%lld",&n,&k,&a,&b,&c);
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++) scanf("%lld",&mp[i][j]);
}
dijkstra();
for (int i=0;i<=k;i++) ans=min(ans,f[n][n][i]);
printf("%lld",ans);
return 0;
}
E.比较大小
这就是一个\(Floyd\)传递闭包
注意到\(n\)的范围并不大,所以可以用\(Floyd\)维护连通性。若出现\(f_{u,v}=f_{v,u}=1\)的情况,那么就出现了环,给出关系有误;否则判断\(f_{u,v}\)输出即可。
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int m,n,q;
int f[N][N];
signed main()
{
scanf("%lld%lld%lld",&m,&n,&q);
int a,b;
while (m--)
{
scanf("%lld%lld",&a,&b);
f[a][b]=1;
}
for (int k=1;k<=n;k++)
{
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++) f[i][j]|=f[i][k]&f[k][j];
}
}
for (int i=1;i<=n;i++)
{
for (int j=1;j<=i;j++)
{
if (f[i][j]&&f[j][i])
{
printf("10000words to copy");
return 0;
}
}
}
while (q--)
{
scanf("%lld%lld",&a,&b);
if (f[a][b]) printf("YES\n");
else if(f[b][a]) printf("NO\n");
else printf("DK\n");
}
return 0;
}
F.删边问题
“最大的权值最小”,一眼二分。每次二分答案删边的价值,边权小于\(mid\)的就不走,然后照常跑\(Dijkstra\)就行,复杂度\(O(m\ log_{2}\ m)\)可以过
贴
#incIude <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=2e4+5;
int n,m,T;
struct node{
int nxt,len,val;
};
vector <node> e[N];
int mx;
int ans;
int dis[N],vis[N];
int dijkstra(int x)
{
memset(dis,0x3f,sizeof dis);
memset(vis,0,sizeof vis);
priority_queue < pii,vector <pii>,greater <pii> > q;
dis[1]=0;
q.push(make_pair(0,1));
while (!q.empty())
{
int u=q.top().second;
q.pop();
if (vis[u]) continue;
vis[u]=true;
int _size=e[u].size();
for (int i=0;i<_size;i++)
{
int v=e[u][i].nxt,p=e[u][i].val,w=e[u][i].len;
if (p<=x) continue;
if (dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
q.push(make_pair(dis[v],v));
}
}
}
return dis[n];
}
bool check(int x)
{
if (dijkstra(x)>=T) return true;
return false;
}
signed main()
{
scanf("%lld%lld%lld",&n,&m,&T);
for (int i=1,u,v,len,val;i<=m;i++)
{
scanf("%lld%lld%lld%lld",&u,&v,&len,&val);
e[u].push_back((node){v,len,val});
mx=max(mx,val);
}
if (check(0)) { printf("-1 %lld",dis[n]); return 0; }
int l=1,r=mx;
while (l<r)
{
int mid=(l+r)/2;
if (check(mid)) r=mid;
else l=mid+1;
}
printf("%lld",l);
return 0;
}
G.修建道路
注意到所求答案是所选边中剩下边权的最大值\(mx\),这就意味着另外\(k\)条路径的长度要大于\(mx\)才能最优。注意到对于一条路径,不同\(mx\)所对应的\(k'\)具有单调性,所以可以直接二分答案。
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e3+5;
const int inf=0x3f3f3f3f3f3f3f3f;
int n,p,k;
struct node{
int v,w;
};
vector <node> e[N];
int dis[N],vis[N];
int dijkstra(int x)
{
priority_queue < pair<int,int> > q;
dis[1]=0;
q.push({0,1});
while (!q.empty())
{
int u=q.top().second;
q.pop();
if (vis[u]) continue;
vis[u]=true;
int _size=e[u].size();
for (int i=0;i<_size;i++)
{
int v=e[u][i].v,w=e[u][i].w;
if (dis[v]>dis[u]+(w>x))
{
dis[v]=dis[u]+(w>x);
q.push({-dis[v],v});
}
}
}
return dis[n];
}
bool check(int x)
{
memset(dis,0x3f,sizeof dis);
memset(vis,0,sizeof vis);
if (dijkstra(x)<=k) return true;
return false;
}
signed main()
{
scanf("%lld%lld%lld",&n,&p,&k);
for (int i=1,u,v,l;i<=p;i++)
{
scanf("%lld%lld%lld",&u,&v,&l);
e[u].push_back({v,l});
e[v].push_back({u,l});
}
int l=0,r=1000001;
while (l<r)
{
int mid=l+r>>1;
if (check(mid)) r=mid;
else l=mid+1;
}
if (r==1000001) printf("-1");
else printf("%lld",r);
return 0;
}
H.最小花费
注意到\(m,k\)并没有这么大,于是我们可以在\(Dijkstra\)中大胆地再设一维来维护还有几次0花费的机会。然后照常跑\(Dijkstra\)即可。
贴
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int n,m,k;
int s,t;
struct node{
int nxt,val;
};
vector <node> e[N];
struct NODE{
int dis,pos,k;
bool operator < (const NODE &x) const{
return x.dis<dis;
}
};
int ans=0x3f3f3f3f3f3f3f3f;
priority_queue <NODE> q;
int dis[N][20],vis[N][20];//到i还能走j个0
void dijkstra()
{
memset(dis,0x3f,sizeof dis);
memset(vis,0,sizeof vis);
dis[s][k]=0;
q.push({0,s,k});
while (!q.empty())
{
int u=q.top().pos,kk=q.top().k;
q.pop();
if (vis[u][kk]) continue;
vis[u][kk]=1;
int _size=e[u].size();
for (int i=0;i<_size;i++)
{
int v=e[u][i].nxt,w=e[u][i].val;
if (dis[u][kk]+w<dis[v][kk])
{
dis[v][kk]=dis[u][kk]+w;
q.push({dis[v][kk],v,kk});
}
if (kk&&dis[u][kk]<dis[v][kk-1])
{
dis[v][kk-1]=dis[u][kk];
q.push({dis[v][kk-1],v,kk-1});
}
}
}
}
signed main()
{
scanf("%lld%lld%lld",&n,&m,&k);
scanf("%lld%lld",&s,&t);
for (int i=1,x,y,v;i<=m;i++)
{
scanf("%lld%lld%lld",&x,&y,&v);
e[x].push_back({y,v});
e[y].push_back({x,v});
}
dijkstra();
for (int i=0;i<=k;i++) ans=min(ans,dis[t][i]);
printf("%lld",ans);
return 0;
}
I.收费站点
注意到答案具有单调性,所以可以二分所交费用的最大值,每次跑\(Dijkstra\)求出到\(v\)的最小花费、与总容量\(s\)比较并判断是否合法即可。
贴
#incIude <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=1e4+5;
int inf=1e10;
int n,m,s,t,tol;
int f[N];
struct node{
int nxt,val;
};
vector <node> e[N];
int mx;
int ans=inf;
int dis[N],vis[N];
int dijkstra(int x)
{
priority_queue < pii,vector <pii>,greater <pii> > q;
if (f[s]>x) return inf;
dis[s]=0;
q.push({0,s});
while (!q.empty())
{
int u=q.top().second;
q.pop();
if (vis[u]) continue;
vis[u]=1;
int _size=e[u].size();
for (int i=0;i<_size;i++)
{
int v=e[u][i].nxt,w=e[u][i].val;
if (f[v]>x) continue;
if (dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
q.push({dis[v],v});
}
}
}
return dis[t];
}
bool check(int x)
{
memset(dis,0x3f,sizeof dis);
memset(vis,0,sizeof vis);
if (dijkstra(x)<=tol) return true;
else return false;
}
signed main()
{
scanf("%lld%lld%lld%lld%lld",&n,&m,&s,&t,&tol);
for (int i=1;i<=n;i++) scanf("%lld",&f[i]);
for (int i=1,a,b,c;i<=m;i++)
{
scanf("%lld%lld%lld",&a,&b,&c);
e[a].push_back({b,c});
e[b].push_back({a,c});
}
int l=0,r=inf;
while (l<r)
{
int mid=l+r>>1;
if (check(mid)) r=mid,ans=min(ans,r);
else l=mid+1;
}
if (ans==inf) printf("-1");
else printf("%lld",ans);
return 0;
}