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\) 的数量,所以这个 vector
的 size()
可以直接帮我们判定分类。
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!
标签:15,图究极,idx,ades,Implementation,dg,之环,int,水管 From: https://www.cnblogs.com/BrianPeng/p/17069195.html