最短路&差分约束笔记
最短路径
基础算法
特殊图
特殊图即边权只包含 \(0,1\) 或 \(1\) 或某个特定的数的图。这种图可以用 \(bfs\) 在 \(\rm O(n)\) 时间内求出单源最短路,在 \(\rm O(n^2)\) 内求出多源最短路。
单源最短路径
单元最短路径指的是在一张联通图中,起点 \(s\) 到其他所有点的最短路径。
计算单元最短路的常见算法有:\(spfa\),\(dijkstra\)。
若图带负边权(注意,此时只能是有向图,无向图负边权类似负环),则必须使用 \(spfa\),时间复杂度 \(O(kE)\),\(E\) 表示边的数量;最坏时间复杂度会被卡到 \(O(VE)\),\(V\) 表示点的数量。
若图带正边权,则使用 \(dijkstra\) 速度更快。堆优化时间复杂度约为 \(O(m\log n)\),相当稳定。
若图边权为 \(1\),则使用 \(bfs\) 即可,由于 \(bfs\) 的特性,第一次碰到的点就是最短路,所以时间复杂度是 \(O(n)\);若边权是 \(0,1\),使用 \(01bfs\) 即可,时间复杂度仍为 \(O(n)\)。
多源最短路径
在一张连通图中,分别以点 \(1\sim n\) 为起点,到其他点的最短距离。
\(Floyed\) 算法可以在 \(O(n^3)\) 的时间求解多源最短路。算法的代码是这样的:
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\) 时,任意点对 \(i\rightarrow j\) 的最短路径上都只包括了 \(1,2\) 两点(端点不算),对于 \(3\sim n\) 的点,并未包括在内。
坑点:该算法需要判断重边的情况(求最短路的话是取边中的\(\min\))。
可以用这个性质 \(Floyed\) 算法还能求解图中的最小环。题目:无向图的最小环问题。
\(Floyed\) 的衍生应用:求传递闭包(即任意两点之间的连通性)。
朴素的 \(\rm O(n^3)\) 做法:直接将 f[i][j]=min(f[i][k]+f[k][j],f[i][j]);
改为 f[i][j]|=f[i][k]&f[k][j]
即可。
\(bitset\) 优化的传递闭包,时间复杂度 \(\rm O(\frac{n^3}{\omega})\)。
观察到如果 \(f_{i,k}=1\),那么原式即可转化为:\(f_{i,j}|=f_{k,j}\),而由于 \(j\) 是同一位,那么即用 \(bitset\) 将 \(f_{i}\) 表示为点 \(i\) 的联通情况,原式转化为 \(f_i|=f_k\),代码:
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\) 的边)出发,计算出到每个点的最短距离 \(d_i\),接着将 \(u\rightarrow v\) 的边权重新赋值为 \(d_u-d_v+w_{u\rightarrow v}\),然后从每个点跑一遍 \(dijkstra\)即可,时复(\(O(nm\log n\))。
最短路满足三角不等式:\(d_v\le d_u+w_{u\rightarrow v}\),所以新赋值的边权一定是 \(\ge 0\) 的,并且对于一条 \(a\rightarrow b\rightarrow c\) 的路径,用赋值后的边权计算得:\(d_a-d_c+w_{a\rightarrow b}+w_{b\rightarrow c}\),容易发现只与起点和终点的 \(d\) 有关,所以这样求出来的最短路一定是最短的。
多源多汇最短路
并不等同于多源最短路,这种问题包括:一个终点,多个起点,求起点到终点的最短路;多个起点,多个终点,求任意两点的最短路的最值。
这种问题的通用解法,就是建立超级原点或超级汇点。
对于第一个问题,我们可以建立超级源点,记作 \(0\),然后向每个起点连一条边权为 \(0\) 的边,然后直接从 \(0\) 号点跑一遍最短路即可。
对于第二个问题,建立一个超级原点(记作 \(0\)),向每个起点连一条边权为 \(0\) 的边,建立一个超级汇点(记作 \(n+1\)),从每个终点向其连一条边权为 \(0\) 边,接着从 \(0\) 到 \(n+1\) 跑一遍最短路即可。
负环
【模板】负环
负环是一个相当恶心的东西,只要有负环在,就不存在最短路(可以在负环绕,越绕距离越小)(包括负边权的双向边),判负环的方式就是跑 \(spfa\),然后记录一个 \(cnt\) 数组,表示最短路径点的数量,如果存在 \(cnt_x>n\),说明最短路径上的点超过了 \(n\),但是这显然是不可能的。
判断负环一定要从一个可以到达所有点的点出发,需要建立超级源点。并且有一个很 \(trick\) 的方法,就是当总入队次数超过 \(5\times E\) 时,说明很大概率是有负环的,此时直接跳出即可。
最短路径树(SPT)
在这里讲了。
同余最短路
用于解决 \(0\le\sum_{i=1}^{n}a_ix_i\le n\) 的问题。
我们记一个合法解为 \(b=\sum_{i=1}^{n}a_ix_i\),那么 \(b\) 显然可以表示为 \(b=i+p\times q\) 的形式(其中\(i<q\)),那么 \(i\) 实际上是 \(b\) 模 \(q\) 意义下的值。如果对于一个 \(q\),我们可以求出所有可行的 \(i\),那么我们显然可以计算出 \(n\) 以内的方案数。
设 \(dis_i\) 表示模 \(q\) 意义下为 \(i\) 的最小值。比如 \(dis_2\) 表示模 \(3=2\) 的最小值,也许是 \(2\),也可以是 \(5\)。
那么显然有如下转移:
由于转移顺序并不确定,所以将其当做最短路来做。可以看做 \(i\) 向 \((i+a[j])\% p\) 连了一条边权为 \(a[j]\) 的边。
然后跑一遍最短路,求出 \(dis_i\),那么答案就是\(\sum_{i=0}^{q-1}\left \lfloor\frac{n-dis_i}{p}\right \rfloor+1\)
差分约束
差分约束可用于求解形如下面的方程组的特殊解:
我们随便拿一个式子出来,变形得:
可以发现,这个东西很像三角不等式。如果将其看做从 \(j\) 向 \(i\) 连的一条边权为 \(k\) 的有向边,那么上述结果就是求完最长路的情况。
如果求最长路,如果存在正环,就说明无解;求最短路,存在负环,说明无解。
如果求最小值,我们显然将式子变形成:\(x_i\ge x_j+a_k\) 的形式,即跑一遍最长路;如果求最大值,将式子变形成 \(x_i\le x_j+a_k\) 的形式,跑最短路。
一定要从一个可以到达所有点的点开始,这样才能保证符合所有情况,建立超级源点即可。
题目
最短路计数
题意简述:求 \(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\) 时刻,从 \(x\) 到 \(y\) 的最短路。
考察对 \(Floyed\) 的理解。
首先是一个全源最短路问题,观察到 \(n\le 200\),大胆用 \(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;
}
大逃离
题意简述:求严格次短路。
记录 \(dismin_x\) 表示到 \(x\) 最短路,\(discmin_x\) 表示到 \(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&°[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的路线
题意简述:求两个点对最短路的最长公共路径。
写过题解,这里再强调一下最短路的相关应用:
判断一条边 \(u\leftrightarrow v\) 是否在 \(s\rightarrow t\) 的最短路上呢?只要满足如下式子:
或者:
即可,这里需要从起点和终点分别出发跑一遍最短路。
[POI2014] RAJ-Rally
题意简述:给定一个边权均为 \(1\) 的 \(DAG\),求删去一个点后的最长路径的最小值。
想不到啊,居然真的是一个一个删点,然后优化求最小值。。。
记 \(dst_x\) 表示 \(x\) 为起点的最短路,\(ded_x\) 表示 \(x\) 为终点的最短路。
在 \(DAG\) 中有一个很神奇的结论。最短路可以表示为:
其实很好理解,若存在一条 \(u\rightarrow v\) 的边,那么 \(v\) 的拓扑序一定是 \(>u\) 的,不可能通过一个环在绕回 \(u\) 的前面。所以在 \(DAG\) 中,这个式子是成立的。
那么在非\(DAG\)中,其实也可以推出类似的结论,我们只需规定好起点终点即可:
我们按照拓扑序从小到大进行删点,删完点的集合称作\(A\),还没删的点的集合称作 \(B\),直观的看:\(A\) 在 \(B\) 的前面。
所以最长路应该来自:\(ded_A,dst_B,ded_A+dst_B+w_{A\rightarrow B}\),如果此时删除 \(B\) 集合中的 \(x\) 点,那么很显然,所有包含 \(x\) 点的最短路都应该删去,即,我们应该将 \(dst_x,ded_A+w_{A\rightarrow x}+dst_x\) 删去。然后在剩下的值中统计最小值。在计算完贡献后,显然应该将 \(x\) 点加进 \(A\) 集合中,此时加入 \(ded_x,dst_x+w_{x\rightarrow B}+ded_{B}\) 即可。
为了维护以上信息,我们需要实现几个操作:
- 插入数字。
- 删除数字。
- 查询最小值。
由于数字可能有重复,所以使用 \(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(2^n\times m\log n)\)。
这样很显然是过不了的,甚至不如直接枚举起点和终点,然后直接跑最短路,时间复杂度 \(O(n^2m\log n)\)。
让我们来思考上面的做法究竟慢在哪里?
我们设取到最小值的两点是 \(st,ed\),那么只要将 \(st,ed\) 分到不同的组别就好,至于谁和他们一组,这个无所谓。
这里就要采用二进制分组的思想,由于 \(st,ed\) 至多有 \(1\) 个二进制位不相同,所以我们按照二进制位分组,枚举 \(0\sim \log n\),若第 \(i\) 位为 \(1\),分到 \(1\) 组,否则分到 \(2\) 组,注意将起点终点调换。
总时间复杂度是 \(O(m\log^2 n)\),开 \(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;
}
[国家集训队] 墨墨的等式
将 \(l\sim r\) 的解转化为 \(0\sim r\) 的解减去 \(0\sim l-1\) 的解即可。
点击查看代码
#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;
}