[网络流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;
}