[网络流24题]火星探险问题[题解]

火星探险问题

题目大意

给你一张 \(P \times Q\) 的地图,其上共有三种地形:

  • \(mp[i][j]=0\),表示地图的第 \(i\) 行第 \(j\) 列是平坦地形,可以随意通过。

  • \(mp[i][j]=1\),表示地图的第 \(i\) 行第 \(j\) 列是障碍地形,不可通过。

  • \(mp[i][j]=2\),表示地图的第 \(i\) 行第 \(j\) 列是岩石标本,可以随意通过,但只能记录一次通过表示采集。

现在,有 \(n\) 辆探测车,它们只能向东或向南走,你需要在保证他们能够到达终点的情况下,尽可能获得多的岩石标本(注意:若探测车没有到达终点,则其采集到的岩石标本作废

题目分析

显然,此题可以用最大费用最大流解决。

考虑如何建图:

  • 对于每一个为 \(2\) 的节点,只能被采集一次,我们很自然的想到了通过拆点来解决。把节点 \(i\) 拆成节点 \(i_1\)\(i_2\) ,实现岩石标本采集一次的操作其实就是在 \(i_1\)\(i_2\) 之间连一条流量为 \(1\) ,费用也为 \(1\) 的边。但在被采集过后, \(2\) 节点还可以被经过无数次,对应的建边是再建一条流量为正无穷,费用为 \(0\) 的边。

  • 对于每一个为 \(1\) 的节点,显然任何点都不会向它连边,他也不会向任何点连边。

  • 对于每一个为 \(0\) 的节点,其实理论上来说我们应该可以不用拆点,因为除 \(1\) 外每个节点的点权都是正无穷,没有必要将点权转化为边权来限制流量,但是为了方便,我们还是把其拆成两个点,中间用一条流量为正无穷,费用为 \(0\) 的边将他们连接起来。

具体可参考下面的建图代码:

//建图(总结点数位2*p*q,源点位0,汇点为2*p*q+1)
//将所有点全部拆开
//拆开点之间的连边:0->0,流量INF,费用0,1->1流量1,费用1 
for(register int i=1;i<=p;i++){
	for(register int j=1;j<=q;j++){
		if(mp[i][j]==1) continue; //任何点都不与障碍物连边
		int u=Get_id(i,j),v; //u表示现在的节点编号,v表示目标节点编号 
		//设节点i的拆点为i' 
		//向外连边:u'->v
		for(register int k=0;k<=1;k++){
			int nx=i+dx[k],ny=j+dy[k];
			if(nx>p||ny>q) continue; //不超出边界
			if(mp[nx][ny]==1) continue; //障碍物 
			v=Get_id(nx,ny);
			Add(u+p*q,v,INF,0),Add(v,u+p*q,0,0);
		}
		//自己连接自己:u->u' 
		//对于岩石标本,建立两组边,第一组对答案产生贡献的限流1,表示他只能被采集一次
		//第二组表示岩石标本可以和普通地面一样,可以在任意时刻抵达 
		if(mp[i][j]==2) Add(u,u+p*q,1,1),Add(u+p*q,u,0,-1); //当前节点为岩石标本 
			//初开障碍物外任何点可以任意到达 
		Add(u,u+p*q,INF,0),Add(u+p*q,u,0,0);
	}
}

如何打印路径

对于路径,我们只需要从 \((1,1)\) 处在残次图上跑一边 \(DFS\) ,找到那些被流经的边,将其打印下来即可

参考代码如下:

inline void DFS(int x,int y,int u,int k)
{
	int kx,ky,mov;
	for(register int i=first[u];i!=-1;i=nex[i]){
		int to=v[i];
		if(to==s||to==t||to==u-p*q||!w[i^1]) continue;
		w[i^1]--; //经过的次数--,知道为0,说明该边上经过的探测车都已经被考虑完了
		if(to>p*q) { DFS(x,y,to,k); return; } //如果到达i'即连接其他点的拆点 
		if(to==Get_id(x,y)+1) kx=x,ky=y+1,mov=1; //判断走向了哪一个点 
		else kx=x+1,ky=y,mov=0;
		printf("%d %d\n",k,mov);
		DFS(kx,ky,to+p*q,k); //递归 
		return;
	}
}

注意

本题的 \(p\)\(q\)输入顺序,先输入的列,再输入的行,我调了一年才发现,简直服了。

CODE

#include <bits/stdc++.h>
using namespace std;
const int N=1e2,INF=0x3f3f3f3f;
const int dx[2]={0,1},dy[2]={1,0};
int n,p,q;
int s,t,ans,maxf;
int mp[N][N];
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[N*N*N],w[N*N*N],pay[N*N*N],nex[N*N*N],first[2*N*N];
inline void Add(int x,int y,int z,int c)
{
	nex[++tot]=first[x];
	first[x]=tot;
	v[tot]=y,w[tot]=z,pay[tot]=c;
}
inline int Get_id(int x,int y) { return (x-1)*q+y; }
bool vis[2*N*N];
//Min[i]表示到达点i经过的边流量最小的一个 
int dis[2*N*N],pre[2*N*N],Min[2*N*N];
inline bool SPFA()
{
	for(register int i=s;i<=t;i++) vis[i]=false;
	for(register int i=s;i<=t;i++) dis[i]=-INF;
	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[to]<dis[now]+pay[i]){ //最长路更新 
				dis[to]=dis[now]+pay[i];
				Min[to]=min(Min[now],w[i]);
				pre[to]=i;
				if(!vis[to]) q.push(to),vis[to]=true;
			}
		}
	}
	return dis[t]!=-INF;
}
inline void EK()
{
	while(SPFA()){
		maxf+=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 DFS(int x,int y,int u,int k)
{
	int kx,ky,mov;
	for(register int i=first[u];i!=-1;i=nex[i]){
		int to=v[i];
		if(to==s||to==t||to==u-p*q||!w[i^1]) continue;
		w[i^1]--;
		if(to>p*q) { DFS(x,y,to,k); return; } //如果到达i'即连接其他点的拆点 
		if(to==Get_id(x,y)+1) kx=x,ky=y+1,mov=1; //判断走向了哪一个点 
		else kx=x+1,ky=y,mov=0;
		printf("%d %d\n",k,mov);
		DFS(kx,ky,to+p*q,k); //递归 
		return;
	}
}
int main()
{
	memset(first,-1,sizeof(first));
	n=read(),q=read(),p=read();
	for(register int i=1;i<=p;i++)
		for(register int j=1;j<=q;j++)  mp[i][j]=read();
	s=0,t=2*p*q+1;
	//建图(总结点数位2*p*q,源点位0,汇点为2*p*q+1)
	//将所有点全部拆开
	//拆开点之间的连边:0->0,流量INF,费用0,1->1流量1,费用1 
	for(register int i=1;i<=p;i++){
		for(register int j=1;j<=q;j++){
			if(mp[i][j]==1) continue; //任何点都不与障碍物连边
			int u=Get_id(i,j),v; //u表示现在的节点编号,v表示目标节点编号 
			//设节点i的拆点为i' 
			//向外连边:u'->v
			for(register int k=0;k<=1;k++){
				int nx=i+dx[k],ny=j+dy[k];
				if(nx>p||ny>q) continue; //不超出边界
				if(mp[nx][ny]==1) continue; //障碍物 
				v=Get_id(nx,ny);
				Add(u+p*q,v,INF,0),Add(v,u+p*q,0,0);
			}
			//自己连接自己:u->u' 
			//对于岩石标本,建立两组边,第一组对答案产生贡献的限流1,表示他只能被采集一次
			//第二组表示岩石标本可以和普通地面一样,可以在任意时刻抵达 
			if(mp[i][j]==2) Add(u,u+p*q,1,1),Add(u+p*q,u,0,-1); //当前节点为岩石标本 
			//初开障碍物外任何点可以任意到达 
			Add(u,u+p*q,INF,0),Add(u+p*q,u,0,0);
		}
	}
	if(mp[1][1]!=1) Add(s,1,n,0),Add(1,s,0,0);
	if(mp[p][q]!=1) Add(Get_id(p,q)+p*q,t,n,0),Add(t,Get_id(p,q)+p*q,0,0);
	EK();
	for(register int i=1;i<=maxf;i++) DFS(1,1,1,i);
	return 0;
}
posted @ 2021-02-18 16:32  ╰⋛⋋⊱๑落叶๑⊰⋌⋚╯  阅读(100)  评论(0编辑  收藏  举报