网络流做题记录

P2764 最小路径覆盖问题

解法

直接从某个/某些点出发寻找路径不太现实,考虑从路径覆盖的另一个特点出发:路径中间的点一定入度和出度都为 1,而起点的入度为 0,终点的出度为 0

此时可以考虑如何使得入度/出度为 0 的点最少。考虑某条有向边 uv 出现在路径中可以使 u 的出度和 v 的入度增加 1,而每个点的入度和出度均最多为 1。考虑建一张二分图,把代表第 i 个点的入度的点定为 ai,代表第 i 个点的出度的点定为 bi。对于每条边 uv,将 buav 连一条边。此时某组路径覆盖方案即对应了二分图的一组匹配(匹配中选中的边即对应了路径中的边),然后某条路径的起点 s 和终点 t 对应了 asbt 不在匹配中(即两个非匹配点)。求最小路径覆盖即为求一组匹配满足非匹配点最少,也就是最大匹配。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=310;
const int maxm=6400;
const int INF=1145141919;
int n,m,i,j,u,v,w,l,r,tot=1;
int h[maxn],c[maxn],d[maxn],q[maxn];
int nxt[maxn];
bool pre[maxn];
struct edge{int to,nxt,fl;}E[maxm<<1];
int dfs(const int &p,int f){
if(p==w) return f;
int to,fl=0,we;
for(int &lp=c[p];lp;lp=E[lp].nxt){
if(!E[lp].fl) continue;
to=E[lp].to;
if(d[to]!=d[p]+1) continue;
we=dfs(to,1);
if(!we) continue;
f-=we;fl+=we;
E[lp].fl=0;
E[lp^1].fl=1;
if(!f) break;
}
if(!fl) d[p]=0;
return fl;
}
int main(){
scanf("%d%d",&n,&m);
w=(n<<1)|1;
while(m--){
scanf("%d%d",&u,&v);
E[++tot]={v+n,h[u],1};h[u]=tot;
E[++tot]={u,h[v+n],0};h[v+n]=tot;
}
for(i=1;i<=n;++i){
E[++tot]={i,h[0],1};h[0]=tot;
E[++tot]={0,h[i],0};h[i]=tot;
E[++tot]={w,h[i+n],1};h[i+n]=tot;
E[++tot]={i+n,h[w],0};h[w]=tot;
}
for(;;){
for(i=0;i<=w;++i){
d[i]=0;
c[i]=h[i];
}
l=r=d[0]=1;
while(l<=r){
u=q[l++];
for(i=h[u];i;i=E[i].nxt){
if(!E[i].fl) continue;
v=E[i].to;
if(d[v]) continue;
d[v]=d[u]+1;q[++r]=v;
}
}
if(!d[w]) break;
if(!dfs(0,INF)) break;
}
for(i=1;i<=n;++i){
for(j=h[i];j;j=E[j].nxt){
if(E[j].fl) continue;
if(!(v=E[j].to)) continue;
nxt[i]=v-n;pre[v-n]=1;
}
}
v=0;
for(i=1;i<=n;++i){
if(pre[i]) continue;
printf("%d ",u=i);
while(nxt[u]) printf("%d ",u=nxt[u]);
putchar('\n');++v;
}
printf("%d",v);
return 0;
}

P2762 太空飞行计划问题

解法

考虑建图,将每个实验向对应的仪器连有向边,每个实验和仪器的点权对应为实验/仪器的收益/代价,则问题变成求该图的一个点权和最大的子图,满足子图中点的点权和最大,且子图中每个点的后继都在图内。(称为最大权闭合子图)

考虑割的定义:某个割将图分为了两部分,满足某部分不存在能够到达另一部分的边。此时如果保证原图内的边始终存在,则割中该部分一定可以为一个候选答案。保证子图中的点权和最大,即为删去点权和最小的点满足剩下的点构成的图为候选答案。可以把不选择某个点的代价看成割边的代价,使问题转化为最小割问题。

可以把原图的每条边的容量设为 ,然后从源点向正权点连容量为该点点权的边,从负权点向汇点连容量为该点点权相反数的边。某个最小割一定只能对应删除某些点使得源点不与汇点相通的方案。此时对于某个正权点,不选其的代价为该点点权;而对于某个负权点,选择其则需要把该点和汇点相连的边割掉,代价为该点点权相反数。答案的点权和即为所有正权之和减最小割。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=110;
const int maxm=2610;
const int INF=2147483647;
int n,m,i,j,u,v,w,l,r,tot=1;
int h[maxn],c[maxn],d[maxn],q[maxn];
char s[10010];ll a;
struct edge{int to,nxt,fl;}E[maxm<<1];
inline void Add(const int x,const int y,const int z){
E[++tot]={y,h[x],z};h[x]=tot;
E[++tot]={x,h[y],0};h[y]=tot;
}
ll dfs(const int &p,int f){
if(p==w) return f;
int to;ll fl=0,nw;
for(int &lp=c[p];lp;lp=E[lp].nxt){
if(!E[lp].fl) continue;
to=E[lp].to;
if(d[to]!=d[p]+1) continue;
nw=dfs(to,min(f,E[lp].fl));
if(!nw) continue;
f-=nw;fl+=nw;
E[lp].fl-=nw;
E[lp^1].fl+=nw;
if(!f) break;
}
if(!fl) d[p]=0;
return fl;
}
int main(){
scanf("%d%d",&n,&m);
w=n+m+1;
for(i=1;i<=n;++i){
scanf("%d",&u);
Add(0,i,u);j=0;a+=u;
cin.getline(s,10000);
while(~sscanf(s+j,"%d",&v)){
Add(i,v+n,INF);
if(!v) v=1;
while(v){v/=10;++j;}
++j;
}
}
for(i=1;i<=m;++i){
scanf("%d",&v);
Add(i+n,w,v);
}
for(;;){
for(i=0;i<=w;++i){
d[i]=0;
c[i]=h[i];
}
l=r=d[0]=1;
while(l<=r){
u=q[l++];
for(i=h[u];i;i=E[i].nxt){
if(!E[i].fl) continue;
v=E[i].to;
if(d[v]) continue;
d[v]=d[u]+1;q[++r]=v;
}
}
if(!d[w]) break;
a-=dfs(0,INF);
}
for(i=1;i<=n;++i) if(d[i]) printf("%d ",i);
putchar('\n');
for(i=1;i<=m;++i) if(d[i+n]) printf("%d ",i);
printf("\n%lld",a);
}

CWOI 24th July 2022 T1 光 Hikari

题意

有一张 n 个点 m 条边的二分图,有一枚棋子。

先后手轮流进行操作,每次操作者需要将棋子沿着二分图上的边移动到相邻的节点上,无法移动者判负。先后手均采取最优策略。求棋子在每个点上对应的获胜者。1n5×104,1m105

解法

听说 P4617 [COCI2017-2018#5] Planinarenje 和这道题很像。

这种题属于一种名为二分图博弈的题型(并且是板子题)。有一个重要的结论:先手必胜当且仅当起点为最大匹配必经点。

证明(转自 知乎):考虑某个最大匹配必经点 u 和对应的某个最大匹配 M。先手可以一直按照 M 中的匹配边走,后手不会走到非匹配点(显然最后先手走完一条匹配边会使得后手无路可走)。如果从 u 走到了某个非匹配点 p,则可以将走过的路径上的匹配边和非匹配边状态反转,得到不包含 u 的最大匹配,与条件矛盾。

如果对于某个起点 v,存在不包含其的最大匹配 M,则先手只能先走到某个匹配点,然后从每个点处后手直接走到其对应匹配点即可。此时先手不能走 M 上的匹配边,并且不能走到非匹配点(否则走的路径会是匹配 M 对应的增广路,与最大匹配性质矛盾),后手总会走到使得先手无路可走的点。

至于求每个点是否是二分图最大匹配必经点,根据上述证明内容中(如果从 u 走到了某个非匹配点 p,则可以将走过的路径上的匹配边和非匹配边状态反转,得到不包含 u 的最大匹配)可得:从每个非匹配点出发,按照 非匹配边 -> 匹配边 -> 非匹配边 -> 匹配边 -> …… 的方式走,每走过一条匹配边后到达的节点即为非必经点。这样则其余点均为必经点。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=50010;
const int maxm=300010;
int n,m,i,j,l,r,u,v,t,tot=1;
int h[maxn],c[maxn],d[maxn],q[maxn];
int tv[maxn],col[maxn];
struct edge{int to,nxt,fl;}E[maxm];
bool fg,mat[maxn];
inline void add(int x,int y){
E[++tot]={y,h[x],1}; h[x]=tot;
E[++tot]={x,h[y],0}; h[y]=tot;
}
void color(int p){
if(col[p]) add(p,t);
else add(0,p);
tv[p]=tot^1;
int lp,to;
for(lp=h[p];lp;lp=E[lp].nxt){
to=E[lp].to;
if(col[to]>1) continue;
if(col[to]<0){
col[to]=!col[p];
color(to);
}
if(col[p]){
E[lp].fl=0;
E[lp^1].fl=1;
}
}
}
bool dfs(int p,int f){
if(p==t) return 1;
int to,nw,fl=0;
for(int &lp=c[p];lp;lp=E[lp].nxt){
if(!E[lp].fl) continue;
to=E[lp].to;
if(d[to]!=d[p]+1) continue;
if(!(nw=dfs(to,1))) continue;
--f; ++fl; E[lp].fl=0; E[lp^1].fl=1;
if(!f) break;
}
if(!fl) return d[p]=0;
return 1;
}
int main(){
freopen("hikari.in","r",stdin);
freopen("hikari.out","w",stdout);
scanf("%d%d",&n,&m);
memset(col,-1,sizeof(col));
col[t=n+1]=col[0]=2;
for(i=1;i<=m;++i){
scanf("%d%d",&u,&v);
add(u,v);
}
for(i=1;i<=n;++i){
if(~col[i]) continue;
col[i]=0; color(i);
}
for(;;){
for(i=0;i<=t;++i){
d[i]=0;
c[i]=h[i];
}
d[0]=1; l=r=0;
while(l<=r){
u=q[l++];
for(i=h[u];i;i=E[i].nxt){
if(!E[i].fl) continue;
v=E[i].to; if(d[v]) continue;
d[v]=d[u]+1; q[++r]=v;
}
}
if(!d[t]) break;
dfs(0,1145141919);
}
for(i=1;i<=t;++i) d[i]=0;
l=r=0; d[0]=1;
while(l<=r){
u=q[l++];
for(i=h[u];i;i=E[i].nxt){
if(!E[i].fl) continue;
v=E[i].to; if(d[v]) continue;
d[v]=1; q[++r]=v;
if(!col[v]) mat[v]=1;
}
}
for(i=0;i<t;++i) d[i]=0;
l=r=0; d[t]=1; q[0]=t;
while(l<=r){
u=q[l++];
for(i=h[u];i;i=E[i].nxt){
if(E[i].fl) continue;
v=E[i].to; if(d[v]) continue;
d[v]=1; q[++r]=v;
if(col[v]) mat[v]=1;
}
}
for(i=1;i<=n;++i){
if(mat[i]) printf("Tairitsu\n");
else printf("Hikari\n");
}
return 0;
}

CF1721F Matching Reduction

考虑二分图中最大匹配 = 最小点覆盖大小的性质。显然删去一个点之后最大匹配大小最多会减少 1(如果其为最大匹配必经点),而删去最小点覆盖之后整个图只会剩下最大独立集,此时最大匹配大小为 0;所以每次删去最小点覆盖内的一个点一定满足题意。

此时我们需求最小点覆盖和最大独立集之间的最大匹配。然而求得的最大匹配一定就是最小点覆盖和最大独立集之间的最大匹配,因为匹配中每条边至少有一个端点在最小点覆盖内,而最小点覆盖大小即为最大匹配大小;所以直接依次删去原最大匹配的边即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=400010;
const int maxm=1200010;
const int INF=1145141919;
int m,Q,i,j,u,v,t,n1,n2,te=1;
bool fl[maxm],vis[maxn];
int to[maxm],mt[maxn],nxt[maxm];
int h[maxn],d[maxn],c[maxn],me[maxn];
long long a; queue<int> q;
inline void Add(int x,int y){
to[++te]=y; fl[te]=1; nxt[te]=h[x]; h[x]=te;
to[++te]=x; nxt[te]=h[y]; h[y]=te;
}
int dinic(int p,int f){
if(p==t) return 1;
int to,fl=0;
for(int &lp=c[p];lp;lp=nxt[lp]){
if(!::fl[lp]) continue;
to=::to[lp];
if(d[to]!=d[p]+1) continue;
if(!dinic(to,1)) continue;
::fl[lp]=0; ::fl[lp^1]=1;
--f; ++fl; if(!f) break;
}
if(!fl) d[p]=0; return fl;
}
void dfs(int p){
vis[p]=1; int lp,to;
for(lp=h[p];lp;lp=nxt[lp])
if(fl[lp]&&!vis[to=::to[lp]])
dfs(to);
}
int main(){
scanf("%d%d%d%d",&n1,&n2,&m,&Q); t=n1+n2+1;
while(m--){
scanf("%d%d",&u,&v);
Add(u,v+n1);
}
for(i=1;i<=n1;++i) Add(0,i);
for(i=1;i<=n2;++i) Add(n1+i,t);
for(d[0]=1,m=0;;){
memset(d+1,0,t<<2);
memcpy(c,h,(t+1)<<2);
for(q.push(0);!q.empty();){
u=q.front(); q.pop();
for(i=h[u];i;i=nxt[i]){
if(!fl[i]) continue;
v=to[i]; if(d[v]) continue;
d[v]=d[u]+1; q.push(v);
}
}
if(!d[t]) break; m+=dinic(0,INF);
}
vis[t]=1; dfs(0);
for(i=1;i<=n1;++i){
if(!vis[i]){
for(j=h[i];!to[j]||fl[j];j=nxt[j]);
mt[i]=to[j]; me[i]=(j>>=1); a+=j;
}
}
for(i=1;i<=n2;++i){
if(vis[u=n1+i]){
for(j=h[u];to[j]==t||!fl[j];j=nxt[j]);
mt[u]=to[j]; me[u]=(j>>=1); a+=j;
}
}
for(v=0,j=1;Q--;){
scanf("%d",&u);
if(u==1){
printf("1\n");
while(!mt[v]) ++v; a-=me[v];
if(j=v,j>n1) j=n1-j; --m;
printf("%d\n%lld",j,a); ++v;
}
else{
printf("%d\n",m);
for(i=v;i<t;++i) if(me[i]) printf("%d ",me[i]);
}
putchar('\n'); fflush(stdout);
}
return 0;
}

P1251 餐巾计划问题

解法

网络流 24 题中一道最有思维的题。

思路中最难之处在于 每天强制要求至少 ri 条餐巾 看成最大流模型中的 每天对应的节点强制向汇点流满 ri至少 的限制可以转化为从某个点向汇点流满)。然后其他的问题结合题意不难处理:

把每天的 脏餐巾和干净餐巾分为两个点 处理,设代表第 i 天的脏餐巾的点为 di,干净餐巾的点为 ci

  1. 源点需要向每个 ci 连容量为 +,费用为 p 的边(每天都可以购买任意数量的餐巾)。
  2. 源点需要向每个 di 连容量为 ri,费用为 0 的边(第 i 天一定会有 ri 条脏餐巾,最多多把 ri 条脏餐巾拿去洗)。
  3. 每个 dici+m 连容量为 +,费用为 f 的边(把脏餐巾拿去快洗)。
  4. 每个 dici+n 连容量为 +,费用为 s 的边(把脏餐巾拿去慢洗)。
  5. 每个 didi+1 连容量为 +,费用为 0 的边(把脏餐巾留着)。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=4010;
const int maxm=14010;
const int INF=1000000000;
const ll LINF=11451419198101926;
int n,m,i,u,v,w,t,l,r,py,kt,kc,mt,mc,tot=1;
int h[maxn],c[maxn],q[maxn*maxm];
ll d[maxn],a;
bool vis[maxn];
struct edge{int to,nxt,fl,we;}E[maxm<<1];
void Add(const int x,const int y,const int z,const int p){
E[++tot]={y,h[x],z,p};h[x]=tot;
E[++tot]={x,h[y],0,-p};h[y]=tot;
}
int dfs(const int &p,int f){
if(p==w) return f;
int to,fl=0,nw;
vis[p]=1;
for(int &lp=c[p];lp;lp=E[lp].nxt){
if(!E[lp].fl) continue;
to=E[lp].to;
if(d[to]!=d[p]+E[lp].we) continue;
if(vis[to]) continue;
nw=dfs(to,min(E[lp].fl,f));
if(!nw) continue;
f-=nw;fl+=nw;
E[lp].fl-=nw;
E[lp^1].fl+=nw;
a+=1LL*E[lp].we*nw;
if(!f) break;
}
if(!fl) d[p]=LINF;
vis[p]=0;return fl;
}
int main(){
scanf("%d",&n);
w=n<<1|1;
for(i=1;i<n;++i){
scanf("%d",&t);
Add(0,i+n,t,0);
Add(i,w,t,0);
Add(i,i+1,INF,0);
Add(i+n,i+n+1,INF,0);
}
scanf("%d",&t);
Add(0,n<<1,t,0);
Add(n,w,t,0);
scanf("%d%d%d%d%d",&py,&kt,&kc,&mt,&mc);
for(i=1;i<=n;++i){
Add(0,i,INF,py);
if(i+kt<=n){
Add(i+n,i+kt,INF,kc);
if(i+mt<=n) Add(i+n,i+mt,INF,mc);
}
}
for(;;){
for(i=0;i<=w;++i){
d[i]=LINF;
c[i]=h[i];
}
l=r=1;d[0]=0;
while(l<=r){
u=q[l++];
vis[u]=0;
for(i=h[u];i;i=E[i].nxt){
if(!E[i].fl) continue;
v=E[i].to;
if(d[v]>d[u]+E[i].we){
d[v]=d[u]+E[i].we;
if(!vis[v]){
vis[v]=1;
q[++r]=v;
}
}
}
}
if(d[w]==LINF) break;
dfs(0,INF);
}
printf("%lld",a);
}

SP839 OPTM - Optimal Marks

解法

当成复习最小割构造题。

首先计算 (u,v)(markumarkv) 时显然 mark 的每一位互不影响,直接把拆成 O(log) 个二进制位计算答案,而每一位对答案的贡献显然是割的定义。此时如果某个确定的 mark 的这一位为 1 则从源点向该点连边,这一位为 0 则从该点向汇点连边。

注意:连边时方向不能错掉:在最后求对应最小割方案时需要从源点出发走残量大于 0 的边找到这一位为 1 的所有点,感性理解就是使得源点连向的点数量最小,确保得到的方案为 imarki 最小的方案。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=510;
const int maxm=6510;
const int INF=1145141919;
int t,n,m,u,v,i,j,l,r,T,td,te;
int w[maxn],p[maxn],ph[maxn];
int h[maxn],d[maxn],c[maxn],q[maxn];
struct edge{int to,nxt,fl;}E[maxm<<1];
inline void Add(int x,int y,int w){
E[++te]={y,h[x],w}; h[x]=te;
E[++te]={x,h[y],0}; h[y]=te;
}
int dinic(int p,int f){
if(p==t) return f;
int to,fl=0,nw;
for(int &lp=c[p];lp;lp=E[lp].nxt){
if(!E[lp].fl) continue;
to=E[lp].to;
if(d[to]!=d[p]+1) continue;
nw=dinic(to,min(f,E[lp].fl));
if(!nw) continue; f-=nw; fl+=nw;
E[lp].fl-=nw; E[lp^1].fl+=nw;
if(!f) break;
}
if(!fl) d[p]=0;
return fl;
}
int main(){
scanf("%d",&T); d[0]=1;
while(T--){
scanf("%d%d",&n,&m);
te=1; t=n+1;
memset(h,0,(n+2)<<2);
memset(w+1,-1,n<<2);
memset(p+1,0,n<<2);
while(m--){
scanf("%d%d",&u,&v);
Add(u,v,1); Add(v,u,1);
}
memcpy(ph,h,(t+1)<<2);
td=te; scanf("%d",&m);
while(m--){
scanf("%d%d",&u,&v);
w[u]=v;
}
for(j=(1<<30);j;j>>=1){
for(i=1;i<=n;++i){
u=w[i]; if(u==-1) continue;
if(u&j) Add(0,i,INF);
else Add(i,t,INF);
}
for(;;){
memset(d+1,0,t<<2);
memcpy(c,h,(t+1)<<2);
l=r=0;
while(l<=r){
u=q[l++];
for(i=h[u];i;i=E[i].nxt){
if(!E[i].fl) continue;
v=E[i].to;
if(d[v]) continue;
d[v]=d[u]+1; q[++r]=v;
}
}
if(!d[t]) break;
dinic(0,INF);
}
memset(d,0,t<<2);
l=r=0; d[0]=1;
while(l<=r){
u=q[l++];
for(i=h[u];i;i=E[i].nxt){
if(!E[i].fl) continue;
v=E[i].to;
if(d[v]) continue;
p[v]|=j; d[v]=1;
q[++r]=v;
}
}
memcpy(h,ph,(n+2)<<2); te=td;
}
for(i=1;i<=n;++i){
if(~w[i]) printf("%d\n",w[i]);
else printf("%d\n",p[i]);
}
}
return 0;
}

P3227 [HNOI2013]切糕

解法

如果不考虑 D 值,则问题相当于在某个 (x,y) 的合法 z 值区间内找出一个最小的 f(x,y,z)。可以使用最小割的思路解决:将每个三维坐标 (x,y,z) 表示成一个点,每个 (x,y,z) 连一条到 (x,y,z+1) 的容量为 v(x,y,z) 的边,源点向 (x,y,1) 连一条容量为 的边,(x,y,R) 向汇点连一条容量为 v(x,y,R) 的边。最小割一定会是 (x,y,1) 到汇点中的边的最小容量即 minzv(x,y,z)

考虑如何使得 x,y,x,y,(|xx|+|yy|=1)(x,y)(x,y) 选择的 z 值之差不能大于 D。此时可以 z,从 (x,y,z)(x,y,zD) 连一条容量为 的边。此时 (x,y,z+1+D) 也会向 (x,y,z+1) 连边,所以 (x,y,z)(x,y,z+1) 的边被割掉后一定需要 (x,y,zD)(x,y,z+1+D) 之间有一条边被割掉,否则源汇点不会不连通。

最小割中每个 (x,y) 只有一条边被割。证明:显然某条割边 uv 必须满足残量网络上源点能到达 uv 能到达汇点;否则留下这条边后源汇点仍然不连通,形成更小的割。然后考虑一组包含了不只一条割边的 (x,y),设其的第二条割边的起点为 s,则一定存在某个 (xa,ya,za) 满足源点到 (xa,ya,1) 的连边和 z<za,(xa,ya,z)(xa,ya,z+1) 的连边未断,(xa,ya,za) 连向某个 (x,y,zaD),且满足 (x,y) 满足 (x,y,1)(x,y,zaD) 之间有割边。显然存在某个 z 满足可以从 (xa,ya,z) 到达这条割边的终点,从而源点可以通过 (xa,ya,z) 到汇点。

(听说这个模型名叫“切糕模型”/“离散变量模型”)

所以 G=(V,E)|V||E|105 级别时 Dinic 是怎么跑过去的(或者 Dinic 在某些特殊性质的图上有某个平均时间复杂度?)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxl=44;
const int maxn=64010;
const int maxm=320010;
const int INF=1145141919;
const int dx[4]={-1,0,1,0};
const int dy[4]={0,-1,0,1};
int n,m,l,r,i,j,k,p,u,v,t,s,a,te=1;
int ve[maxl][maxl][maxl];
int h[maxn],d[maxn],c[maxn],q[maxn];
struct edge{int to,fl,nxt;}E[maxm<<1];
inline void Add(int x,int y,int w){
E[++te]={y,w,h[x]}; h[x]=te;
E[++te]={x,0,h[y]}; h[y]=te;
}
int dinic(int p,int f){
if(p==t) return f;
int to,fl=0,nw;
for(int &lp=c[p];lp;lp=E[lp].nxt){
if(!E[lp].fl) continue;
to=E[lp].to;
if(d[to]!=d[p]+1) continue;
nw=dinic(to,min(f,E[lp].fl));
if(!nw) continue; f-=nw; fl+=nw;
E[lp].fl-=nw; E[lp^1].fl+=nw;
if(!f) break;
}
if(!fl) d[p]=0;
return fl;
}
int main(){
scanf("%d%d%d%d",&n,&m,&r,&s);
for(i=1;i<=n;++i){
for(j=1;j<=m;++j){
Add(0,t+1,INF);
for(k=1;k<=r;++k) ve[i][j][k]=++t;
}
}
++t;
for(k=s+1;k<=r;++k){
for(i=1;i<=n;++i){
for(j=1;j<=m;++j){
for(p=0;p<4;++p){
u=ve[i][j][k-s];
v=ve[i+=dx[p]][j+=dy[p]][k];
if(i&&j&&i<=n&&j<=m) Add(v,u,INF);
i-=dx[p]; j-=dy[p];
}
}
}
}
for(k=1;k<r;++k){
for(i=1;i<=n;++i){
for(j=1;j<=m;++j){
scanf("%d",&u);
Add(ve[i][j][k],ve[i][j][k+1],u);
}
}
}
for(i=1;i<=n;++i){
for(j=1;j<=m;++j){
scanf("%d",&u);
Add(ve[i][j][r],t,u);
}
}
for(;;){
memset(d,0,(t+1)<<2);
memcpy(c,h,(t+1)<<2);
d[0]=1; l=r=0;
while(l<=r){
u=q[l++];
for(i=h[u];i;i=E[i].nxt){
if(!E[i].fl) continue;
v=E[i].to;
if(d[v]) continue;
d[v]=d[u]+1; q[++r]=v;
}
}
if(!d[t]) break;
a+=dinic(0,INF);
}
printf("%d",a);
return 0;
}

P4177 [CEOI2008] order

解法

发现这个题中如果每个 b 均为 ,则问题变成了 太空飞行计划问题。而问题中 b 有限,可以对应把最大权闭合子图模型中原图的边的容量变为对应 b 值,表示可以删除原图的某条边(租用某台机器),对应代价为 b。其他内容不变。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=2450;
const int maxm=1442500;
const int INF=1145141919;
int n,m,i,u,v,t,l,r,a,te=1;
int h[maxn],d[maxn],c[maxn],q[maxn];
struct edge{int to,nxt,fl;}E[maxm<<1];
inline void Add(int x,int y,int f){
E[++te]={y,h[x],f}; h[x]=te;
E[++te]={x,h[y],0}; h[y]=te;
}
int dinic(int p,int f){
if(p==t) return f;
int to,fl=0,nw;
for(int &lp=c[p];lp;lp=E[lp].nxt){
if(!E[lp].fl) continue;
to=E[lp].to;
if(d[to]!=d[p]+1) continue;
nw=dinic(to,min(E[lp].fl,f));
if(!nw) continue; f-=nw; fl+=nw;
E[lp].fl-=nw; E[lp^1].fl+=nw;
if(!f) break;
}
if(!fl) d[p]=0;
return fl;
}
int main(){
scanf("%d%d",&n,&m);
for(i=1;i<=n;++i){
scanf("%d%d",&v,&t);
Add(0,i,v); a+=v;
while(t--){
scanf("%d%d",&u,&v);
Add(i,n+u,v);
}
}
t=n+m+1;
for(i=1;i<=m;++i){
scanf("%d",&v);
Add(n+i,t,v);
}
for(;;){
memset(d,0,(t+1)<<2);
memcpy(c,h,(t+1)<<2);
l=r=0; d[0]=1;
while(l<=r){
u=q[l++];
for(i=h[u];i;i=E[i].nxt){
if(!E[i].fl) continue;
v=E[i].to;
if(d[v]) continue;
d[v]=d[u]+1; q[++r]=v;
}
}
if(!d[t]) break;
a-=dinic(0,INF);
}
printf("%d",a);
return 0;
}

SP4063 MPIGS - Sell Pigs & P4638 [SHOI2011]银行家

解法

考虑每名客户来的时候对应保险箱的金币进行调整的过程。可以把每个保险箱和每名客户看成点,设第 i 个保险箱对应的节点为 si,第 j 名客户对应的节点为 tj,则在某名拥有第 i 个保险箱的钥匙的客户 j 到来时,先新建一个点 p,然后将 sitjtjp 连边权为 + 的边。然后从源点向每个 si 连边权为金币数量的边,从每个 ti 向汇点连边权为客户需求数量的边。理论上点数和边数均为 O(nm) 级别的,但是非常优秀地过掉了两个题。

不过有一个更加优秀的做法:考虑直接将上述新的 si 设为 tj,省去了建新的节点和在 tjsi 之间建边的过程。使用颜色段均摊的知识可证对应点数和边数均为 O(n+m) 的。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxd=2510;
const int maxn=610+maxd;
const int INF=1145141919;
int n,m,i,u,v,t,l,r,a,te=1,s[maxd],w[maxn];
int h[maxn],d[maxn],c[maxn],q[maxn];
struct edge{int to,fl,nxt;}E[maxn<<2];
inline void Add(int x,int y,int f){
E[++te]={y,f,h[x]}; h[x]=te;
E[++te]={x,0,h[y]}; h[y]=te;
}
int dinic(int p,int f){
if(p==t) return f;
int to,fl=0,nw;
for(int &lp=c[p];lp;lp=E[lp].nxt){
if(!E[lp].fl) continue;
to=E[lp].to;
if(d[to]!=d[p]+1) continue;
nw=dinic(to,min(f,E[lp].fl));
if(!nw) continue; f-=nw; fl+=nw;
E[lp].fl-=nw; E[lp^1].fl+=nw;
if(!f) break;
}
if(!fl) d[p]=0;
return fl;
}
int main(){
scanf("%d%d",&m,&n); t=n+m+1;
for(i=1;i<=m;++i){
scanf("%d",&u);
Add(0,i,u); s[i]=i;
}
for(i=1;i<=n;++i){
r=i+m; w[r]=r;
scanf("%d",&u);
while(u--){
scanf("%d",&v);
l=s[v];
if(w[l]!=r){
Add(s[v],r,INF);
w[l]=r;
}
s[v]=r;
}
scanf("%d",&v);
Add(r,t,v);
}
for(;;){
memset(d,0,(t+1)<<2);
memcpy(c,h,(t+1)<<2);
l=r=0;
for(d[0]=1;l<=r;){
u=q[l++];
for(i=h[u];i;i=E[i].nxt){
if(!E[i].fl) continue;
v=E[i].to;
if(d[v]) continue;
d[v]=d[u]+1; q[++r]=v;
}
}
if(!d[t]) break;
a+=dinic(0,INF);
}
printf("%d",a);
return 0;
}

P4249 WC2007 石头剪刀布 & CF1264E Beautiful League

解法

显然这张竞赛图的三元环数量为 (n3)i=1n(di2)(其中 dii 点的入度)。任意三个点之间不能构成三元环当且仅当另外两个点存在连向某个点的边。

di 抽象为 i 点对应的流入的流量,则可以构造一个最小费用最大流模型:

  • 对于定向的边 ij,从源点向 j 连一条容量为 1 费用为 0 的边,表示 j 入度(流入量)加上了 1
  • 对于不定向的边 ij,从源点向 i 连一条容量为 1 费用为 0 的边,同时从 ij 连一条容量为 1 费用为 0 的边,表示 ij 的入度(流入量)必须有且只有一者加上 1
  • 同时对于 i,考虑用某个内容表示 (di2)。可以发现 y=(x2) 是凸函数,并且需要求 mini=1n(di2);同时有 (12)=0,di2,(di2)(di12)=di(di1)2(di1)(di2)2=di1;故而可以从 i 点向汇点连 n1 条边(i 点的入度显然不超过 n1),这些边的容量为 1,费用分别为 0,1,2,,n2。同时跑一遍最小费用最大流;则 i 点流入量一定会从费用分别为 0,1,,di1 的边流至汇点,带来 i=0di1i=(di2) 的费用;并且最小费用即为 mini=1n(di2)。(p.s. 费用为流量的某种凸函数/凹函数且需要求费用的最小/最大值时,可以用类似的方式构造,不过可以把斜率相同的一段看成一条边)

至于构造方案,可以构造一个二维数组 WWi,j=1 代表存在 ij 的边)。对于定向的边 ij,预先把 Wi,j 设为 1;对于不定向的边 ij,考虑其在网络流建出的图中的对应性质。若建出的图中存在 ji(1i,jn) 边未分配流量,则 dj 由于 ij 边而增加了 1;故而对应的有向边为 ij,把 Wi,j 设为 1。时间复杂度为 O(能过)

代码实现上可能有较大出入,但是原理相同。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=110;
const int maxm=40250;
const int INF=1145141919;
int n,i,j,l,r,u,v,t,o,ans,tot=1;
int h[maxn],fl[maxm],cs[maxm],nxt[maxm],ver[maxm];
int d[maxn],c[maxn],q[maxn*maxm];
bool vis[maxn],win[maxn][maxn];
inline void Add(const int &x,const int &y,const int &f,const int &c){
ver[++tot]=y;fl[tot]=f;cs[tot]=c;nxt[tot]=h[x];h[x]=tot;
ver[++tot]=x;fl[tot]=0;cs[tot]=-c;nxt[tot]=h[y];h[y]=tot;
}
int dinic(const int &p,int f){
if(p==t) return f;
int to,fa=0,nw;vis[p]=1;
for(int &lp=c[p];lp;lp=nxt[lp]){
if(!fl[lp]) continue; to=ver[lp];
if(vis[to]||d[to]!=d[p]+cs[lp]) continue;
nw=dinic(to,min(f,fl[lp]));
if(!nw) continue;
f-=nw;fa+=nw;ans+=nw*cs[lp];
fl[lp]-=nw;fl[lp^1]+=nw;
if(!f) break;
}
if(!fa) d[p]=INF;
vis[p]=0;return fa;
}
int main(){
scanf("%d",&n);t=n+1;
for(i=1;i<=n;++i){
for(j=1;j<=n;++j){
scanf("%d",&o);
Add(i,t,1,j-1);
if(i==j||!o) continue;
if(o==1){
Add(0,i,1,0);
win[i][j]=1;
}
else if(i<j){
Add(0,i,1,0);
Add(i,j,1,0);
}
}
}
for(;;){
for(i=0;i<=t;++i){
d[i]=INF;
c[i]=h[i];
}
d[0]=l=r=0;
while(l<=r){
u=q[l++];vis[u]=0;
for(i=h[u];i;i=nxt[i]){
if(!fl[i]) continue;
v=ver[i];
if(d[v]>d[u]+cs[i]){
d[v]=d[u]+cs[i];
if(!vis[v]){
vis[v]=1;
q[++r]=v;
}
}
}
}
if(d[t]==INF) break;
dinic(0,INF);
}
printf("%d\n",n*(n-1)*(n-2)/6-ans);
for(i=1;i<=n;++i){
for(j=h[i];j;j=nxt[j]){
if(!fl[j]) continue;
win[i][ver[j]]=1;
}
for(j=1;j<=n;++j) printf("%d ",win[i][j]);
putchar('\n');
}
return 0;
}

CF724E Goods Transportation

听说 GaryH 秒切这道 *2900 的题

解法

有一个朴素的网络流做法:建立超级源点 ST。对于 i 点,从 Si 连一条边权为 ai 的边,从 iT 连一条边权为 bi 的边。对于 ij,从 min(i,j)max(i,j) 连一条边权为 c 的边。最大流即为答案。然而其时间复杂度高达 O(n4)

考虑最大流转为最小割。(对于长得很别致而点数边数高的图,常常可以考虑模拟网络流。)最小割中 i 一定与 ST 相连。从小到大考虑 i 点,设 dpi,j 为前 i 个点共有 j 个点与 S 相连的答案。转移即有:

dpi,j=min(dpi1,j+ai+jc,dpi1,j1+bi)

时间复杂度:O(n2)。常数小所以随便过,听说有 O(nlogn) 的做法。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
int n,i,j;
long long f[10010],p[10010],s,c,r,t;
int main(){
memset(f,0x3f,sizeof(f));
scanf("%d%lld",&n,&c);
f[0]=0;
for(i=1;i<=n;++i) scanf("%lld",p+i);
for(i=1;i<=n;++i){
scanf("%lld",&s);t=p[i]+(r+=c);
for(j=i;j;--j) f[j]=min(f[j]+t,f[j-1]+s),t-=c;
f[0]+=p[i];
}
printf("%lld",*min_element(f,f+n+1));
return 0;
}

CF1368H1 Breadboard Capacity (easy version)

CF1368H2 Breadboard Capacity (hard version)

听说 tourist 都没有时间做掉 hard version,然后这个题成为了某场考试的 T2,plate_let 做完 hard version 都耗时三个半小时

解法

考虑网络流做法:

对整个网格每对相邻位置建边权为 1 的双向边,将源点 S 向每个红色接口,每个蓝色接口向汇点 T 连边权为 inf 的单向边。最大流即为答案。

这样做的点数和边数高达 O(nm)

考虑 CF724E Goods Transportation 的做法:利用最大流=最小割的性质模拟最小割。但是本题中我们建了大量的双向边,看似不能保证求出的最大流每条边最多只被流经一次(最小割每条边只会被考虑一次),进而建立模拟最小割模型。考虑如下转化:

被经过两次的双向边可以在不改变答案的前提下改为不被经过,从而确保了每对点的边在割掉时贡献只会被计算一次。

考虑割的标准定义:将与 ST 连通的点(包括 ST)划分为两个点集 VSVT,满足 VS={S}{一部分 S 能到达的点}VT={T}{一部分能到达 T 的点},定义这样划分的价值(即割的权值和)为 e=(uv)(uVS,vVT)|e|。所有划分的最小价值即为最小割的权值和。

对此题进行上述划分后,则红色接口只会在 VS 内,蓝色接口只在 VT 内。下面为了方便,会把 VS 内的点表示为红色,VT 内的点表示为蓝色,将割掉边后相连的点集记为连通块。异色连通块的边界总长(不包括网格四周的)即为割边总数。

定理:最优方案中网格内连通块一定与至少一组对边相连且为矩形。

证明:

若某连通块不与周围任意一边相连,则将其变成另一种颜色后割边变少,一定更优。

同时若某连通块只与一条边相连,同样可以将其变为另一种颜色,减少的割边数一定多于增加的,答案也会更优。

同理,这样的只与一组相邻边界的连通块也可以反色,减少的割边数不会少于增加的。

并且,连通块凸出的边界可以反色,凸型的边界也可以推进到对边上(或是进行收缩),可以发现答案始终不会更劣。

由此,我们完成了定理的证明,即每行或每列必须同色。考虑每列同色时,设 dpi,X(X[0,1]) 表示考虑到第 i 行染色为 X (令 0 为红色,1 为蓝色),转移方程如下:

dpi,A=min(dpi1,A,dpi1,!A+n)+[Ui=A]+[Di=A]

注意初值 dp1,A=[U1=A]+[Di=A]+i=0n[Li=A],最后 dpm,Adpm,A+i=0n[Ri=A]

其中 LRUD 分别为左右两列,上下两行的接口颜色序列。

对每行同色时采取类似方法。总复杂度为 O(n+m)

加强版:考虑线段树维护矩阵乘法。上述式子 dpi,A=min(dpi1,A+[Ui=A]+[Di=A],dpi1,!A+n+[Ui=A]+[Di=A]) 可以看作广义矩阵乘法的形式:

[dpi,0dpi,1]×[!Ui+!Din+Ui+Din+!Ui+!DiUi+Di]=[dpi+1,0dpi+1,1]

其中 A×B=C(Ci,j=min(Ai,k+Bk,j))

证明上述乘法满足结合律:

AP×M 的矩阵,BM×Q 的矩阵,CQ×R 的矩阵,则

((A×B)×C)i,j=mink=1Q((A×B)i,k+Ck,j)=mink=1Q(minx=1M(Ai,x+(Bx,k+Ck,j)))=minx=1M(Ai,x+mink=1Q(Bx,k+Ck,j))=minx=1M(Ai,x+(B×C)x,j)=(A×(B×C))i,j

故而在修改时,使用线段树维护所有可能的 4 种矩阵(目前状态下不反转颜色,反转一条边的颜色,反转两条边的颜色)和 i=0nLi 等内容,复杂度为 O((n+q)logn+(m+q)logm),常数有一点大。

代码(似乎有极致压行者代码 2k 左右?)

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
#define ll long long
const ll INF=1145141919810;
int n,m,q,i,lt,rt,th;
char o,s[2][maxn];
ll sr,sb,sR,sB,ans;
struct mat{
ll xx,xy,yx,yy;
inline mat operator *(const mat &p){
return (mat){min(xx+p.xx,xy+p.yx),
min(xx+p.xy,xy+p.yy),
min(yx+p.xx,yy+p.yx),
min(yx+p.xy,yy+p.yy)};
}
}tmp;
struct seg{
int l,r,m,len;
}tr[4][maxn<<2];
#define l(p) Tr[p].l
#define r(p) Tr[p].r
#define m(p) Tr[p].m
#define len(p) Tr[p].len
#define ls(p) p<<1
#define rs(p) p<<1|1
#define sum(p) tr[p].sum
#define tag(p) tr[p].tag
#define rev(p) tr[p].rev
#define tag1(p) tr[p].tag1
#define tag2(p) tr[p].tag2
#define mul(p,c,d) tr[p].mul[c][d]
void Build(const int t,const int p,const int l,const int r){
tr[t][p]={l,r,(l+r)>>1,r-l+1};
if(l==r) return;
Build(t,ls(p),l,(l+r)>>1);
Build(t,rs(p),((l+r)>>1)+1,r);
}
struct Sums{
seg *Tr;
struct segs{
int sum;
bool tag;
}tr[maxn<<2];
inline void Pushup(const int p){
sum(p)=sum(ls(p))+sum(rs(p));
}
void Build(const int p,const bool u){
if(l(p)==r(p)){
sum(p)=s[u][l(p)];
return;
}
Build(ls(p),u);
Build(rs(p),u);
Pushup(p);
}
inline void Pushdown(const int p){
if(!tag(p)) return;
sum(ls(p))=len(ls(p))-sum(ls(p));
sum(rs(p))=len(rs(p))-sum(rs(p));
tag(ls(p))^=1;tag(rs(p))^=1;
tag(p)=0;
}
void Reverse(const int p){
if(lt<=l(p)&&rt>=r(p)){
sum(p)=len(p)-sum(p);
tag(p)^=1;
return;
}
Pushdown(p);
if(lt<=m(p)) Reverse(ls(p));
if(rt>m(p)) Reverse(rs(p));
Pushup(p);
}
int QueryL(){
int p=1;
while(r(p)!=1){
Pushdown(p);
p=ls(p);
}
return sum(p);
}
}S[4];
struct Mats{
seg *Tr;
struct segm{
bool tag1,tag2;
mat mul[2][2];
}tr[maxn<<2];
inline void Pushup(const int &p){
mul(p,0,0)=mul(ls(p),0,0)*mul(rs(p),0,0);
mul(p,0,1)=mul(ls(p),0,1)*mul(rs(p),0,1);
mul(p,1,0)=mul(ls(p),1,0)*mul(rs(p),1,0);
mul(p,1,1)=mul(ls(p),1,1)*mul(rs(p),1,1);
}
void Build(const int p,const int &k){
if(l(p)==r(p)){
const int x=l(p);
int s1=s[0][x]+s[1][x],s2=!s[0][x]+s[1][x];
mul(p,0,0)=(mat){s1,k+2-s1,k+s1,2-s1};
mul(p,0,1)=(mat){s2,k+2-s2,k+s2,2-s2};
mul(p,1,0)=(mat){2-s2,k+s2,k+2-s2,s2};
mul(p,1,1)=(mat){2-s1,k+s1,k+2-s1,s1};
return;
}
Build(ls(p),k);Build(rs(p),k);
Pushup(p);
}
inline void Swap1(const int &p){
swap(mul(p,0,0),mul(p,0,1));
swap(mul(p,1,0),mul(p,1,1));
tag1(p)^=1;
}
inline void Swap2(const int &p){
swap(mul(p,0,0),mul(p,1,0));
swap(mul(p,0,1),mul(p,1,1));
tag2(p)^=1;
}
inline void Pushdown(const int &p){
if(tag1(p)){
Swap1(ls(p));
Swap1(rs(p));
tag1(p)=0;
}
if(tag2(p)){
Swap2(ls(p));
Swap2(rs(p));
tag2(p)=0;
}
}
void Reverse(const int p,const bool x){
if(lt<=l(p)&&rt>=r(p)){
if(x) Swap2(p);
else Swap1(p);
return;
}
Pushdown(p);
if(lt<=m(p)) Reverse(ls(p),x);
if(rt>m(p)) Reverse(rs(p),x);
Pushup(p);
}
}M[2];
inline void Ans(){
th=S[2].QueryL()+S[3].QueryL();
sr=S[0].tr[1].sum;
sb=n-sr+2-th;sr+=th;
tmp=M[0].tr[1].mul[0][0];
sR=min(sr+tmp.xx,sb+tmp.yx);
sB=min(sr+tmp.xy,sb+tmp.yy);
sr=S[1].tr[1].sum;
sR+=sr;sB+=n-sr;
ans=min(sR,sB);
th=S[0].QueryL()+S[1].QueryL();
sr=S[2].tr[1].sum;
sb=m-sr+2-th;sr+=th;
tmp=M[1].tr[1].mul[0][0];
sR=min(sr+tmp.xx,sb+tmp.yx);
sB=min(sr+tmp.xy,sb+tmp.yy);
sr=S[3].tr[1].sum;
sR+=sr;sB+=m-sr;
printf("%lld\n",min(ans,min(sR,sB)));
}
int main(){
scanf("%d%d%d",&n,&m,&q);
Build(0,1,1,n);
Build(1,1,1,m);
if(n>1) Build(2,1,2,n);
if(m>1) Build(3,1,2,m);
S[0].Tr=S[1].Tr=tr[0];
S[2].Tr=S[3].Tr=tr[1];
M[0].Tr=tr[3];M[1].Tr=tr[2];
scanf("%s%s",s[0]+1,s[1]+1);
for(i=1;i<=n;++i){
if(s[0][i]=='B') s[0][i]=1;
else s[0][i]=0;
if(s[1][i]=='B') s[1][i]=1;
else s[1][i]=0;
}
S[0].Build(1,0);
S[1].Build(1,1);
if(n>1) M[1].Build(1,m);
else M[1].tr[1].mul[0][0]={0,INF,INF,0};
scanf("%s%s",s[0]+1,s[1]+1);
for(i=1;i<=m;++i){
if(s[0][i]=='B') s[0][i]=1;
else s[0][i]=0;
if(s[1][i]=='B') s[1][i]=1;
else s[1][i]=0;
}
S[2].Build(1,0);
S[3].Build(1,1);
if(m>1) M[0].Build(1,n);
else M[0].tr[1].mul[0][0]={0,INF,INF,0};
Ans();
while(q--){
scanf(" %c%d%d",&o,&lt,&rt);
if(o=='L'){
S[0].Reverse(1);
if(n>1&&rt>1){
if(lt==1) lt=2;
M[1].Reverse(1,0);
}
}
else if(o=='R'){
S[1].Reverse(1);
if(n>1&&rt>1){
if(lt==1) lt=2;
M[1].Reverse(1,1);
}
}
else if(o=='U'){
S[2].Reverse(1);
if(m>1&&rt>1){
if(lt==1) lt=2;
M[0].Reverse(1,0);
}
}
else{
S[3].Reverse(1);
if(m>1&&rt>1){
if(lt==1) lt=2;
M[0].Reverse(1,1);
}
}
Ans();
}
return 0;
}
posted @   Fran-Cen  阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示