最短路&差分约束笔记
最短路径
基础算法
特殊图
特殊图即边权只包含 或 或某个特定的数的图。这种图可以用 在 时间内求出单源最短路,在 内求出多源最短路。
单源最短路径
单元最短路径指的是在一张联通图中,起点 到其他所有点的最短路径。
计算单元最短路的常见算法有:,。
若图带负边权(注意,此时只能是有向图,无向图负边权类似负环),则必须使用 ,时间复杂度 , 表示边的数量;最坏时间复杂度会被卡到 , 表示点的数量。
若图带正边权,则使用 速度更快。堆优化时间复杂度约为 ,相当稳定。
若图边权为 ,则使用 即可,由于 的特性,第一次碰到的点就是最短路,所以时间复杂度是 ;若边权是 ,使用 即可,时间复杂度仍为 。
多源最短路径
在一张连通图中,分别以点 为起点,到其他点的最短距离。
算法可以在 的时间求解多源最短路。算法的代码是这样的:
for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) f[i][j]=min(f[i][k]+f[k][j],f[i][j]);
本质是:当 时,任意点对 的最短路径上都只包括了 两点(端点不算),对于 的点,并未包括在内。
坑点:该算法需要判断重边的情况(求最短路的话是取边中的)。
可以用这个性质 算法还能求解图中的最小环。题目:无向图的最小环问题。
的衍生应用:求传递闭包(即任意两点之间的连通性)。
朴素的 做法:直接将 f[i][j]=min(f[i][k]+f[k][j],f[i][j]);
改为 f[i][j]|=f[i][k]&f[k][j]
即可。
优化的传递闭包,时间复杂度 。
观察到如果 ,那么原式即可转化为:,而由于 是同一位,那么即用 将 表示为点 的联通情况,原式转化为 ,代码:
for(int k=1;k<=n;k++){ for(int i=1;i<=n;i++) { if(f[i][k]) f[i]=f[i]|f[k]; } }
模板题:[JSOI2010] 连通数
全源最短路算法。
先从一超级源点(记作 ,向每个点连一条权值为 的边)出发,计算出到每个点的最短距离 ,接着将 的边权重新赋值为 ,然后从每个点跑一遍 即可,时复()。
最短路满足三角不等式:,所以新赋值的边权一定是 的,并且对于一条 的路径,用赋值后的边权计算得:,容易发现只与起点和终点的 有关,所以这样求出来的最短路一定是最短的。
多源多汇最短路
并不等同于多源最短路,这种问题包括:一个终点,多个起点,求起点到终点的最短路;多个起点,多个终点,求任意两点的最短路的最值。
这种问题的通用解法,就是建立超级原点或超级汇点。
对于第一个问题,我们可以建立超级源点,记作 ,然后向每个起点连一条边权为 的边,然后直接从 号点跑一遍最短路即可。
对于第二个问题,建立一个超级原点(记作 ),向每个起点连一条边权为 的边,建立一个超级汇点(记作 ),从每个终点向其连一条边权为 边,接着从 到 跑一遍最短路即可。
负环
【模板】负环
负环是一个相当恶心的东西,只要有负环在,就不存在最短路(可以在负环绕,越绕距离越小)(包括负边权的双向边),判负环的方式就是跑 ,然后记录一个 数组,表示最短路径点的数量,如果存在 ,说明最短路径上的点超过了 ,但是这显然是不可能的。
判断负环一定要从一个可以到达所有点的点出发,需要建立超级源点。并且有一个很 的方法,就是当总入队次数超过 时,说明很大概率是有负环的,此时直接跳出即可。
最短路径树(SPT)
在这里讲了。
同余最短路
用于解决 的问题。
我们记一个合法解为 ,那么 显然可以表示为 的形式(其中),那么 实际上是 模 意义下的值。如果对于一个 ,我们可以求出所有可行的 ,那么我们显然可以计算出 以内的方案数。
设 表示模 意义下为 的最小值。比如 表示模 的最小值,也许是 ,也可以是 。
那么显然有如下转移:
由于转移顺序并不确定,所以将其当做最短路来做。可以看做 向 连了一条边权为 的边。
然后跑一遍最短路,求出 ,那么答案就是
差分约束
差分约束可用于求解形如下面的方程组的特殊解:
我们随便拿一个式子出来,变形得:
可以发现,这个东西很像三角不等式。如果将其看做从 向 连的一条边权为 的有向边,那么上述结果就是求完最长路的情况。
如果求最长路,如果存在正环,就说明无解;求最短路,存在负环,说明无解。
如果求最小值,我们显然将式子变形成: 的形式,即跑一遍最长路;如果求最大值,将式子变形成 的形式,跑最短路。
一定要从一个可以到达所有点的点开始,这样才能保证符合所有情况,建立超级源点即可。
题目
最短路计数
题意简述:求 到其他点的最短路数量(边权为 )。
算是一个基本模型,我们在原来记录最短路长度的基础上再记录一个方案数,在松弛的时候,如果距离等于现在储存的距离,就 ,否则设为走过来的点的方案数。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int N=1e6+10; const int MOD=100003; int n,m; vector<int> g[N]; queue<int> q; int dis[N],vis[N],cnt[N]; void bfs() { memset(dis,0x3f,sizeof(dis)); dis[1]=0; vis[1]=1; cnt[1]=1; q.push(1); while(q.size()) { int x=q.front(); q.pop(); for(int y:g[x]) { if(dis[y]>dis[x]+1) { dis[y]=dis[x]+1; cnt[y]=cnt[x]; if(!vis[y]) { vis[y]=1; q.push(y); } } else if(dis[y]==dis[x]+1) { cnt[y]=(cnt[y]+cnt[x])%MOD; } } } for(int i=1;i<=n;i++) cout<<cnt[i]<<'\n'; } int main() { cin>>n>>m; for(int i=1;i<=m;i++) { int x,y; cin>>x>>y; g[x].push_back(y); g[y].push_back(x); } bfs(); return 0; }
灾后重建
题意简述:每个点有一个恢复时间,在这个时间之前,计算最短路不能经过这个点,现在给定 ,要求计算在 时刻,从 到 的最短路。
考察对 的理解。
首先是一个全源最短路问题,观察到 ,大胆用 ,由于该算法的本质是:只有在以 为中转点计算过以后,任意两点的最短路中才会包含 点,所以我们可以写一个 函数,表示将 作为中转点计算一遍。
由于题目保证 不降,所以还是挺好写的。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int N=210; int n,m,q; int f[N][N]; int bt[N],vis[N]; void change(int 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]); } int main() { memset(f,0x3f,sizeof(f)); cin>>n>>m; for(int i=1;i<=n;i++) { cin>>bt[i]; } for(int i=1;i<=m;i++) { int x,y,w; cin>>x>>y>>w; ++x; ++y; f[x][y]=f[y][x]=w; } int p=1; cin>>q; while(q--) { int x,y,t; cin>>x>>y>>t; ++x; ++y; while(bt[p]<=t&&p<=n) { change(p); vis[p]=1; p++; } if(!vis[x]||!vis[y]||f[x][y]==1061109567) cout<<-1<<endl; else cout<<f[x][y]<<endl; } return 0; }
大逃离
题意简述:求严格次短路。
记录 表示到 最短路, 表示到 的严格次短路,仔细分析一下转移就好。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long LL; const int N=5100; int n,m,k; int deg[N]; struct node { int to,w; }; vector<node> g[N]; set<int> se[N]; void add(int x,int y,int z) { g[x].push_back({y,z}); } queue<int> q; priority_queue<int,vector<int>,greater<int>> qq; LL dis_min[N],dis_cmin[N]; int f[N]; void spfa() { q.push(1); f[1]=1; memset(dis_min,0x3f,sizeof(dis_min)); memset(dis_cmin,0x3f,sizeof(dis_cmin)); dis_min[1]=0; for(node t:g[1]) { int y=t.to,w=t.w; if(y!=n&°[y]<k) continue; if(dis_min[1]+2*w<dis_cmin[1]) dis_cmin[1]=dis_min[1]+2*w; } while(q.size()) { int x=q.front(); f[x]=0; q.pop(); for(node t:g[x]) { int y=t.to,w=t.w; if(y!=n&°[y]<k) continue; if(dis_min[y]>dis_min[x]+w) { dis_cmin[y]=dis_min[y]; dis_min[y]=dis_min[x]+w; if(!f[y]) { f[y]=1; q.push(y); } if(dis_cmin[y]>dis_cmin[x]+w) { dis_cmin[y]=dis_cmin[x]+w; } } else if(dis_min[y]!=dis_min[x]+w&&dis_cmin[y]>dis_min[x]+w) { dis_cmin[y]=dis_min[x]+w; if(!f[y]) { f[y]=1; q.push(y); } } dis_cmin[y]=min(dis_cmin[y],dis_min[y]+2*w); } } } int main() { cin>>n>>m>>k; for(int i=1;i<=m;i++) { int x,y,z; cin>>x>>y>>z; add(x,y,z); add(y,x,z); se[x].insert(y); se[y].insert(x); } for(int i=1;i<=n;i++) { deg[i]=se[i].size(); } spfa(); if(dis_cmin[n]==4557430888798830399) cout<<-1; else cout<<dis_cmin[n]; return 0; }
Elaxia的路线
题意简述:求两个点对最短路的最长公共路径。
写过题解,这里再强调一下最短路的相关应用:
判断一条边 是否在 的最短路上呢?只要满足如下式子:
或者:
即可,这里需要从起点和终点分别出发跑一遍最短路。
[POI2014] RAJ-Rally
题意简述:给定一个边权均为 的 ,求删去一个点后的最长路径的最小值。
想不到啊,居然真的是一个一个删点,然后优化求最小值。。。
记 表示 为起点的最短路, 表示 为终点的最短路。
在 中有一个很神奇的结论。最短路可以表示为:
其实很好理解,若存在一条 的边,那么 的拓扑序一定是 的,不可能通过一个环在绕回 的前面。所以在 中,这个式子是成立的。
那么在非中,其实也可以推出类似的结论,我们只需规定好起点终点即可:
我们按照拓扑序从小到大进行删点,删完点的集合称作,还没删的点的集合称作 ,直观的看: 在 的前面。
所以最长路应该来自:,如果此时删除 集合中的 点,那么很显然,所有包含 点的最短路都应该删去,即,我们应该将 删去。然后在剩下的值中统计最小值。在计算完贡献后,显然应该将 点加进 集合中,此时加入 即可。
为了维护以上信息,我们需要实现几个操作:
- 插入数字。
- 删除数字。
- 查询最小值。
由于数字可能有重复,所以使用 即可。注意 删除值时,使用:s.erase(s.find(x));
,这样是删除一个,而 s.erase(x);
是将 x
全删。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long LL; const int N=5e5+10; int n,m; struct node { int to,w; }; vector<node> gz[N],gf[N]; int degz[N],degf[N]; int dis_ed[N],dis_st[N]; int topo_num[N],id[N]; void topoz() { queue<int> q; while(q.size()) q.pop(); q.push(0); while(q.size()) { int x=q.front(); q.pop(); for(auto t:gz[x]) { int y=t.to,w=t.w; if(dis_ed[y]<dis_ed[x]+w) { dis_ed[y]=dis_ed[x]+w; } degz[y]--; if(!degz[y]) { q.push(y); topo_num[y]=++topo_num[0]; id[topo_num[y]]=y; } } } } void topof() { queue<int> q; while(q.size()) q.pop(); q.push(n+1); while(q.size()) { int x=q.front(); q.pop(); for(auto t:gf[x]) { int y=t.to,w=t.w; if(dis_st[y]<dis_st[x]+w) { dis_st[y]=dis_st[x]+w; } degf[y]--; if(!degf[y]) { q.push(y); } } } } multiset<int> a,b; int main() { cin>>n>>m; for(int i=1;i<=m;i++) { int x,y; cin>>x>>y; gz[x].push_back({y,1}); degz[y]++; gf[y].push_back({x,1}); degf[x]++; } for(int i=1;i<=n;i++) { degz[i]++; gz[0].push_back({i,0}); } for(int i=1;i<=n;i++) { gf[n+1].push_back({i,0}); degf[i]++; } topof(); topoz(); int ans=1e9,p=0; for(int i=1;i<=n;i++) b.insert(dis_st[i]); for(int i=1;i<=n;i++) {//此处i表示拓扑序 int x=id[i]; for(auto t:gf[x]) { int y=t.to,w=t.w; if(!w) continue; a.erase(a.find(dis_ed[y]+dis_st[x]+1)); } b.erase(b.find(dis_st[x])); int maxn=0; if(b.size()) maxn=max(maxn,*b.rbegin()); if(a.size()) maxn=max(maxn,*a.rbegin()); if(maxn<ans) { ans=maxn; p=x; } a.insert(dis_ed[x]); for(auto t:gz[x]) { int y=t.to,w=t.w; if(!w) continue; a.insert(dis_ed[x]+1+dis_st[y]); } } cout<<p<<' '<<ans; return 0; }
[GXOI/GZOI2019] 旅行者
题意简述:求 个点两两最短路的最小值。
求“两两”的其实并不陌生。多源多汇最短路就能求若干个起点到若干个终点的最短路的最小值,但现在问题是:这题求的两两,我们根本不知道谁是起点,谁是终点。
所以一个很暴力的想法就是:爆搜进行分组,分到 组的作为起点,分到 组的作为终点,建超级源汇跑一遍就能求,时间复杂度 。
这样很显然是过不了的,甚至不如直接枚举起点和终点,然后直接跑最短路,时间复杂度 。
让我们来思考上面的做法究竟慢在哪里?
我们设取到最小值的两点是 ,那么只要将 分到不同的组别就好,至于谁和他们一组,这个无所谓。
这里就要采用二进制分组的思想,由于 至多有 个二进制位不相同,所以我们按照二进制位分组,枚举 ,若第 位为 ,分到 组,否则分到 组,注意将起点终点调换。
总时间复杂度是 ,开 还是不成问题的。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long LL; const int N=1e5+10; int T,n,m,k; int city[N]; struct node { int to,w; }; vector<node> g[N]; LL minn=1e18; #define PII pair<LL,int> priority_queue<PII> q; LL dis[N]; int vis[N]; void dij(int st,int ed) { memset(vis,0,sizeof(vis)); memset(dis,0x3f,sizeof(dis)); dis[st]=0; q.push({0,st}); while(q.size()) { int x=q.top().second; q.pop(); if(vis[x]) continue; vis[x]=1; for(node t:g[x]) { int y=t.to,w=t.w; if(dis[y]>dis[x]+w) { dis[y]=dis[x]+w; q.push({-dis[y],y}); } } } minn=min(minn,dis[ed]); } void Solve() { cin>>n>>m>>k; for(int i=1;i<=m;i++) { int x,y,z; cin>>x>>y>>z; g[x].push_back({y,z}); } for(int i=1;i<=k;i++) { cin>>city[i]; } sort(city+1,city+1+k); k=unique(city+1,city+1+k)-city-1; int len=log(k); for(int i=0;i<=len;i++) { for(int j=1;j<=k;j++) { if(j&(1<<i)) g[n+2].push_back({city[j],0}); else g[city[j]].push_back({n+1,0}); } dij(n+2,n+1); g[n+2].clear(); for(int j=1;j<=k;j++) { if(!(j&(1<<i))) g[city[j]].pop_back(); } for(int j=1;j<=k;j++) { if(!(j&(1<<i))) g[n+2].push_back({city[j],0}); else g[city[j]].push_back({n+1,0}); } dij(n+2,n+1); g[n+2].clear(); for(int j=1;j<=k;j++) { if(j&(1<<i)) g[city[j]].pop_back(); } } cout<<minn<<endl; } void Clear() { minn=1e18; for(int i=0;i<=n+2;i++) g[i].clear(); memset(city,0,sizeof(city)); n=m=k=0; } int main() { cin>>T; while(T--) { Solve(); Clear(); } return 0; }
[国家集训队] 墨墨的等式
将 的解转化为 的解减去 的解即可。
点击查看代码
#include<bits/stdc++.h> using namespace std; #define PII pair<int,int> typedef long long LL; const int N=20,M=5e5+10; int n,q; LL l,r; int a[N]; struct node{ int to,w; }; vector<node> g[M]; void add(int x,int y,int z) { g[x].push_back({y,z}); } LL dis[M]; int vis[M]; priority_queue<PII> qu; void dij() { memset(dis,0x3f,sizeof(dis)); dis[0]=0; qu.push({0,0}); while(qu.size()) { int x=qu.top().second; qu.pop(); if(vis[x]) continue; vis[x]=1; for(auto t:g[x]) { int y=t.to,w=t.w; if(dis[y]>dis[x]+w) { dis[y]=dis[x]+w; qu.push({-dis[y],y}); } } } } LL query(LL x,LL y) { if(x>=y) return (x-y)/q+1; else return 0; } int main() { cin>>n>>l>>r; for(int i=1;i<=n;i++) { cin>>a[i]; q=max(q,a[i]); } for(int i=0;i<q;i++) { for(int j=1;j<=n;j++) { add(i,(i+a[j])%q,a[j]); } } dij(); LL ans=0; for(int i=0;i<q;i++) { ans+=query(r,dis[i])-query(l-1,dis[i]); } cout<<ans; return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效