【笔记】最短路问题
来自\(\texttt{SharpnessV}\)的省选复习计划中的最短路问题。
最短路是图论一个经典模型,也是OI种的常考模型。
对于只有正边权的图,\(\texttt{Dijkstra}\) 是目前最优的做法。
我们用堆维护当前图中 \(\texttt{Distance}\) 最小的节点,每次用堆顶元素更新其他节点。
对于每个边和每个点最多遍历一次,每遍历一条边最多向堆中加入一个元素,所以时间复杂度为\(\rm O(M\log M)\)。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 500005
using namespace std;
int n,m,s,h[N],tot;
struct edge{
int to,nxt,val;
}e[N<<1];
void add(int x,int y,int z){
e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;e[tot].val=z;
}
priority_queue<pair<int,int> >q;
int d[N],v[N];
void dij(){
memset(d,0x3f,sizeof(d));
memset(v,0,sizeof(v));
d[s]=0;q.push(make_pair(0,s));
while(!q.empty()){
int x=q.top().second;q.pop();
v[x]=1;
for(int i=h[x];i;i=e[i].nxt)if(d[x]+e[i].val<d[e[i].to]){
d[e[i].to]=d[x]+e[i].val;q.push(make_pair(-d[e[i].to],e[i].to));
}
while(!q.empty()&&v[q.top().second])q.pop();
}
}
int main(){
scanf("%d%d%d",&n,&m,&s);
rep(i,1,m){
int x,y,z;scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
dij();rep(i,1,n)printf("%d ",d[i]);
return 0;
}
众所周知 Dij 只能跑正边权,Floyd 太慢了,SPFA 它死了。
如果我们要求全源最短路,Floyd 是\(\rm O(N^3)\),跑 \(N\) 次 SPFA 的时间复杂度是\(\rm O(N^2M)\),跑 \(N\) 次 Dij 只能求正边权,复杂度是 \(\rm O(NM\log M)\)。
考虑将图转换为正边权。
我们先建立一个源点\(S\),从\(S\)向所有点连边权为\(0\)边,然后跑单源最短路得到每个点的势能\(h\)。然后对于原图上的边\(u\to v\),边权转换为 \(h[u]-h[v]+w\)。
势能只与起始和初始位置有关,所以对于新图的一条最短路,原图也是最短路。
并且根据三角形不等式有\(h[u]+w\ge h[v]\),所以新边一定\(\ge 0\)。
然后再跑 \(N\) 次 \(\texttt{Dijkstra}\) 即可。时间复杂度 \(\rm O(NM\log M)\)。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 3005
using namespace std;
typedef pair<int,int> Pr;
vector<Pr>a[N];int n,m;
namespace task1{
int h[N],tot;
struct edge{
int to,nxt,val;
}e[N<<2];
void add(int x,int y,int z){
e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;e[tot].val=z;
}
int d[N],v[N],len[N];
queue<int>q;
bool spfa(){
rep(i,1,n)add(0,i,0);
memset(d,0x3f,sizeof(d));
q.push(0);d[0]=0;
while(!q.empty()){
int x=q.front();q.pop();v[x]=0;
if(len[x]>n+1){puts("-1");return true;}
for(int i=h[x];i;i=e[i].nxt){
int y=e[i].to;
if(d[y]>d[x]+e[i].val){
d[y]=d[x]+e[i].val;
len[y]=len[x]+1;
if(!v[y])v[y]=1,q.push(y);
}
}
}
rep(x,1,n)for(int i=h[x];i;i=e[i].nxt)a[x].push_back(make_pair(e[i].to,e[i].val+d[x]-d[e[i].to]));
//rep(i,1,n)printf("%d ",d[i]);putchar('\n');
return false;
}
}
int v[N],d[N];priority_queue<Pr>q;
void dij(int s){
memset(d,0x3f,sizeof(d));
memset(v,0,sizeof(v));d[s]=0;
q.push(make_pair(0,s));
while(!q.empty()){
int x=q.top().second;q.pop();
v[x]=1;
for(int i=0;i<(int)a[x].size();i++){
int y=a[x][i].first,z=a[x][i].second;
//cout<<"ss "<<x<<" "<<y<<" "<<z<<endl;
if(z+d[x]<d[y]){
d[y]=d[x]+z;
q.push(make_pair(-d[y],y));
}
}
while(!q.empty()&&v[q.top().second])q.pop();
}
long long ans=0;
//rep(i,1,n)cout<<" "<<d[i];cout<<endl;
rep(i,1,n)ans+=1LL*i*min(1000000000,(d[i]+task1::d[i]-task1::d[s]));
printf("%lld\n",ans);
}
int main(){
scanf("%d%d",&n,&m);
rep(i,1,m){
int x,y,z;scanf("%d%d%d",&x,&y,&z);
task1::add(x,y,z);
}
if(task1::spfa()){return 0;}
rep(i,1,n)dij(i);
return 0;
}
我们把 \(d[u]+w_{u,v}=d[v]\) 的边 \(u\to v\) 称为关键路径。
那么所有关键路径一定构成一个有向无环图。我们直接跑有向无环图路径计数即可。
#include<bits/stdc++.h>
#define mod 100003
using namespace std;
int n,m,h[100005],tot=0;
struct edge{
int to,next;
}e[400005];
void add(int x,int y){
e[++tot].next=h[x];h[x]=tot;e[tot].to=y;
}
int d[100005],cnt[100005],v[100005];
priority_queue<pair<int,int> >q;
void dij(){
memset(d,0x3f,sizeof(d));
d[1]=0;cnt[1]=1;q.push(make_pair(0,1));
while(true){
while(!q.empty()&&v[q.top().second])q.pop();
if(q.empty())break;
int x=q.top().second;q.pop();
v[x]=1;
for(int i=h[x];i;i=e[i].next)
if(d[x]+1<d[e[i].to]){
d[e[i].to]=d[x]+1;cnt[e[i].to]=cnt[x];
q.push(make_pair(-d[e[i].to],e[i].to));
}
else if(d[x]+1==d[e[i].to]){
cnt[e[i].to]=(cnt[x]+cnt[e[i].to])%mod;
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;scanf("%d%d",&x,&y);
add(x,y);add(y,x);
}
dij();
for(int i=1;i<=n;i++)printf("%d\n",cnt[i]);
return 0;
}
经典模型,最短路可以与二分结合。
如果一张图除了边权限制,还有别的限制,我们就可以考虑二分最短路模型。
#include<cstdio>
#include<queue>
#include<cstring>
#include<iostream>
using namespace std;
struct edge{
int to,next;long long data;
}e[5000000];
int n,m,f[100005],h[100005],pop=0;
void add(int x,int y,int z){
pop++;
e[pop].to=y;e[pop].data=z;
e[pop].next=h[x];h[x]=pop;
}
long long d[100005],b;int vis[100005];
queue<int>q;
long long spfa(int p){
while(!q.empty()){
int x=q.front();q.pop();
for(int i=h[x];i;i=e[i].next){
if(!vis[e[i].to]){
if(f[e[i].to]<=p){
if(d[x]+e[i].data<=d[e[i].to])
{
d[e[i].to]=d[x]+e[i].data;
q.push(e[i].to);
}
}
}
}
}
return d[n];
}
bool judge(int x){
if(f[1]>x||f[n]>x){
return false;
}
memset(d,0x7f,sizeof(d));
memset(vis,0,sizeof(vis));
d[1]=0;while(!q.empty())q.pop();
q.push(1);long long y=spfa(x);
if(y>=b)return false;
return true;
}
signed main()
{
scanf("%d%d%lld",&n,&m,&b);
for(int i=1;i<=n;i++)
scanf("%d",&f[i]);
for(int i=1;i<=m;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);add(y,x,z);
}
int l=1,r=1000000005,ans=-1;
while(l<=r){
int mid=(l+r)>>1;
if(judge(mid))r=mid-1,ans=mid;
else l=mid+1;
}
if(ans!=-1)
printf("%d\n",ans);
else printf("AFK\n");
return 0;
}
经典模型,最短路与平面图最小割的转换。
我们将每个封闭区间作为一个点,然后建立源点和汇点并建图,最后得到的一条\(S\to T\)的路径对应原图的割,最短路径就是原图的最小割。
当然本题直接网络最大流卡常也可以通过。
经典模型,分层图。
类似于网络流中的拆点,对于原图中的一个点\(v_i\),我们可以拆成若干个点\(v_{i,j}\)。形象化的,我们将\(j\)相同的节点放到一起,会得到若干层,分层图因此得名。
如果原图中节点 \(v_i\) 还有附加状态,我们可以考虑将节点拆开表示当前节点的不同状态,并在分层图中建图。
分层图建图一般将同层和异层分开考虑,这样可以使得模型更加清晰。
#include<bits/stdc++.h>
using namespace std;
int n,m,k,s,t,h[300005],tot=0;
struct edge{
int to,next,data;
}e[5000005];
void add(int x,int y,int z){
e[++tot].to=y;e[tot].data=z;e[tot].next=h[x];h[x]=tot;
}
int get(int x,int floor){return x+floor*n;}
priority_queue<pair<int,int> >q;
int d[300005],v[300005];
void dij(){
memset(d,0x7f,sizeof(d));
d[s]=0;q.push(make_pair(0,s));
while(!q.empty()){
int x=q.top().second;q.pop();
v[x]=1;
for(int i=h[x];i;i=e[i].next)
if(d[x]+e[i].data<d[e[i].to]){
d[e[i].to]=d[x]+e[i].data;
q.push(make_pair(-d[e[i].to],e[i].to));
}
while(!q.empty()&&v[q.top().second])q.pop();
}
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
scanf("%d%d",&s,&t);s++;t++;
for(int i=1;i<=m;i++){
int x,y,z;scanf("%d%d%d",&x,&y,&z);
x++;y++;
for(int i=0;i<=k;i++)add(get(x,i),get(y,i),z),add(get(y,i),get(x,i),z);
for(int i=0;i<k;i++)add(get(x,i),get(y,i+1),0),add(get(y,i),get(x,i+1),0);
}
dij();
int ans=0x7fffffff;
for(int i=0;i<=k;i++)ans=min(ans,d[get(t,i)]);
printf("%d\n",ans);
return 0;
}
有趣的思维题,最短路与贪心的结合。
两年前写的排版有点丑见谅。。。
有趣的思维题,进制拆分与最短路的结合。
对答案产生贡献的点对\((i,j)\),\(i,j\)一定在某一位不同。
那么我们枚举不同的位,当前位为\(0\)的接源点,否则接汇点,跑源汇点之间的最短路更新答案。
时间复杂度为\(\rm O(TN\log^2 N)\)。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 100005
#define M 500005
using namespace std;
int n,m,k;
struct node{
int u,v,val;
}a[M];
int h[N],tot,u[N],s,t;
struct edge{
int to,nxt,val;
}e[M<<1];
void add(int x,int y,int z){
e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;e[tot].val=z;
}
typedef long long ll;
ll d[N];bool v[N];
priority_queue<pair<ll,int> >q;
ll dij(){
memset(v,0,sizeof(v));
memset(d,0x3f,sizeof(d));
d[s]=0;q.push(make_pair(0,s));
while(!q.empty()){
int x=q.top().second;q.pop();
v[x]=1;
for(int i=h[x];i;i=e[i].nxt)if(d[e[i].to]>d[x]+e[i].val){
d[e[i].to]=d[x]+e[i].val;
q.push(make_pair(-d[e[i].to],e[i].to));
}
while(!q.empty()&&v[q.top().second])q.pop();
}
return d[t];
}
void solve(){
scanf("%d%d%d",&n,&m,&k);
rep(i,1,m)scanf("%d%d%d",&a[i].u,&a[i].v,&a[i].val);
rep(i,1,k)scanf("%d",&u[i]);
s=n+1;t=s+1;int rc=log2(n);
ll ans=1LL<<62;
rep(i,0,rc){
memset(h,0,sizeof(h));tot=0;
rep(j,1,k){
if((u[j]>>i)&1)add(s,u[j],0);
else add(u[j],t,0);
}
rep(j,1,m)add(a[j].u,a[j].v,a[j].val);
ans=min(ans,dij());
memset(h,0,sizeof(h));tot=0;
rep(j,1,k){
if((u[j]>>i)&1)add(u[j],t,0);
else add(s,u[j],0);
}
rep(j,1,m)add(a[j].u,a[j].v,a[j].val);
ans=min(ans,dij());
}
printf("%lld\n",ans);
}
int main(){
int T;scanf("%d",&T);
while(T--)solve();
return 0;
}
k短路模板,\(A\)*算法。
对于每个点,它的估价函数为它到终点的最短路。
我们在堆中维护当前代价和估价之和最小的点,当终点第\(k\)次被取出的时候,代价便是k短路的长度。
但是\(A\)*能被构造数据卡掉,需要更好的黑科技可持久化左偏树,这里不再赘述。
经典模型,最短路树。
前文 最短路计数 一题已经指出,所有关键边构成一张有向无环图。
根据这张有向无环图我们可以生成一棵树,不难发现这个树任意节点到根的路径就是原图的最短路。
所以本题我们直接输出最短路树上的\(k\)条联通的边即可。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 300005
using namespace std;
int n,m,k,h[N],tot;
map<pair<int,int>,int>c;
struct edge{
int to,nxt,val;
}e[N<<1];
void add(int x,int y,int z){
e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;e[tot].val=z;
}
priority_queue<pair<long long,int> >q;
vector<int>a[N];
long long d[N];int v[N],fa[N];
void dij(){
memset(d,0x3f,sizeof(d));
d[1]=0;q.push(make_pair(0,1));
while(!q.empty()){
int x=q.top().second;q.pop();v[x]=1;
for(int i=h[x];i;i=e[i].nxt)if(d[x]+e[i].val<d[e[i].to]){
d[e[i].to]=d[x]+e[i].val;fa[e[i].to]=x;
q.push(make_pair(-d[e[i].to],e[i].to));
}
while(!q.empty()&&v[q.top().second])q.pop();
}
rep(i,2,n)a[fa[i]].push_back(i);
}
void dfs(int x){
if(k&&x!=1)k--,printf("%d ",c[make_pair(fa[x],x)]);
if(!k)return;
for(int i=0;i<(int)a[x].size();i++)dfs(a[x][i]);
}
int main(){
scanf("%d%d%d",&n,&m,&k);
k=min(k,n-1);
rep(i,1,m){
int x,y,z;scanf("%d%d%d",&x,&y,&z);
add(x,y,z);add(y,x,z);c[make_pair(x,y)]=c[make_pair(y,x)]=i;
}
dij();printf("%d\n",k);dfs(1);
return 0;
}
CF1005F Berland and the Shortest Paths
根据生成的方式,我们知道最短路树并不唯一。
我们可以考虑任意生成一棵树,然后替换一些边。
对于两条终点相同的关键边,显然可以相互替换。
所以本题我们记录一个以\(i\)节点为终点的关键边数量,然后暴搜出所有方案即可。
本题边权为\(1\),极大程度简化了这个过程。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 200005
using namespace std;
int n,m,k,d[N],v[N],h[N],tot;
struct edge{
int to,nxt,val;
}e[N<<1];
void add(int x,int y,int z){
e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;e[tot].val=z;
}
queue<int>q;
void bfs(){
q.push(1);d[1]=0;v[1]=1;
while(!q.empty()){
int x=q.front();q.pop();
for(int i=h[x];i;i=e[i].nxt)if(!v[e[i].to])
v[e[i].to]=1,d[e[i].to]=d[x]+1,q.push(e[i].to);
}
}
vector<int>a[N];
pair<int,int>c[N];
char s[N];
void dfs(int x){
//cout<<x<<" "<<k<<endl;
if(!k)return;
if(x==n){puts(s+1);k--;return;}
int cur=c[x].second;
for(int i=0;i<(int)a[cur].size();i++){
s[a[cur][i]]='1';
dfs(x+1);
if(!k)return;
s[a[cur][i]]='0';
}
}
int main(){
scanf("%d%d%d",&n,&m,&k);
rep(i,1,m){
int x,y;scanf("%d%d",&x,&y);
add(x,y,i);add(y,x,i);
}
bfs();
rep(x,1,n)for(int i=h[x];i;i=e[i].nxt)if(d[x]+1==d[e[i].to])
a[e[i].to].push_back(e[i].val);
long long cur=1;
rep(i,2,n)c[i-1]=make_pair(-a[i].size(),i);
rep(i,2,n){
cur=cur*a[i].size();
if(cur>=k)break;
}
rep(i,1,m)s[i]='0';
k=min(1LL*k,cur);
printf("%d\n",k);
sort(c+1,c+n);
//rep(i,1,n-1)cout<<c[i].second<<" ";cout<<endl;
dfs(1);
return 0;
}
分层图,留给读者思考。