最短路&差分约束笔记

最短路径

基础算法

特殊图

特殊图即边权只包含 0,11 或某个特定的数的图。这种图可以用 bfsO(n) 时间内求出单源最短路,在 O(n2) 内求出多源最短路。

单源最短路径

单元最短路径指的是在一张联通图中,起点 s 到其他所有点的最短路径。

计算单元最短路的常见算法有:spfadijkstra

若图带负边权(注意,此时只能是有向图,无向图负边权类似负环),则必须使用 spfa,时间复杂度 O(kE)E 表示边的数量;最坏时间复杂度会被卡到 O(VE)V 表示点的数量。

若图带正边权,则使用 dijkstra 速度更快。堆优化时间复杂度约为 O(mlogn),相当稳定。

若图边权为 1,则使用 bfs 即可,由于 bfs 的特性,第一次碰到的点就是最短路,所以时间复杂度是 O(n);若边权是 0,1,使用 01bfs 即可,时间复杂度仍为 O(n)

多源最短路径

在一张连通图中,分别以点 1n 为起点,到其他点的最短距离。

Floyed 算法可以在 O(n3) 的时间求解多源最短路。算法的代码是这样的:

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]);

本质是:当 k=2 时,任意点对 ij 的最短路径上都只包括了 1,2 两点(端点不算),对于 3n 的点,并未包括在内。

坑点:该算法需要判断重边的情况(求最短路的话是取边中的min)。

可以用这个性质 Floyed 算法还能求解图中的最小环。题目:无向图的最小环问题

Floyed 的衍生应用:求传递闭包(即任意两点之间的连通性)。

朴素的 O(n3) 做法:直接将 f[i][j]=min(f[i][k]+f[k][j],f[i][j]); 改为 f[i][j]|=f[i][k]&f[k][j] 即可。

bitset 优化的传递闭包,时间复杂度 O(n3ω)
观察到如果 fi,k=1,那么原式即可转化为:fi,j|=fk,j,而由于 j 是同一位,那么即用 bitsetfi 表示为点 i 的联通情况,原式转化为 fi|=fk,代码:

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] 连通数


Johnson 全源最短路算法。

先从一超级源点(记作 0,向每个点连一条权值为 0 的边)出发,计算出到每个点的最短距离 di,接着将 uv 的边权重新赋值为 dudv+wuv,然后从每个点跑一遍 dijkstra即可,时复(O(nmlogn)。

最短路满足三角不等式:dvdu+wuv,所以新赋值的边权一定是 0 的,并且对于一条 abc 的路径,用赋值后的边权计算得:dadc+wab+wbc,容易发现只与起点和终点的 d 有关,所以这样求出来的最短路一定是最短的。

多源多汇最短路

并不等同于多源最短路,这种问题包括:一个终点,多个起点,求起点到终点的最短路;多个起点,多个终点,求任意两点的最短路的最值。

这种问题的通用解法,就是建立超级原点或超级汇点。

对于第一个问题,我们可以建立超级源点,记作 0,然后向每个起点连一条边权为 0 的边,然后直接从 0 号点跑一遍最短路即可。

对于第二个问题,建立一个超级原点(记作 0),向每个起点连一条边权为 0 的边,建立一个超级汇点(记作 n+1),从每个终点向其连一条边权为 0 边,接着从 0n+1 跑一遍最短路即可。

负环

【模板】负环
负环是一个相当恶心的东西,只要有负环在,就不存在最短路(可以在负环绕,越绕距离越小)(包括负边权的双向边),判负环的方式就是跑 spfa,然后记录一个 cnt 数组,表示最短路径点的数量,如果存在 cntx>n,说明最短路径上的点超过了 n,但是这显然是不可能的。

判断负环一定要从一个可以到达所有点的点出发,需要建立超级源点。并且有一个很 trick 的方法,就是当总入队次数超过 5×E 时,说明很大概率是有负环的,此时直接跳出即可。

最短路径树(SPT)

这里讲了。

同余最短路

用于解决 0i=1naixin 的问题。

我们记一个合法解为 b=i=1naixi,那么 b 显然可以表示为 b=i+p×q 的形式(其中i<q),那么 i 实际上是 bq 意义下的值。如果对于一个 q,我们可以求出所有可行的 i,那么我们显然可以计算出 n 以内的方案数。

disi 表示模 q 意义下为 i 的最小值。比如 dis2 表示模 3=2 的最小值,也许是 2,也可以是 5

那么显然有如下转移:

dis(i+a[j])%p=min{disi+a[j]}

由于转移顺序并不确定,所以将其当做最短路来做。可以看做 i(i+a[j])%p 连了一条边权为 a[j] 的边。

然后跑一遍最短路,求出 disi,那么答案就是i=0q1ndisip+1

差分约束

差分约束可用于求解形如下面的方程组的特殊解:

{xi1xj1a1xi2xj2a2...xinxjnan

我们随便拿一个式子出来,变形得:

xixj+ak

可以发现,这个东西很像三角不等式。如果将其看做从 ji 连的一条边权为 k 的有向边,那么上述结果就是求完最长路的情况。

如果求最长路,如果存在正环,就说明无解;求最短路,存在负环,说明无解。

如果求最小值,我们显然将式子变形成:xixj+ak 的形式,即跑一遍最长路;如果求最大值,将式子变形成 xixj+ak 的形式,跑最短路。

一定要从一个可以到达所有点的点开始,这样才能保证符合所有情况,建立超级源点即可。

题目

最短路计数

题意简述:求 1 到其他点的最短路数量(边权为 1)。

算是一个基本模型,我们在原来记录最短路长度的基础上再记录一个方案数,在松弛的时候,如果距离等于现在储存的距离,就 +1,否则设为走过来的点的方案数。

点击查看代码
#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;
}

灾后重建

题意简述:每个点有一个恢复时间,在这个时间之前,计算最短路不能经过这个点,现在给定 x,y,t,要求计算在 t 时刻,从 xy 的最短路。

考察对 Floyed 的理解。

首先是一个全源最短路问题,观察到 n200,大胆用 Floyed,由于该算法的本质是:只有在以 k 为中转点计算过以后,任意两点的最短路中才会包含 k 点,所以我们可以写一个 update(k) 函数,表示将 k 作为中转点计算一遍。

由于题目保证 t 不降,所以还是挺好写的。

点击查看代码
#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;
}

大逃离

题意简述:求严格次短路。

记录 disminx 表示到 x 最短路,discminx 表示到 x 的严格次短路,仔细分析一下转移就好。

点击查看代码
#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&&deg[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&&deg[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的路线

题意简述:求两个点对最短路的最长公共路径。

写过题解,这里再强调一下最短路的相关应用:
判断一条边 uv 是否在 st 的最短路上呢?只要满足如下式子:

dissu+wuv+disvt=disst

或者:

dissv+wvu+disut=disst

即可,这里需要从起点和终点分别出发跑一遍最短路。

[POI2014] RAJ-Rally

题意简述:给定一个边权均为 1DAG,求删去一个点后的最长路径的最小值。

想不到啊,居然真的是一个一个删点,然后优化求最小值。。。

dstx 表示 x 为起点的最短路,dedx 表示 x 为终点的最短路。

DAG 中有一个很神奇的结论。最短路可以表示为:

dedu+wuv+dstv

其实很好理解,若存在一条 uv 的边,那么 v 的拓扑序一定是 >u 的,不可能通过一个环在绕回 u 的前面。所以在 DAG 中,这个式子是成立的。

那么在非DAG中,其实也可以推出类似的结论,我们只需规定好起点终点即可:

dissu+wuv+disvt

我们按照拓扑序从小到大进行删点,删完点的集合称作A,还没删的点的集合称作 B,直观的看:AB 的前面。

所以最长路应该来自:dedA,dstB,dedA+dstB+wAB,如果此时删除 B 集合中的 x 点,那么很显然,所有包含 x 点的最短路都应该删去,即,我们应该将 dstx,dedA+wAx+dstx 删去。然后在剩下的值中统计最小值。在计算完贡献后,显然应该将 x 点加进 A 集合中,此时加入 dedx,dstx+wxB+dedB 即可。

为了维护以上信息,我们需要实现几个操作:

  • 插入数字。
  • 删除数字。
  • 查询最小值。

由于数字可能有重复,所以使用 multiset 即可。注意 multiset 删除值时,使用: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] 旅行者

题意简述:求 n 个点两两最短路的最小值。

求“两两”的其实并不陌生。多源多汇最短路就能求若干个起点到若干个终点的最短路的最小值,但现在问题是:这题求的两两,我们根本不知道谁是起点,谁是终点。

所以一个很暴力的想法就是:爆搜进行分组,分到 1 组的作为起点,分到 2 组的作为终点,建超级源汇跑一遍就能求,时间复杂度 O(2n×mlogn)

这样很显然是过不了的,甚至不如直接枚举起点和终点,然后直接跑最短路,时间复杂度 O(n2mlogn)

让我们来思考上面的做法究竟慢在哪里?

我们设取到最小值的两点是 st,ed,那么只要将 st,ed 分到不同的组别就好,至于谁和他们一组,这个无所谓。

这里就要采用二进制分组的思想,由于 st,ed 至多有 1 个二进制位不相同,所以我们按照二进制位分组,枚举 0logn,若第 i 位为 1,分到 1 组,否则分到 2 组,注意将起点终点调换。

总时间复杂度是 O(mlog2n),开 O2 还是不成问题的。

点击查看代码
#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;
}

[国家集训队] 墨墨的等式

lr 的解转化为 0r 的解减去 0l1 的解即可。

点击查看代码
#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;
}
posted @   2017BeiJiang  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示