Loading

【笔记】最短路问题

来自\(\texttt{SharpnessV}\)省选复习计划中的最短路问题


最短路是图论一个经典模型,也是OI种的常考模型。


例题1:单源最短路径

对于只有正边权的图,\(\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;
}

例题2:全源最短路

众所周知 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;
} 

例题3:最短路计数

我们把 \(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;
}

例题4:通往奥格瑞玛的道路

经典模型,最短路可以与二分结合。

如果一张图除了边权限制,还有别的限制,我们就可以考虑二分最短路模型。

#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;
}

[ICPC-Beijing 2006]狼抓兔子

经典模型,最短路与平面图最小割的转换。

我们将每个封闭区间作为一个点,然后建立源点和汇点并建图,最后得到的一条\(S\to T\)的路径对应原图的割,最短路径就是原图的最小割。

当然本题直接网络最大流卡常也可以通过。

代码


[JLOI2011]飞行路线

经典模型,分层图。

类似于网络流中的拆点,对于原图中的一个点\(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;
}

[SDOI2010]大陆争霸

有趣的思维题,最短路与贪心的结合。

题解Solution

两年前写的排版有点丑见谅。。。


[GXOI/GZOI2019]旅行者

有趣的思维题,进制拆分与最短路的结合。

对答案产生贡献的点对\((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短路 / [SDOI2010]魔法猪学院

k短路模板,\(A\)*算法。

对于每个点,它的估价函数为它到终点的最短路。

我们在堆中维护当前代价和估价之和最小的点,当终点第\(k\)次被取出的时候,代价便是k短路的长度。

但是\(A\)*能被构造数据卡掉,需要更好的黑科技可持久化左偏树,这里不再赘述。

代码


CF1076D Edge Deletion

经典模型,最短路树。

前文 最短路计数 一题已经指出,所有关键边构成一张有向无环图。

根据这张有向无环图我们可以生成一棵树,不难发现这个树任意节点到根的路径就是原图的最短路。

所以本题我们直接输出最短路树上的\(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;
}

CF1486E Paired Payment

分层图,留给读者思考。

posted @ 2021-12-16 22:35  7KByte  阅读(187)  评论(0编辑  收藏  举报