网络流学习笔记
0.相关概念(来自oi-wiki)
网络(network)是指一个特殊的有向图 \(G=(V,E)\),其与一般有向图的不同之处在于有容量和源汇点。
\(E\) 中的每条边 \((u, v)\) 都有一个被称为容量(capacity)的权值,记作$ c(u, v)$。当 \((u,v)\notin E\) 时,可以假定 \(c(u,v)=0\)。
\(V\) 中有两个特殊的点:源点(source)\(s\) 和汇点(sink)\(t\)(\(s \neq t\))。
对于网络 \(G=(V, E)\),流(flow)是一个从边集 \(E\) 到整数集或实数集的函数,其满足以下性质。
容量限制:对于每条边,流经该边的流量不得超过该边的容量,即 \(0 \leq f(u,v) \leq c(u,v)\);
流守恒性:除源汇点外,任意结点 \(u\) 的净流量为 \(0\)。其中,我们定义 \(u\) 的净流量为 \(f(u) = \sum_{x \in V} f(u, x) - \sum_{x \in V} f(x, u)\)。
对于网络 \(G = (V, E)\) 和其上的流 \(f\),我们定义 f 的流量 \(|f|\) 为 s 的净流量 \(f(s)\)。作为流守恒性的推论,这也等于 t 的净流量的相反数 \(-f(t)\)。
对于网络 \(G = (V, E)\),如果 \(\{S, T\}\) 是 V 的划分(即 \(S \cup T = V\) 且 \(S \cap T = \varnothing\)),且满足 \(s \in S, t \in T\),则我们称 \(\{S, T\}\) 是 \(G\) 的一个 \(s-t 割\)(cut)。我们定义 \(s-t\) 割 \(\{S, T\}\) 的容量为 \(||S, T|| = \sum_{u \in S} \sum_{v \in T} c(u, v)\)。
瞄一眼就行。
1.板子选讲
(1)网络最大流
板子题: Luogu P3376
口糊思路:先用 BFS 给整张图分层,判断 \(s\) 和 \(t\) 是否连通。只要两者联通,就跑 DFS。 DFS 之前,在现有网络基础上建立残余网络。残余网络的定义是在任意时刻,网络中所有节点以及剩余容量大于 \(0\) 的边构成的子图.每次跑 DFS,就对当前动作做一次增广,所谓增广,可以理解为扩张。
如果理解不了上面的话,可以把网络流理解成水流,自己模拟一下。
代码附上:
il bool bfs(int s,int t)
{
for(int i=1;i<=n;i++)d[i]=-1;
queue<int> q;
q.push(s);d[s]=0;
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=h[u];~i;i=e[i].nxt)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w)q.push(v),d[v]=d[u]+1;
}
}
return d[t]!=-1;
}
int dfs(int u,int minf)
{
if(!minf||u==t)return minf;
int f,flow=0;
for(int i=cur[u];~i;i=e[i].nxt)
{
cur[u]=i;int v=e[i].v;
if(d[v]==d[u]+1&&(f=dfs(v,min(minf,e[i].w))))
{
minf-=f,flow+=f;
e[i].w-=f,e[i^1].w+=f;
if(minf==0)return flow;
}
}
return flow;
}
il int Dinic()
{
MaxFlow=0;
while(bfs(s,t))
{
for(int i=1;i<=n;i++)cur[i]=h[i];
MaxFlow+=dfs(s,inf);
}
return MaxFlow;
}
signed main()
{
memset(h,-1,sizeof(h));
scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
addedge(u,v,w);
addedge(v,u,0);
}
printf("%lld\n",Dinic());
return 0;
}
有了网络最大流,我们就可以通过建模的方式完成二分图最大匹配问题。具体建模如下:
构造两个虚点 \(s\) 和 \(t\),把其中一部分的点和 \(s\) 连边,另一部分的点和 \(t\) 连边,让图中边的流量为1,从 \(s\) 到 \(t\) 跑一边网络最大流,结果就是二分图最大匹配。
代码:
signed main(){
memset(h,-1,sizeof(h));
cin>>n>>m>>ed;
t=n+m+1;
for(int i=1;i<=n;i++)addedge(s,i,1),addedge(i,s,0);
for(int i=1;i<=ed;i++)
{
int u,v;cin>>u>>v;
if(u>n||v>m)continue;
addedge(u,v+n,1),addedge(v+n,u,0);
}
for(int i=1+n;i<=m+n;i++)addedge(i,t,1),addedge(t,i,0);
Dinic();
cout<<MaxFlow<<"\n";
return 0;
}
练习:飞行员配对方案问题
(2)最大流最小割定理
什么是割?对于一个网络流图 \(G=(V,E)\),其割的定义为一种 点的划分方式:将所有的点划分为 \(S\) 和 \(T=V-S\) 两个集合,其中源点 \(s\in S\),汇点 \(t\in T\)。就是把整个图劈成 \(S,T\) 两半。定义割的容量为所有从 \(S\) 到 \(T\) 的边的容量和。最小割就是所有割中,割的容量最小的一条割的容量。
最大流最小割定理就是说最小割就等于最大流。这个可以用木桶原理感性理解一下,这样,我们就可以直接跑一遍最大流,求出最小割。
(3)费用流
每个边除了流量还有一个费用,我们要求在最大流基础上的最小(或最大)费用。这里使用贪心的 SSP 算法,每次找单位费用最小的路径增广,直到图上找不到增广路为止。时间复杂度 \(O(nmf)\),实际上跑不满。实现只用将 bfs
改成 spfa
就行。
bool spfa()
{
for(int i=1;i<=t;i++)dis[i]=INF,vis[i]=false;
dis[s]=0,vis[s]=true;q.push(s);
while(!q.empty())
{
int u=q.front();q.pop();vis[u]=false;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(dis[v]>dis[u]+e[i].w&&e[i].c)
{
dis[v]=dis[u]+e[i].w;
if(!vis[v])q.push(v),vis[v]=true;
}
}
}
return dis[t]!=INF;
}
int dfs(int u,int minf)
{
if(u==t||!minf) return minf;
int flow=0,f;
vis[u]=true;
for(int i=cur[u];i;i=e[i].nxt)
{
cur[u]=i;
int v=e[i].v;
if(!vis[v]&&e[i].c&&dis[v]==dis[u]+e[i].w)
{
f=dfs(v,min(minf,e[i].c));
if(f)
{
e[i].c-=f,e[i^1].c+=f,minf-=f,flow+=f,ans+=f*e[i].w;
if(!minf)return vis[u]=false,flow;
}
}
}
return vis[u]=false,flow;
}
void solve()
{
ans=0;
while(spfa())
{
for(int i=1;i<=t;i++)cur[i]=head[i];
dfs(s,INF);
}
}
2.网络流算法建模经典模型
(1)最大权闭合子图
首先讲一下定义。最大权闭合子图,就是一个带点权的图中,点权和最大的闭合子图。闭合子图的定义就是说,设这个子图的点集为 \(G\),对于任何 \(u \in G\),\(u\) 的所有出边所到达的点 \(v\) 都满足 \(v \in G\)。通法是将权为正的点连源点,权为负的连汇点,容量为权的绝对值,点与点之间连边,容量为 \(\inf\),然后跑最小割。
例题:太空计划飞行问题
建边:考虑抽象一下这个题。我们建个二分图,一边是实验,一边是仪器,然后连上边。我们就这样构造出了一个二分图。如果我们将实验点的点权设为正的,仪器点的点权设为负的,那我们就得到了一个最大权闭合子图问题。
我们有一种冲动,叫做建源点、汇点,源点连实验,汇点连仪器,边权分别是实验的收益、仪器的代价,然后实验和仪器根据关联连边,边权为 \(\inf\)。可以证明,如果这个时候我们搞到这个图的最小割,那这个割肯定不会割到中间边权为 \(\inf\) 的边,也就是说割完之后,与源点相连的点构成的子图就是最大权闭合子图。因此我们只需要跑一边最大流求出最小割,可知这个割上的边,要么代表着带来负收益的实验,要么代表着必须花的配仪器的钱,那么我们只用把所有实验的收益加在一起,减去最小割就是最大利润。至于方案,我们只需要最后跑一边 bfs,看哪些点是源点可以达到的,那就说明这个点肯定在方案里。完结撒花。
放建边的代码。
signed main()
{
memset(head,-1,sizeof(head));
scanf("%lld%lld",&m,&n);
s=m+n+1,t=m+n+2;
for(int i=1;i<=m;i++)
{
scanf("%lld",p+i);
sum+=p[i];
add(s,i,p[i]),add(i,s,0);
char tools[10000];
memset(tools,0,sizeof tools);
cin.getline(tools,10000);
int ulen=0,tool;
while(sscanf(tools+ulen,"%d",&tool)==1)
{
add(i,tool+m,INF),add(tool+m,i,0);
if(tool==0) ulen++;
else while(tool)tool/=10,ulen++;
ulen++;
}
}
for(int i=1;i<=n;i++)
{
scanf("%lld",c+i);
add(i+m,t,c[i]),add(t,i+m,0);
}
int ans=Dinic(); bfs();
for(int i=1;i<=m;i++)if(d[i]!=-1)printf("%lld ",i);
printf("\n");
for(int i=m+1;i<=m+n;i++)if(d[i]!=-1)printf("%lld ",i-m);
printf("\n%lld\n",sum-ans);
return 0;
}
圆桌问题也是此类题,要简单一点。
(2)最小路径覆盖集
例题:最小路径覆盖问题
考虑最劣的情况,就是每个点只和自己连边,这样答案就是 \(n\),如果我们能将两个集合通过连边的方式合并的话,那答案就会 \(-1\)。考虑建图,把每个点拆开,建二分图,从二分图左侧向右侧连给的图中的边,源点连左边,右边连汇点,途中流量全设为 1。可以知道,这样跑出来的网络流,结果就是最大的合并次数,用 \(n\) 减去这个次数就是我们要求的答案。
对于输出路径,我们可以通过维护并查集来实现。
#define INF 0x7f7f7f7f
const int N=1005,M=6005;
int n,m,s,t,head[N],tot=-1,fa[N];bool vis[N];
int getf(int x){return fa[x]==x?x:fa[x]=getf(fa[x]);}
struct edge{int v,w,nxt;}e[M<<1];
void add(int u,int v,int w){}
int d[N],cur[N];
queue<int> q;
bool bfs(){}
int dfs(int u,int minf){}
int Dinic(){}
void ddfs(int u)
{
if(u!=s&&u!=t)cout<<u<<" ";
for(int i=head[u];~i;i=e[i].nxt)
{
int v=e[i].v;
if(!e[i].w&&v>n&&v<s)ddfs(v-n);
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
memset(head,-1,sizeof(head));
cin>>n>>m;s=2*n+1,t=2*n+2;
for(int i=1;i<=n;i++)fa[i]=i,add(s,i,1),add(i,s,0);
for(int i=1;i<=m;i++)
{
int u,v;cin>>u>>v;
add(u,v+n,1),add(v+n,u,0);
}
for(int i=1;i<=n;i++)add(i+n,t,1),add(t,i+n,0);
int ans=Dinic();
for(int i=0;i<=tot;i++)
{
int u=e[i^1].v,v=e[i].v;
if(u>=1&&u<=n&&v>n&&v<s&&!e[i].w)fa[getf(v-n)]=getf(u);
}
for(int i=1;i<=n;i++)
{
int x=getf(i);
if(i==x){ddfs(x);cout<<"\n";}
}
cout<<n-ans<<"\n";
return 0;
}