LsWn的网络流学习笔记
网络流算法
本文所有的序号用红色标了出来
目录
1 网络流模板
1.1 Dinic
1.2 Hungary
1.3 ZKW
1.4 EK
2 最大闭权合子图
2.1 六省联考2017 寿司餐厅
3 拆点
3.1 SCOI2007 蟋蟀
3.2 BOI2008 黑手党
4 网格图&网络流
4.1 [U05 Jan] Muddy Fields G
4.2 [TJOI2016] 游戏
5 流量约束
5.1 二分图匹配
5.2 [网络流24题] 试题库
1.1 Dinic 最大流算法
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+9;
struct edge{int to,nxt,c;}e[N*2]; int hd[N],tot=1;
void add(int u,int v,int c){e[++tot]=(edge){v,hd[u],c};hd[u]=tot;}
int n,m,s,t,ans,tmp; int d[N];
bool bfs(){
queue<int>q; q.push(s); memset(d,0,sizeof(d)); d[s]=1;
while(!q.empty()){
int u=q.front(); q.pop();
for(int i=hd[u],v;i;i=e[i].nxt)
if(!d[v=e[i].to]&&e[i].c){
d[v]=d[u]+1; q.push(v);
if(v==t) return 1;
}
} return 0;
}
int dinic(int u,int flow){
int rest=flow;
if(u==t) return flow;
for(int i=hd[u],v;i&&rest;i=e[i].nxt)
if(e[i].c&&d[v=e[i].to]==d[u]+1){
int used=dinic(v,min(e[i].c,rest));
if(!used) d[v]=0;
rest-=used; e[i].c-=used,e[i^1].c+=used;
}
return flow-rest;
}
int main(){while(bfs()) while(tmp=dinic(s,0x3f3f3f3f)) ans+=tmp;}
1.2 Hungary 二分图匹配算法
#include<bits/stdc++.h>
using namespace std;
#define glx memset
const int N=59*59;
struct edge{int to,nxt;}pr[N*2]; int hd[N],tot;
void add(int u,int v){pr[++tot]=(edge){v,hd[u]};hd[u]=tot;}
bool have[N*N],stay[N];
int T,n,cnt,ans; int c[N]; bool vst[N];
bool ix35txdy(int u){
for(int i=hd[u],v;i;i=pr[i].nxt)
if(!vst[v=pr[i].to])
if(c[v]==!(vst[v]=1)||ix35txdy(c[v]))
return c[v]=u,1;
return 0;
}
int main(){
for(int i=1;i<=n;i++) glx(vst,0,sizeof(vst)),ans+=ix35txdy(i);
}
1.3 ZKW 费用流算法
未学
1.4 EK 带权二分图匹配算法
2 最大闭权合子图
模型
有一些物品,每个物品都有一个收益 \(v_i\),物品有限制关系,即选 \(x\) 必须选 \(y\),(用 \(x\to y\) 表示)最大化代价和。
解决方案
最小割。
每个物品与源点和汇点相连。割掉与源点的边代表不选,割掉与汇点相连的边表示选择这个物品。
对于一个物品的价值 \(v\):
如果 \(v>0\) 则让他和源点相连的边权值为 \(v\),与汇点相连的边权值为 \(0\),将 \(v\) 加入总答案。
如果 \(v<0\) 则让他和源点相连的边权值为 \(0\),与汇点相连的边权值为 \(-v\)。
对于 \(x\to y\) ,转化为 \(x\) 向 \(y\) 连一条权值为 \(\infty\) 的边,显然这样的话可以保证这条边不会被割。
最后求最大流,然后总答案减去最大流即可。
于是可以建立这样一个网络流模型。
从右边的那个blog借图:https://www.cnblogs.com/TreeDream/p/5942354.html
其中一个比较主要的建模元素是 收益不重复,根据此我们可以去转换成最大闭权和子图
2.1 T1 六省联考2017 寿司餐厅
题目链接:https://loj.ac/problem/2146
参考的题解:https://www.cnblogs.com/PinkRabbit/p/10422815.html
题意简化
有 \(n\) 种寿司,第 \(i\) 种寿司的类型为 \(a_i\),如果吃了第 \(i\) 到 \(j\) 种寿司,会得到 \(d_{i,j}\) 的收益。如果吃了 \(c\) (\(c>0\)) 种类型为 \(x\) 的寿司,会付出 \(mx^2+cx\) 的代价。最大化收益与代价的差。
模型转换
\(d_{i,j}\) 转化为一个利益点,那么 \(S\) 连向这个点的边权就是 \(d_{i,j}\) 。
\(x\to y\) 即,由于选择一段区间 \((l,r)\) 还要选择区间 \((l,r-1),(l+1,r)\),于是 \(d_{i,j}\) 指向 \(d_{i,j-1},d_{i+1,j}\)
吃了 \(c\) 个类型为 \(x\) 的寿司,拆分为 \(mx^2\) 和 \(c\times x\),也就是说,吃过是 \(mx^2\),然后再每多吃一种就是增加 \(x\)
同时,我们把每个类型也做为一个物品,代价为 \(mx^2\) 表示一旦取了 \(d_{i,i}\),就要取这个物品。
于是转换为了最大闭权和子图问题,可以用 \(\texttt{dinic}\) 爆切之。
#include<bits/stdc++.h>
#pragma optimize("ofast,unroll-loop")
#define int long long
using namespace std;
const int N=109,inf=0x3f3f3f3f3f3f3f3f;
int n,m,ix35txdy,a[N],cnt=2,id[N][N],dd[N*N],st,ed;
struct edge{int to,nxt,c;}e[N*N*4]; int hd[N*N],tot=1;
void add(int u,int v,int c){e[++tot]=(edge){v,hd[u],c};hd[u]=tot;}
void addh(int u,int v,int c){add(u,v,c),add(v,u,0);}
int d[N*N],ans,tans;
bool bfs(){
queue<int>q; q.push(st); memset(d,0,sizeof(d)); d[st]=1;
while(!q.empty()){
int u=q.front(); q.pop();
for(int i=hd[u],v;i;i=e[i].nxt)
if(!d[v=e[i].to]&&e[i].c){
d[v]=d[u]+1; q.push(v);
if(v==ed) return 1;
}
}
return 0;
}
int dinic(int u,int flow){
int rest=flow; if(u==ed) return flow;
for(int i=hd[u],v;i&&rest;i=e[i].nxt)
if(d[v=e[i].to]==d[u]+1&&e[i].c){
int use=dinic(v,min(rest,e[i].c));
if(!use) d[v]=0;
rest-=use,e[i].c-=use,e[i^1].c+=use;
}
return flow-rest;
}
signed main(){
ios::sync_with_stdio(0);
std::cin>>n>>m;
for(int i=1;i<=n;i++) std::cin>>a[i],ix35txdy=max(ix35txdy,a[i]);
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++)
std::cin>>dd[id[i][j]=++cnt];
st=1,ed=2;
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++){
int tmp=dd[id[i][j]];
if(i==j){
if(m) addh(id[i][j],cnt+a[i],inf); //d_ii连向类型点
tmp-=a[i]; //d_ii的特殊代价
}
else addh(id[i][j],id[i][j-1],inf), addh(id[i][j],id[i+1][j],inf);
if(tmp>0) addh(1,id[i][j],tmp),tans+=tmp;
else addh(id[i][j],2,-tmp); //两种建边情况
}
if(m) for(int i=1;i<=ix35txdy;i++) addh(cnt+i,2,i*i); //类型点的代价
int tmp=0;
while(bfs()) while(tmp=dinic(st,inf)) ans+=tmp;
std::cout<<tans-ans; //减去最小割
return 0;
}
3 拆点
一个有点容量的图很难处理。我们可以考虑把一个点拆成 \(2\) 个点甚至多个点。
最简单的模型就是把一个点拆分成入点和出点,假设为 \(u_0\) 和 \(u_1\),则对于一条边 \(<u,v>\) 我们可以转化为 \(<u_1,v_0, \infty/0>\),对于一个点容量点 \(u\) 容量为 \(c\),我们连边 \(<u_0,u_1,c>\),以解决点容量问题。
3.1 T1 [SCOI2007] 蟋蟀
题目链接:https://www.luogu.com.cn/problem/P2472
题意简化
一个网格图,有 \(n\) 个石柱,每个点只允许蜥蜴踩过 \(h\) 次。现在有些石柱上有蜥蜴,每只蜥蜴只能跳到距离不超过 \(d\) 的石柱或网格外。问无法逃离网格的蜥蜴总数最小值。
模型转换
显然,每个石柱有一个容量,所以这是经典的点容量问题。我们对每一个点建立入点和出点,然后进行连边。
所以我们去这样拆点连边,然后跑\(\texttt{ dinic }\)爆切就行了。
#include<bits/stdc++.h>
#pragma optimize("Ofast,unroll-loop")
using namespace std;
const int N=809,inf=0x3f3f3f3f;
struct edge{int to,nxt,c;}e[N*N*4]; int hd[N],tot=1;
int r,ds,h[N],st,ed,cnt; int c;
int hsh(int x,int y){return (x-1)*c+y;}
void add(int u,int v,int c){e[++tot]=(edge){v,hd[u],c};hd[u]=tot;}
int dd(int x0,int x1){return (x0-x1)*(x0-x1);}
double dis(int i,int j,int k,int l){return sqrt(1.*dd(i,k)+dd(j,l));}
void addh(int u,int v,int c){add(u,v,c),add(v,u,0);}
int f(int i){return i+r*c;}
int d[N];
bool bfs(){
queue<int>q; q.push(st); memset(d,0,sizeof(d)); d[st]=1;
while(!q.empty()){
int u=q.front(); q.pop();
for(int i=hd[u],v;i;i=e[i].nxt)
if(!d[v=e[i].to]&&e[i].c){
d[v]=d[u]+1,q.push(v);
if(v==ed) return 1;
}
}
return 0;
}
int dinic(int pre,int u,int flow){
int rest=flow; if(u==ed) return flow;
for(int i=hd[u],v;i&&rest;i=e[i].nxt)
if((d[v=e[i].to]==d[u]+1)&&e[i].c){
int use=dinic(u,v,min(rest,e[i].c));
if(use==0) d[v]=0;
rest-=use,e[i].c-=use,e[i^1].c+=use;
}
return flow-rest;
}
int main(){
ios::sync_with_stdio(0);
std::cin>>r>>c>>ds;
st=r*c*2+1,ed=r*c*2+2;
for(int i=1;i<=r;i++){
string s; cin>>s;
for(int j=1;j<=c;j++){
h[hsh(i,j)]=s[j-1]-'0';
if(!h[hsh(i,j)]) continue;
if(i-ds<1||i+ds>r||j-ds<1||j+ds>c) addh(f(hsh(i,j)),ed,inf);
addh(hsh(i,j),f(hsh(i,j)),h[hsh(i,j)]);
}
}
for(int i=1;i<=r;i++){
for(int j=1;j<=c;j++){
for(int k=1;k<=r;k++){
for(int l=1;l<=c;l++){
if(i==k&&j==l) continue;
if(!h[hsh(i,j)]||!h[hsh(k,l)]) continue;
if(dis(i,j,k,l)<=ds) addh(f(hsh(i,j)),hsh(k,l),inf);
}
}
}
}
for(int i=1;i<=r;i++){
string s;cin>>s;
for(int j=1;j<=c;j++){
if(h[hsh(i,j)]&&s[j-1]=='L') addh(st,hsh(i,j),1),cnt++;
}
}
int ans=0,tmp=0; while(bfs()) while(tmp=dinic(0,st,0x3f3f3f3f)) ans+=tmp;
std::cout<<cnt-ans;
return 0;
}
3.2 T2 [BOI2008] 黑手党
题目链接:
洛谷:https://www.luogu.com.cn/problem/P4662
LOJ:https://loj.ac/problem/2368
题意简化
一张无向图,割掉一个点 \(u\) 有自己的代价 \(a_u\),问这张图的最小点割,并输出割掉的点。
模型转换
同样用拆点的方法,跑 \(\texttt{dinic}\)。
最后输出方案,对于最后的惨量网络,跑一遍dfs,如果一个点的入点被访问但出点未被访问,代表这个点被割了。
#include<bits/stdc++.h>
#define rint register int
using namespace std;
const int N=2009,M=2e4+9,inf=0x3f3f3f3f;
struct edge{int to,nxt,c;}e[M*10]; int hd[N],tot=1;
void add(int u,int v,int w){e[++tot]=(edge){v,hd[u],w};hd[u]=tot;}
void addh(int u,int v,int w){add(u,v,w),add(v,u,0);}
int n,m,s,t,a[N],ans;
int d[N];
bool bfs(){
queue<int>q; q.push(s); memset(d,0,sizeof(d)); d[s]=1;
while(!q.empty()){
int u=q.front(); q.pop();
for(rint i=hd[u],v;i;i=e[i].nxt)
if(!d[v=e[i].to]&&e[i].c){
d[v]=d[u]+1; q.push(v);
if(v==t+n) return 1;
}
} return 0;
}
int ix35txdy(int u,int flow){
int rest=flow; if(u==t+n) return flow;
for(int i=hd[u],v;i&&rest;i=e[i].nxt)
if((d[v=e[i].to]==d[u]+1)&&e[i].c){
int use=ix35txdy(v,min(e[i].c,rest));
if(use==0) d[v]=0;
rest-=use,e[i].c-=use,e[i^1].c+=use;
}
return flow-rest;
}
int vst[N],cnt,ar[N];
void dfs(int u){
vst[u]=1;
for(rint i=hd[u];i;i=e[i].nxt) if(!vst[e[i].to]&&e[i].c) dfs(e[i].to);
}
void prtxdy(){
for(rint i=1;i<=n;i++) if(vst[i]&&(!vst[i+n])) printf("%d ",i);
}
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t);
for(rint i=1;i<=n;i++) scanf("%d",&a[i]),addh(i,i+n,a[i]);
int u,v; for(rint i=1;i<=m;i++)
scanf("%d%d",&u,&v),addh(u+n,v,inf),addh(v+n,u,inf);
int tmp=0; while(bfs()) while(tmp=ix35txdy(s,inf)) ans+=tmp;
dfs(s); prtxdy();
return 0;
}
//ps 小黄鸭调试法真好用啊
4 网格图&网络流
网格图的建模一般有几种,其中很多都离不开一种建边:把行,列想象成点。对于一些覆盖类问题,可以让覆盖对象作为边;对于一些放置类问题,可以让放置对象作为边,连接行和列。
4.1 T1 [U05 Jan] Muddy Fields G
题意简化
牧场是一个的矩形。每个格子是草地或者泥坑。约翰决定在泥泞的牧场里放置一些木板。每一块木板的宽度为 \(1\) 个单位,长度任意,每一个板必须放置在平行于牧场的泥地里,不能穿过草地。
模型转换
按照网格图建模套路,这里把一个泥坑块想象成一条边,所在的横向和纵向泥坑块是点,遇到一个泥坑点就把横向泥坑块所对应的点和纵向泥坑块所对应的点连起来。
整理一下,点是每一个横/纵向泥坑连通块作为一个点,然后每一个小泥坑作为边连接两个点,求最小点覆盖。
由于这是一个二分图(纵连通块不能连纵连通块,横连通块也不能连横连通块),所以珂以用 König 定理转换为二分图做。
#include<bits/stdc++.h>
using namespace std;
const int N=109,M=109*109*2;
int n,m; char c[N][N]; int cnt1,cnt2,h[N][N],l[N][N],ans;
struct edge{int to,nxt;}e[M]; int hd[M],tot;
void add(int u,int v){e[++tot]=(edge){v,hd[u]}; hd[u]=tot;}
bool vst[M*2]; int cc[M*2];
bool hungary(int u){
for(int i=hd[u],v;i;i=e[i].nxt)
if(!vst[v=e[i].to])
if(cc[v]==(!(vst[v]=1))||hungary(cc[v]))
return cc[v]=u,1;
return 0;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%s",c[i]+1);
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){
if(c[i][j]=='*') ++cnt1; //新的连通块
while(c[i][j]=='*') h[i][j++]=cnt1; //横向连通块
}
for(int j=1;j<=m;j++) for(int i=1;i<=n;i++){
if(c[i][j]=='*') ++cnt2; //新的连通块
while(c[i][j]=='*') l[i++][j]=cnt2,l[i-1][j]+=cnt1; //纵向连通块
}
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
if(c[i][j]=='*') add(h[i][j],l[i][j]),add(l[i][j],h[i][j]); //建边
for(int i=1;i<=cnt1;i++) memset(vst,0,sizeof(vst)),ans+=hungary(i);
printf("%d",ans);
return 0;
}
4.2 T2 [TJOI2016] 游戏
题目链接 https://loj.ac/problem/2057 https://www.luogu.com.cn/problem/P2825
题意简化
一个图,一个炸弹可以放在 '*' 处,并炸到自己所在的一行和一列。炸弹的威力可以穿过 'x',但不可以穿过 '#' 。现在问最多可以放多少个炸弹,让炸弹炸到的格子不重叠。
模型转换
按照网格图建模套路,我们可以把空地表示成一条边,连接行和列。
现在最大的问题是硬石头拦截炸弹,所以我们可以学着第一题的样子把每一行每一列分块,分成用硬石头阻拦的几段。
最后求一个二分图最大匹配即可(即行配列,网格图二分图模板)。
#include<bits/stdc++.h>
using namespace std;
const int N=2509;
string s[N]; int n,m; int h[N][N],toth,l[N][N],totl,use[N*N];
struct edge{int to,nxt;}e[N*N*2]; int hd[N*N],tot;
void add(int u,int v){e[++tot]=(edge){v,hd[u]};hd[u]=tot;}
int c[N],pr[N],ans;
bool ix35txdy(int u){
for(int i=hd[u],v;i;i=e[i].nxt)
if(!pr[v=e[i].to])
if(c[v]==(!(pr[v]=1))||ix35txdy(c[v]))
return c[v]=u,1;
return 0;
}
int main(){
ios::sync_with_stdio(0);
std::cin>>n>>m;
for(int i=0;i<n;i++) std::cin>>s[i];
for(int i=0;i<n;i++) for(int j=0;j<m;j++){
if(j==0) toth++;if(s[i][j]=='#') toth++; h[i][j]=toth;
}
for(int j=0;j<m;j++) for(int i=0;i<n;i++){
if(i==0) toth++;if(s[i][j]=='#') totl++; l[i][j]=totl+toth;
}
for(int i=0;i<n;i++) for(int j=0;j<m;j++)
if(s[i][j]=='*') add(h[i][j],l[i][j]),add(l[i][j],h[i][j]),use[h[i][j]]=1;
for(int i=1;i<=toth;i++) if(use[i]) memset(pr,0,sizeof(pr)),ans+=ix35txdy(i);
std::cout<<ans;
return 0;
}
5 流量约束
匹配和流量
对于一些求匹配的题目,可以对于相匹配的物品连边,最后连超级源和超级汇。如果 \(u\) 和 \(v\) 最多可以匹配 \(a\) 次,那么边容量为 \(a\)。一个物品取 \(b\) 次可以与超级源/汇连容量为 \(b\) 的边。
这种做法利用到了网络流中边的流量约束(即边的容量),因为跑最大流求匹配的过程中,匹配即流一次这条匹配边。于是这种网络流的流量约束可以包含很多种类的匹配。
注意,这个图一定要是和二分图类似的图,否则就不叫匹配(或者多元匹配,比如 dining 一题)。
5.1 二分图匹配
二分图匹配可以转化为流量约束的匹配模型。
匹配物用一次 \(\to\) 超级源连向匹配物,容量为 \(1\)
被匹配物用一次 \(\to\) 被匹配物连向超级汇,容量为 \(1\)
两者可匹配的连边,容量为 \(1\)
5.2 [网络流24题] 试题库
题意简化
假设一个试题库中有 \(n\) 道试题。每道试题都标明了所属类别。同一道题可能有多个类别属性。现要从题库中抽取 \(m\) 道题组成试卷。并要求试卷包含指定类型的试题。试设计一个满足要求的组卷算法。
即 \(n\) 个物品,每个物品有可以属于一些类,现在要选择物品,使得第 \(i\) 个类中要包含 \(a_i\) 个物品,需要输出方案。每个物品只能当做一个类里的用。
模型转换
类和物品匹配。
第 \(i\) 个类要有 \(a_i\) 个物品 \(\to\) 第 \(i\) 个类和超级源连边,容量为 \(a_i\),如果跑完最大流后 \(a_i\) 没有用完,那么代表 \(\texttt{No Solution}\)
每个物品用一次 \(\to\) 第 \(i\) 个物品和超级汇连边,容量为 \(1\)。
每个类有几个物品 \(\to\) 第 \(i\) 个类连向那几个物品,容量为 \(1\),如果用掉了,代表这个类用了这个物品。
最后输出结果,如果边的残量为 \(0\),代表边被用,输出即可。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+9,inf=0x3f3f3f3f;
struct edge{int to,nxt,c;}e[N*2]; int hd[N],tot=1;
void add(int u,int v,int c){e[++tot]=(edge){v,hd[u],c};hd[u]=tot;}
void addh(int u,int v,int c){add(u,v,c),add(v,u,0);}
int k,n,s,t,ans;
int d[N]; vector<int>vec[29];
bool bfs(){
queue<int>q; q.push(s); memset(d,0,sizeof(d)); d[s]=1;
while(!q.empty()){
int u=q.front(); q.pop();
for(int i=hd[u],v;i;i=e[i].nxt)
if(e[i].c&&(!d[v=e[i].to])){
d[v]=d[u]+1; q.push(v);
if(v==t) return 1;
}
}
return 0;
}
int dinic(int u,int flow){
int rest=flow; if(u==t) return flow;
for(int i=hd[u],v;i&&rest;i=e[i].nxt)
if(d[v=e[i].to]==d[u]+1&&e[i].c){
int use=dinic(v,min(e[i].c,rest));
if(!use) d[v]=0;
rest-=use,e[i].c-=use,e[i^1].c+=use;
}
return flow-rest;
}
bool cal(){
for(int i=hd[s];i;i=e[i].nxt) if(e[i].c) return 0;
for(int u=1;u<=k;u++){
for(int i=hd[u],v;i;i=e[i].nxt){
if(e[i].c==0&&e[i].to<=n+k) vec[u].push_back(e[i].to-k);
}
} return 1;
}
int main(){
scanf("%d%d",&k,&n); s=k+n+1,t=k+n+2;
for(int i=1,num;i<=k;i++) scanf("%d",&num),addh(s,i,num);
for(int i=1,num;i<=n;i++){
scanf("%d",&num); addh(i+k,t,1);
for(int j=1,ty;j<=num;j++) scanf("%d",&ty),addh(ty,i+k,1);
} int tmp;
while(bfs()) while(tmp=dinic(s,inf)) ans+=tmp;
bool ansb=cal();
if(!ansb) return puts("No Solution!");
else{
for(int i=1;i<=k;i++){
printf("%d: ",i);
for(int j=0;j<vec[i].size();j++) printf("%d ",vec[i][j]);
puts("");
}
}
return 0;
}