【笔记】二分图/网络流
来自\(\texttt{SharpnessV}\)的省选复习计划中的二分图/网络流。
给定一个二分图,需要找出最多的不相交的边。
比较简单的方法是匈牙利算法,每次找增广路然后直接增广即可。时间复杂度是\(\rm O(NM)\)。
#include<cstdio>
#include<cstring>
using namespace std;
struct edge{
int next;
int to;
}e[1000000];
int n,m,k,h[5005],to[5005],pop=0;
int visit[5005];
void add(int x,int y){
pop++;
e[pop].next=h[x];
e[pop].to=y;
h[x]=pop;
}
bool find(int p){
for(int i=h[p];i;i=e[i].next){
if(visit[e[i].to])continue;
visit[e[i].to]=1;
if(!to[e[i].to]||find(to[e[i].to])){
to[e[i].to]=p;
return true;
}
}
return false;
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=k;i++){
int x,y;
scanf("%d%d",&x,&y);
if(x<=n&&y<=m)add(x,y);
}
int ans=0;
for(int i=n;i>=1;i--){
memset(visit,0,sizeof(visit));
if(find(i))ans++;
}
printf("%d\n",ans);
return 0;
}
复杂一点的方法是直接建图跑网络流,\(\texttt{Dinic}\) 跑二分图的时间复杂度是 \(\rm O(N\sqrt{M})\)。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define N 1005
#define M 100005
using namespace std;
int n,m,k,h[N],tot=1;
struct edge{
int to,nxt,cap;
}e[M<<1];
void add(int x,int y,int z){
e[++tot].nxt=h[x];h[x]=tot;e[tot].cap=z;e[tot].to=y;
}
int s,t,d[N],cur[N];
queue<int>q;
bool bfs(){
memset(d,0,sizeof(d));
d[s]=1;q.push(s);
while(!q.empty()){
int x=q.front();q.pop();
cur[x]=h[x];
for(int i=h[x];i;i=e[i].nxt)if(e[i].cap&&!d[e[i].to])
d[e[i].to]=d[x]+1,q.push(e[i].to);
}
return d[t];
}
int dfs(int x,int flow){
if(x==t)return flow;
int res=flow;
for(int &i=cur[x];i;i=e[i].nxt)
if(res&&e[i].cap&&d[x]+1==d[e[i].to]){
int now=dfs(e[i].to,min(res,e[i].cap));
if(!now)d[e[i].to]=0;
e[i].cap-=now;
e[i^1].cap+=now;
res-=now;
}
return flow-res;
}
int main(){
scanf("%d%d%d",&n,&m,&k);
s=n+m+1;t=n+m+2;
rep(i,1,n)add(s,i,1),add(i,s,0);
rep(i,1,m)add(n+i,t,1),add(t,n+i,0);
rep(i,1,k){
int x,y;scanf("%d%d",&x,&y);
add(x,n+y,1);add(n+y,x,0);
}
int ans=0;
while(bfs())ans+=dfs(s,0x7fffffff);
printf("%d\n",ans);
return 0;
}
仍然是每次找增广路然后增广。每次先 \(\texttt{BFS}\) 出每个节点的层数,然后在分层图上增广。因为分了层,所以可以在找到路径的同时增广。
时间复杂度是\(\rm O(N^2M)\),一般卡不满。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 205
#define M 10005
#define int long long
using namespace std;
int n,m,s,t,h[N],tot=1;
struct edge{
int to,nxt,cap;
}e[M];
void add(int x,int y,int z){
e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;e[tot].cap=z;
e[++tot].nxt=h[y];h[y]=tot;e[tot].to=x;e[tot].cap=0;
}
int d[N],cur[N];queue<int>q;
bool bfs(){
memset(d,0,sizeof(d));
d[s]=1;q.push(s);
while(!q.empty()){
int x=q.front();q.pop();
cur[x]=h[x];
for(int i=h[x];i;i=e[i].nxt)if(e[i].cap&&!d[e[i].to])
d[e[i].to]=d[x]+1,q.push(e[i].to);
}
return d[t]>0;
}
int dfs(int x,int flow){
if(x==t)return flow;
int res=flow;
for(int &i=cur[x];i;i=e[i].nxt){
if(d[x]+1==d[e[i].to]&&e[i].cap){
int now=dfs(e[i].to,min(res,e[i].cap));
if(!now){d[e[i].to]=0;}
e[i].cap-=now;e[i^1].cap+=now;res-=now;
}
if(!res)return flow;
}
return flow-res;
}
signed main(){
scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
rep(i,1,m){
int x,y,z;scanf("%lld%lld%lld",&x,&y,&z);
add(x,y,z);
}
long long ans=0;
while(bfs())ans+=dfs(s,0x7fffffffffffffffLL);
printf("%lld\n",ans);
return 0;
}
由于要使得费用最小,所以我们每次找费用最小的增广路,这样就不能再使用\(\texttt{Dinic}\)算法。
退一步,我们用 \(\texttt{EK}\) 算法,用 \(\texttt{SPFA}\) 找费用最小的增广路,然后增广。
时间复杂度能过。一般最大流不卡 \(\texttt{Dinic}\) ,费用流不卡 \(\texttt{EK}\),如果卡了喷出题人就完事了。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 5005
#define M 100005
using namespace std;
int n,m,s,t,h[N],tot=1;
struct edge{
int to,nxt,cap,val;
}e[M];
void add(int x,int y,int z,int val){
e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;e[tot].cap=z;e[tot].val=val;
e[++tot].nxt=h[y];h[y]=tot;e[tot].to=x;e[tot].cap=0;e[tot].val=-val;
}
queue<int>q;
int d[N],pre[N],ff[N];bool v[N];
bool spfa(){
memset(d,0x3f,sizeof(d));
memset(v,0,sizeof(v));
memset(ff,0,sizeof(ff));
q.push(s);d[s]=0;ff[s]=0x7fffffff;
while(!q.empty()){
int x=q.front();q.pop();v[x]=0;
for(int i=h[x];i;i=e[i].nxt)if(e[i].cap&&e[i].val+d[x]<d[e[i].to]){
d[e[i].to]=d[x]+e[i].val,pre[e[i].to]=i^1,ff[e[i].to]=min(ff[x],e[i].cap);
if(!v[e[i].to])v[e[i].to]=1,q.push(e[i].to);
}
}
if(d[t]<0x3f3f3f3f)return true;return false;
}
int flow,ans;
void updata(){
flow+=ff[t];ans+=ff[t]*d[t];
int now=t;while(now!=s){e[pre[now]].cap+=ff[t],e[pre[now]^1].cap-=ff[t];now=e[pre[now]].to;}
}
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t);
rep(i,1,m){
int x,y,z,op;scanf("%d%d%d%d",&x,&y,&z,&op);
add(x,y,z,op);
}
while(spfa())updata();
printf("%d %d\n",flow,ans);
return 0;
}
以下是正文。
Part 1: 网络流24
二分图模板,两种飞行员相互连边即可。
费用流模板,相邻点连费用为 \(1\) 的边。
拆点,将每天拆成两个点,分别表示这天的新餐巾和这天的旧餐巾。
源点向旧餐巾连容量 \(r_i\),费用为 \(0\) 表示每天产生的旧餐巾。
源点向新餐巾连容量 \(r_i\),费用为 \(p\) 表示购买的新餐巾。
旧餐巾向 \(m/n\) 天后的新餐巾连容量无限,费用为\(f/s\)表示洗餐巾。
新餐巾向汇点连容量 \(r_i\),费用为 \(0\) 表示每天需要的餐巾。
第 \(i\) 天的旧餐巾向第 \(i+1\) 天的旧餐巾连容量无限,费用 \(0\) 的边表示今天的餐巾可以拖到明天。
最后跑费用流即可。
按时间建立分层图,然后跑最大流。
最小割即最大流,证明略。
对于本题,源点向实验连容量为利润的边,器材向汇点连容量为费用的边,相关的实验和器材之间连 \(\inf\) 的边。
当我们割掉一条边,意味着放弃实验/购买器材。如果存在一条由源点到汇点的路径,意味着有一个实验没有放弃,但是器材仍没有购买。所以我们要花费最小的代价使得图不连通,直接跑最小费用最大流。
简单网络流建模,但是要输出方案。
由于最大流等于最小割,所以被流满的边就是割集中的边,就是我们选的试题。
拆点,原 DAG 上一个点拆为入点和出点,原图的边 \(u\to v\),转换为 \(u_{out}\to v_{in}\),源点向入点连边,出点向汇点连边,割掉的一条边表示合并原来的两条路径。
不是很难理解,最后输出方案需要用到并查集。
拆点,对于在点上的限制,例如限制一个点的选取次数,我们可以将点拆为两个点,然后在点之间连边。
本题拆点,然后对于\(f[i]+1=f[j]\)的转移,在\(i\)的出点和\(j\)的入点间连边。
建模不难,这是个二分图最大独立集。
二分图中:最大独立集 \(=\) 点数 \(-\) 最小点覆盖,最小点覆盖 \(=\) 最大匹配 \(=\) 最小割 \(=\) 最大流 。
Part 2:省选
显然补图的最大独立集等于原图的最大团,补图的最大团等于原图的最大独立集。
二分图的最大独立集等于点数减去最小覆盖,最小覆盖等于最大匹配。
如果没有墙,就是经典的行列模型,直接上二分图。
既然有墙,我们仍然可以看作行列模型。只不过如果有墙阻挡,就把原来的行/列拆成多段,然后二分图匹配即可。
求二分图最大匹配必经边。
我们可以先跑网络流,得到残余网络。
残余网络包括很多信息,比如退流的信息。
那么必经边的两段在原图上必定不强连通,因为如果强连通,则必然包含一个环,我们可以将环上的一条边退流,从环的其余部分增广。
所以我们再在残余网络上跑一边\(\texttt{Tarjan}\)算法即可。
经典模型:最大权闭合子图。
给定若干个物品,每个物品有一个价值,以及一些限制条件\(u\to v\)表示选了物品\(u\)就必须选物品\(v\),求最大价值和。
这个简单:我全部选
价值可以为负。
我们可以将模型转换为最小割模型,对于每个物品,如果点权大于\(0\),与\(S\)连边,否则与\(T\)连边,容量为价值的绝对值。
对于一个条件\(u\to v\),从 \(u\) 向 \(v\) 连容量为 \(\inf\) 的边。
最后跑最大流最小割即可。
分析一下,对于每个物品,如果割掉连 \(S\) 的边,表示不选它,割掉和\(T\)的边,表示选它。
那么如果存在一条通路\(S\to a\to b\to T\),表示没有选择了\(a\),而没有选择\(b\)。最小割可以使得网络中不存在通路。
同样是最大权闭合子图,难度低于上面的题,留给思考。
很好的思维题。
拆点,对于每个师傅,我们拆乘\(N\)个点,第\(i\)个点表示是倒数第\(i\)个来修车的。
对于一辆车,如果它是倒数第\(i\)个修的,那么它的修车时间要算\(i\)次。所以费用为修车时间\(\times i\) ,容量为\(1\),拆出的每个点连向汇点的边容量为\(1\),表示一个师傅一个时间只能修一个车。
上下界最小费用可行流。
对于每条必须边,先把它流满。为使整张网络的流量守恒,我们再建立超级源点和超级汇点进行补流操作。
我们对每个点计算 \(d[i]\) 表示将必经边流满后第\(i\)个节点入流和出流的差。
如果\(d[i]>0\)说明供大于求,我们从超级源点向\(i\)连一条容量为\(d[i]\)的边,表示还需要吐出这么多流。
如果\(d[i]<0\)说明供不应求,我们从\(i\)向超级汇点连一条容量为\(-d[i]\)的边,表示还需要吞掉这么多流。
我们还要连\(t\to s\)的容量为\(\inf\)的边,表示整张图的入流和出流平衡。
上下界网络流还有一系列,但本质上相同。