[网络流24题]航空路线问题[题解]

航空路线问题

题意概括

从西向东依次给出 \(n\) 座城市,和这 \(n\) 座城市中的 \(m\) 条边,求出满足下列条件的旅行路径:

  • 从最西端的城市出发,单向从西向东途径若干城市到达最东端的城市,然后再单向从东向西飞回起点(可途径若干城市)。

  • 除起点城市(最西端城市)外,其他任何城市只能被访问一次

  • 尽可能经过的城市。

分析

从这道题的题面再结合范围非常有限的 \(n\) ,很容易想到可以用网络流来解决。

那么下面考虑如何建图:

首先由于每一个点只能被经过一次,那我们可以用到一个非常常见的手法,就是拆点,将 \(i\) 拆成 \(i\)\(i+n\) 两个点,再在这两个点之间连上一条流量为 \(1\) ,费用为 \(1\) 的点。

而对于 \(m\) 条边,假设 \(i\) 为每个城市 \(i\) 的入点, \(i+n\) 为城市 \(i\) 的出点,则我们只需要将西边城市的出点向东边城市的入点连一条流量为 \(1\) ,费用为 \(0\) 的边即可。那么为什么流量为 \(1\) 呢,其实理论上来讲,这里的流量设为 \(INF\) 也是可以的,但是为了方便之后的输出路径,可以直接找到残次图中 \(w[i]=0\) 的边,所以我们这里将其设置为 \(1\)

然后再用到网络流的惯用伎俩,建立超级源点超级汇点,分别与最西边的城市的入点和最东边的城市的出点连边即可,流量都可以设置为\(INF\)

但是上述建图只实现了单边过程,如何返回?

其实我们只需要将最西边城市和最东边城市的两个拆点之间的流量改为 \(2\) 即可,这也就意外这我们可以找到两条流量为 \(1\) 的增广路,恰对应来回两个过程。

这样建完图之后再跑一次最大费用。

最后怎么输出。

我们只需要两次 \(DFS\) ,第一次输出去的过程,找到一条残次图中 \(w[i]=0\) 的路径正序输出,第二次输出回的过程,找到另一条残次图中 \(w[i]=0]\) 的路径,但是这次换为倒序输出

那这样这道题就做完了。

值得注意的是,这道题是有一个特殊情况的。

正常情况下,我们用 \(EK\) 算法跑最大费用,则进行两次,就意味着找到了两条路径。但是如果考虑我们只有一条从最西端的城市连向最东端城市的可行路径,则按照上述建图过程,他只能找到一条路径。这样一来,我们就需要加上一个特殊判断,也就是如果有一条起点和终点的连边且只找到一条路径(我们就能够确认只有这一种最坏的情况了,因为这种情况确实是可行方案中经过城市数量最少的,如果有其他方案,就不会使用它),则直接输出三个字符串,第一个城市的名称,最后一个城市的名称,第一个城市的名称。

CODE

#include <bits/stdc++.h>
using namespace std;
const int N=1e2+10,M=5e3+10,INF=0x7fffffff;
int n,m,s,t,ans,tim;
char ss[N][20];
map<string,int> mp;
inline int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-') w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
	return s*w;
}
int tot=-1,v[M*2+N*2],w[M*2+N*2],p[M*2+N*2],nex[M*2+N*2],first[N*2];
inline void Add(int x,int y,int z,int c)
{
	nex[++tot]=first[x];
	first[x]=tot;
	v[tot]=y,w[tot]=z,p[tot]=c;
}
bool vis[2*N];
int pre[2*N],dis[2*N],Min[2*N];
inline bool SPFA()
{
	memset(dis,-1,sizeof(dis));
	memset(vis,false,sizeof(vis));
	queue<int> q;
	q.push(s);
	vis[s]=true,dis[s]=0,Min[s]=INF;
	while(!q.empty()){
		int now=q.front(); q.pop();
		vis[now]=false;
		for(register int i=first[now];i!=-1;i=nex[i]){
			int to=v[i];
			if(!w[i]) continue;
			if(dis[now]+p[i]>dis[to]){
				dis[to]=dis[now]+p[i];
				Min[to]=min(Min[now],w[i]);
				pre[to]=i;
				if(!vis[to]) q.push(to),vis[to]=true;
			}
		}
	}
	return dis[t]!=-1;
}
inline void EK()
{
	while(SPFA()){
		tim++;
		ans+=dis[t]*Min[t];
		int temp=t,i;
		while(temp!=s){
			i=pre[temp];
			w[i]-=Min[t];
			w[i^1]+=Min[t];
			temp=v[i^1];
		}
	}
}
inline void DFS1(int x) //第一次正序,为去的过程 
{
	cout<<ss[x-n]<<"\n";
	vis[x-n]=true; //防止第二次找到该点 
	for(register int i=first[x];i!=-1;i=nex[i])
		if(v[i]<=n&&v[i]>=1&&!vis[v[i]]&&!w[i]) { DFS1(v[i]+n); break; }
}
inline void DFS2(int x)
{
	vis[x-n]=true;
	for(register int i=first[x];i!=-1;i=nex[i])
		if(v[i]<=n&&v[i]>=1&&!vis[v[i]]&&!w[i]) DFS2(v[i]+n);
	cout<<ss[x-n]<<"\n";
}
int main()
{
	memset(first,-1,sizeof(first));
	n=read(),m=read();
	s=0,t=2*n+1; //起点和终点的位置 
	for(register int i=1;i<=n;i++) cin>>ss[i],mp[ss[i]]=i; //存入编号 
	bool flag=false; 
	for(register int i=1;i<=m;i++){
		char s1[20],s2[20];
		cin>>s1; cin>>s2;
		int x=mp[s1],y=mp[s2];
		if(x>y) swap(x,y); //西向东建边
		if(x==1&&y==n) flag=true; //如果有一条起点和终点的连线,是一定成功的 
		Add(x+n,y,1,0),Add(y,x+n,0,0); 
	}
	Add(s,1,INF,0),Add(1,s,0,0);
	Add(2*n,t,INF,0),Add(t,2*n,0,0);
	for(register int i=1;i<=n;i++){
		if(i!=1&&i!=n) Add(i,i+n,1,1),Add(i+n,i,0,-1);//建立点之间的边
		else Add(i,i+n,2,1),Add(i+n,i,0,-1);
	}
	EK();
	if(tim==2) printf("%d\n",ans-2); //重复计算1,n
	else if(tim==1&&flag) { printf("2\n"),cout<<ss[1]<<"\n"<<ss[n]<<"\n"<<ss[1]<<"\n"; return 0; } //明显只有首尾相连这条线路
	else { printf("No Solution!\n"); return 0; }
	memset(vis,false,sizeof(vis));
	DFS1(1+n),DFS2(1+n); //输出答案 
	return 0;
}
posted @ 2021-02-22 20:10  ╰⋛⋋⊱๑落叶๑⊰⋌⋚╯  阅读(91)  评论(0编辑  收藏  举报