Solution of Implementation: P4003 无限之环 费用流建图究极代码简化

Preface

这是一篇费用流的辅助题解。关于建图的思路和构造,题解栏目 中的内容可以说是非常的详尽。

然而,这些题解几乎都对16种可能性一一建图,而且还要对源汇边分别写。这样下来总共32种情况,不仅码量巨大,而且很容易出错。

本文假设读者已经理解建图的规则。本文以 这篇日报 的 Part 3.3 中的建图思路为参考。

有没有什么办法可以让代码更简单呢?


标号法

方向标号

我们直接使用题目的标号:记 上,右,下,左 分别为 \(0,1,2,3\),那么顺时针旋转几个 \(90^\circ\) 就是对应的数字加几然后 \(\bmod 4\)(毒瘤者可考虑 &3)。

对于 \([0,15]\) 中的每一个数字,我们用一个 vector 记录这个数字二进制包含 \(1\) 的位,从 \(0\) 号位开始。

E.g. \(11=(1011)_2=2^0+2^1+2^3\),那么 \(11\) 对应的 vector 就是 \(\{0,1,3\}\)

因为在分类讨论时,主要用的是输入状态的二进制中 \(1\) 的数量,所以这个 vectorsize() 可以直接帮我们判定分类。

vector<int>dg[16];
for(i from 0 to 15)for(j from 0 to 3)
    if(i&(1<<j))dg[i].push_back(j);

点标号

对于每一个格子 \((x,y),x\in[1,n],y\in[1,m]\),我们分出了5个点(上,右,下,左,中)。那么我们可以用一个三元组 \((x,y,k)\) 来表示一个点,其中 \(k\in[0,4]\)。如果 \(k\in[0,3]\),则表示对应的方向标记;如果 \(k=4\),则表示中点。

E.g. 三元组 \((2,3,3)\) 就表示 \((2,3)\) 格子的左点。

接下来,我们可以把三元组 \((x,y,k)\) 用一个整数 \(\text{idx}(x,y,k)=5((x-1)m+y-1)+k\) 标记,这样就把所有的点都唯一地标记为了 \([0,5nm-1]\) 中的一个整数。


\(\star\) 连边法 \(\star\)

基于 \(x+y\) 的奇偶性,我们把 \((x,y)\) 分为源点一边或汇点一边。本文将 \(x+y\) 为奇数的格子分为源点一边。

对于相邻格子点的连边,因为涉及边界判定和坐标,故不做特殊简化。只要用 \(\text{idx}\) 标号函数就可以足够简洁。

为了表示初始状态和旋转,我们会在格子中的5个分点之间互相连接容量为 \(1\) 的费用边。源点一边和汇点一边会以相反的方式连接。

这促使我们把格子内连边的过程打包为一个函数,让函数接收格子位置,连边的分点和费用,然后根据格子的坐标自行判定连边的方向。

以下的写法中,\((x,y)\) 为格子坐标。在源点一边时,\(k\) 为起始方向标记,\(l\) 为到达方向标记。\(c\) 为费用。adde(u,v,w,c) 是标准的费用流连边函数:连接 \(u\to v\),容量为 \(w\),费用为 \(c\),并且加入反向边。

void ades(int x,int y,int k,int l,int c){
    ((x+y)&1)?adde(idx(x,y,k),idx(x,y,l),1,c)
        :adde(idx(x,y,l),idx(x,y,k),1,c);
}

对于以下的连边法,我们只需要考虑 \((x,y)\) 是源点一边的情况。

初始状态

设点 \((x,y)\) 的输入初始状态为 \(a\in[0,15]\)。那么对于 \(a\) 包含的所有方向,也就是 \(a\) 的二进制展开中为 \(1\) 的方向标号,我们从中点向对应方向的点连接容量为 \(1\),费用为 \(0\) 的边。

for(int k:dg[a])ades(i,j,4,k,0);

旋转

Case 0:不用转

这包含没有水管:\(a=0\)

都是水管:\(a=15\)

直线水管:\(a=5,10\)

可以发现这个情况当且仅当 \(a\)\(5\) 的倍数,可以直接 \(\bmod 5\) 判定。

Case 1:一个水管,Q字型

x=dg[a][0] 可以直接取出唯一的有水管的方向。

\((x+k)\bmod 4\) 可以获得顺时针旋转 \(k\)\(90^\circ\) 后的方向标号。如果 \(k=2\),那么费用为 \(2\)。如果 \(k=1,3\),则费用为 \(1\)

所以说旋转 \(k\) 的的费用就是 \(\text{lowbit}(k)=\) k&-k 了!

if(dg[a].size()==1){
    int x(dg[a][0]);
    for(k from 1 to 3)ades(i,j,x,(x+k)&3,k&-k);
}

Case 2:两个水管,L字型

我们已经排除了直线水管,所以两个水管必然是L字型。

每个存在水管的方向 \(k\) 要向对面连费用为 \(1\) 的边,用 \((k+2)\bmod 4\) 就可以得到对面的标号。

for(int k:dg[a])ades(i,j,k,(k+2)&3,1);

Case 3:三个水管,T字型

我们只需要知道没水管的是哪个方向。如果输入的是 \(a\),那么 \(a \text{ xor } 15\) 就可以把没水管的方向标号的二进制位变成 \(1\)。用 x=dg[a^15][0] 直接取出这个方向。

然后用类似 Case 1 的方法连边即可。

int x(dg[a^15][0]);
for(k from 1 to 3)ades(i,j,(x+k)&3,x,k&-k);

总代码(关于建图的部分仅大约30行):

//This program is written by Brian Peng.
#include<bits/stdc++.h>
using namespace std;
#define Rd(a) (a=rd())
#define Gc(a) (a=getchar())
#define Pc(a) putchar(a)
int rd(){
    int x;char c(getchar());bool k;
    while(!isdigit(c)&&c^'-')if(Gc(c)==EOF)exit(0);
    c^'-'?(k=1,x=c&15):k=x=0;
    while(isdigit(Gc(c)))x=x*10+(c&15);
    return k?x:-x;
}
void wr(int a){
    if(a<0)Pc('-'),a=-a;
    if(a<=9)Pc(a|'0');
    else wr(a/10),Pc((a%10)|'0');
}
signed const INF(0x3f3f3f3f),NINF(0xc3c3c3c3);
long long const LINF(0x3f3f3f3f3f3f3f3fLL),LNINF(0xc3c3c3c3c3c3c3c3LL);
#define Ps Pc(' ')
#define Pe Pc('\n')
#define Frn0(i,a,b) for(int i(a);i<(b);++i)
#define Frn1(i,a,b) for(int i(a);i<=(b);++i)
#define Frn_(i,a,b) for(int i(a);i>=(b);--i)
#define Mst(a,b) memset(a,b,sizeof(a))
#define File(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout)
#define N (10010)
int n,m,s,t,cr[N],d[N],a,cst,s1,s0;
bool vs[N];
struct E{int v,w,c,r;};
vector<E>e[N];
vector<int>dg[16];
void adde(int u,int v,int w,int c);
void ades(int x,int y,int k,int l,int c);
bool spfa();
int dfs(int u,int a);
int mcmf();
int idx(int x,int y,int k){return 5*((x-1)*m+y-1)+k;}
signed main(){
    Rd(n),Rd(m),s=idx(n,m,4)+1,t=s+1;
    //预处理二进制位
    Frn0(i,0,16)Frn0(j,0,4)if(i&(1<<j))dg[i].push_back(j);
    Frn1(i,1,n)Frn1(j,1,m){
        Rd(a);
        if((i+j)&1){
            adde(s,idx(i,j,4),dg[a].size(),0),s1+=dg[a].size();
            //格子间的边
            if(i<n)adde(idx(i,j,2),idx(i+1,j,0),1,0);
            if(i>1)adde(idx(i,j,0),idx(i-1,j,2),1,0);
            if(j<m)adde(idx(i,j,1),idx(i,j+1,3),1,0);
            if(j>1)adde(idx(i,j,3),idx(i,j-1,1),1,0);
        }else adde(idx(i,j,4),t,dg[a].size(),0),s0+=dg[a].size();
        //初始状态,0费边
        for(int k:dg[a])ades(i,j,4,k,0);
        //Case 0: 如果是5的倍数,直接跳
        if(a%5){
            if(dg[a].size()==1){
                //Case 1: Q型
                int x(dg[a][0]);
                Frn1(k,1,3)ades(i,j,x,(x+k)&3,k&-k);
            }else if(dg[a].size()==2){
                //Case 2: L型
                for(int k:dg[a])ades(i,j,k,(k+2)&3,1);
            }else{
                //Case 3: T型
                int x(dg[a^15][0]);
                Frn1(k,1,3)ades(i,j,(x+k)&3,x,k&-k);
            }
        }
    }
    if(s0!=s1||mcmf()<s1)printf("-1"),exit(0);
    wr(cst),exit(0);
}
void adde(int u,int v,int w,int c){
    e[u].push_back({v,w,c,(int)(e[v].size())});
    e[v].push_back({u,0,-c,(int)(e[u].size())-1});
}
//格子内分点加边函数
void ades(int x,int y,int k,int l,int c){
    ((x+y)&1)?adde(idx(x,y,k),idx(x,y,l),1,c)
        :adde(idx(x,y,l),idx(x,y,k),1,c);
}
bool spfa(){
    Mst(d,0x3f),d[s]=0,vs[s]=1;
    queue<int>q({s});
    while(!q.empty()){
        int u(q.front());
        q.pop(),vs[u]=0;
        for(E i:e[u])if(i.w&&d[i.v]>d[u]+i.c){
            d[i.v]=d[u]+i.c;
            if(!vs[i.v])q.push(i.v),vs[i.v]=1;
        }
    }
    return d[t]<INF;
}
int dfs(int u,int a){
    #define V e[u][i].v
    #define W e[u][i].w
    #define C e[u][i].c
    if(u==t||!a)return a;
    vs[u]=1;
    int tot(0),tmp;
    for(int&i(cr[u]);i<e[u].size();++i)if(!vs[V]&&d[u]+C==d[V]){
        tmp=dfs(V,min(a,W));
        if(tmp){
            W-=tmp,cst+=tmp*C,e[V][e[u][i].r].w+=tmp;
            tot+=tmp,a-=tmp;
            if(!a)break;
        }
    }
    vs[u]=0;
    return tot;
}
int mcmf(){
    int f(0);
    while(spfa())Mst(cr,0),f+=dfs(s,INF);
    return f;
}

Postscript

能帮助大家减少码量是再好不过了

Thanks for reading!

posted @ 2023-01-27 19:11  BrianPeng  阅读(46)  评论(0编辑  收藏  举报