【题解】P3356 火星探险问题

\(\mathfrak{1st.\ Preamble|}\) 前言

题目传送门:洛谷 P3356 火星探险问题

这篇题解已在洛谷上通过了。个人认为这篇题解讲的还是比较详细的。

\(\mathfrak{2nd.\ Solution|}\) 题解

看到题目的第一眼,我的反应是这样的:这不跟深海机器人问题差不多吗?Ctrl-C Ctrl-V 秒了。

不过我还是讲讲怎么建模吧。

这里我定义点 \((x,y)\) 表示第 \(x\) 行第 \(y\) 列的数,和题目中的是反过来的 (题目里的那种表示方法有点反人类)

建模

把一个点 \((x,y)\) 拆成 \((x,y)_1\)\((x,y)_2\) 两个点,\(x_1\) 为入点,\(x_2\) 为出点。

平坦无障碍(\(0\)

  1. 建边 \((x,y)_1 \rightarrow (x,y)_2\),容量为 \(\inf\),费用为 \(0\),代表这个点可以走无数次,没有收益。
  2. 建边 \((x,y)_2 \rightarrow (x+1,y)_1\),容量为 \(\inf\),费用为 \(0\),代表这个从这个点向南到下面那个点可以走无数次。
  3. 建边 \((x,y)_2 \rightarrow (x,y+1)_1\),容量为 \(\inf\),费用为 \(0\),代表这个从这个点向东到右边那个点可以走无数次。

障碍(\(1\)

没法走,不用管它。

石块(\(2\)

和平坦无障碍的相比,只需要再多加一条边 \((x,y)_1 \rightarrow (x,y)_2\),容量为 \(1\),费用为 \(1\),代表只可以捡一次石块,收益为 \(1\)

最后跑最大费用最大流即可。

输出

我建完模就直接输出费用,然后开开心心地跑去测样例,丫的,居然要输出路径!我当场懵逼,冥(cha)思(kan)苦(ti)想(jie)后得到了下面的方法:

对于每条路径,从起点开始搜索,每搜到一个点,选一条反向边有剩余容量(说明被走过)的临边走过去,并把反向边的剩余容量减去 \(1\),直到走到终点。

如果看不懂可以看代码:

void put_ans(int a){
    int x=1,y=1;    // 从起点开始搜索
    while(1){
        if(x>=q&&y>=p)return;  // 到达终点
        int u=B(x,y);
        for(int i=head[u];i;i=e[i].pre){
            int v=e[i].to;
            if(!e[i^1].w)continue;  // 反向边有剩余容量
            if(v==A(x+1,y)){x++,e[i^1].w--,printf("%d 0\n",a);break;}  // 向南走
            if(v==A(x,y+1)){y++,e[i^1].w--,printf("%d 1\n",a);break;}  // 向东走
        }
    }
}

\(\mathfrak{3rd.\ Code|}\) 代码

我是把边权设为负数跑最小费用最大流的方式求解最大费用最大流。代码压行请见谅。

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll inf=1e18,N=5e3+10,M=5e4+10;
inline int read(){
	int x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
ll e_cnt=1,head[N];
struct EDGE{ll from,to,w,c,pre;}e[M<<1];
ll n,p,q,s,t,ans,cost;
ll dis[N],pre[N];
inline void add(ll from,ll to,ll w,ll c){
    e[++e_cnt]=(EDGE){from,to,w,c,head[from]};
    head[from]=e_cnt;
}
inline void add_edge(ll u,ll v,ll w,ll c){
    add(u,v,w,c),add(v,u,0,-c);
}
inline int A(int x,int y){return (x-1)*p+y;}    // (x,y)_1
inline int B(int x,int y){return A(x,y)+p*q;}   // (x,y)_2
//------------------最大费用最大流板子---------------------
bool spfa(ll s,ll t){
    bool inq[N];
    memset(pre,-1,sizeof(pre));
    for(int i=1;i<=t;i++)dis[i]=inf,inq[i]=0;
    queue<ll>que;
    dis[s]=0,inq[s]=1,que.push(s);
    while(!que.empty()){
        ll u=que.front();
        que.pop();
        inq[u]=0;
        for(int i=head[u];i;i=e[i].pre){
            if(e[i].w>0){
                ll v=e[i].to,c=e[i].c;
                if(dis[u]+c<dis[v]){
                    dis[v]=dis[u]+c;
                    pre[v]=i;
                    if(!inq[v]){
                        que.push(v);
                        inq[v]=1;
                    }
                }
            }
        }
    }
    return dis[t]!=inf;
}
void mincost(ll s,ll t){
    while(spfa(s,t)){
        ll v=t,flow=inf;
        while(pre[v]!=-1){
            ll i=pre[v],u=e[i].from;
            flow=min(flow,e[i].w);
            v=u;
        }
        v=t;
        while(pre[v]!=-1){
            ll i=pre[v],u=e[i].from;
            e[i].w-=flow;
            e[i^1].w+=flow;
            v=u;
        }
        cost+=dis[t]*flow;
        ans+=flow;
    }
}
//------------end of 最大费用最大流-------------
void put_ans(int a){
    int x=1,y=1;    // 从起点开始搜索
    while(1){
        if(x>=q&&y>=p)return;  // 到达终点
        int u=B(x,y);
        for(int i=head[u];i;i=e[i].pre){
            int v=e[i].to;
            if(!e[i^1].w)continue;  // 反向边有剩余容量
            if(v==A(x+1,y)){x++,e[i^1].w--,printf("%d 0\n",a);break;}  // 向南走
            if(v==A(x,y+1)){y++,e[i^1].w--,printf("%d 1\n",a);break;}  // 向东走
        }
    }
}
int main(){
    n=read(),p=read(),q=read();
    s=p*q*2+1,t=s+1;
    for(int i=1;i<=q;i++){
        for(int j=1;j<=p;j++){
            int x;
            x=read();
            if(x!=1){    // 0和2有三条相同边,合在一起,1啥也不干
                add_edge(A(i,j),B(i,j),inf,0);
                if(i+1<=q)add_edge(B(i,j),A(i+1,j),inf,0);
                if(j+1<=p)add_edge(B(i,j),A(i,j+1),inf,0);
                if(x==2)add_edge(A(i,j),B(i,j),1,-1);  // 2多建一条边
            }
        }
    }
    add_edge(s,A(1,1),n,0);
    add_edge(B(q,p),t,n,0);
    mincost(s,t);
    for(int i=1;i<=ans;i++)put_ans(i);
    return 0;
}

AC 记录

\(\mathfrak{4th.\ Postscript|}\) 后记

第一遍编号标错居然可以拿 \(84\) 分(只有 #5 没过),这数据着实有点水……

posted @ 2024-08-12 13:44  青铜_WU  阅读(28)  评论(0编辑  收藏  举报