网络流学习笔记
网络流学习笔记
本来是不想写的,因为不想在里面博客插入图片,但是发现网络流似乎可以牵扯出许多不为人知的图论内容,因此特此写一篇博客铺路。
前言
网络流是一种说难也不难,说简单也不简单的结构。难就难在对于一道题来说,我们难以分辨需要用到什么算法,怎么建图,因此,我们只能多做多练,积累各种各样的模型。
定义
网络一般是有向的、有边权的、有源汇的一张关于源点和汇点连通的图,即从源点开始存在一条边到达汇点。对于一张网络存在一些流,这些流从源点开始沿着边的方向流到汇点,其中从源点可以引出无穷多的流量,到汇点可以接受无穷多的流量。对于一张网络的可行流而言,满足以下条件:
- 每条网络的流量不超过其的边权,即最大流量。
- 除源汇两点之外的所有点,不会有多余的流量,即如果从各条边向某个点运输来了 \(k\) 个单位的流量,那么也一定从这个点运走了 \(k\) 个单位的流量,即出入平衡。
此外,还有一些专业用词,可以了解一下,便于理解:
-
增广路:对于一张网络已有的流来说,还存在一条从源点到汇点的路径,使得这条路径上的所有边的流量能够增加,那么这条路径就是一条增广路。
-
反向边:因为网络流选取局部最优解,所以通常无法直接且正确的求解出每条边应有的流量,因此通过添加反向边的操作,使得我们可以通过走反向边的方式来撤销影响,下给出证明:
对于两条路径 s->a->b->t 和 s->b->a->t 相当于撤回 a->b 的流量,将这些流量转移到 a->t上 由于s->b->a->t是一条合法的增广路,所以显然可行 此时点 a 出入平衡 对于点 b 来说,缺少的 a->b 的流量可以通过 s->b 的流量补回 此时点 b 出入平衡 因为除源汇两点外所有点出入平衡,所以此时的流是一种可行流
因为在残量网络中,反向边是和正向边相对的存在,因此两者在残量网络中的边权之和总为这条边的最大流量。因为反向边在残量网络中表示有多少流量能撤回,正向边则表示有多少流量能用。
-
满流:指的是一条边上的流量与其最大流量相同。
-
残量网络:通常来说,在一种可行流中,并不是所有的边都能到达满流的状态,当前流量和最大流量的差值就是这条边的残量,所有边的残量作为边权组成的网络叫残量网络。通常来说,满流边不存在于残量网络当中,反向边如果未处于满流状态,也应当存在于残量网络中。
了解了这些,我们就可以看一些简单的场景。
常见模型
最大流
定义
最大流指的是在所有的可行流中,运输到汇点的流量最多的那一种(显然,最小值是每一条边都没有流量)。我们通常采用 Dinic 算法求解。
Dinic 算法
Dinic 算法是对 FF 和 EK 算法的优化,但因为后两者的时间复杂度过差,在现实、考试中普遍不能接受,故只提到 Dinic 算法,有想了解的可以自行搜索。
Dinic 算法本质上采用了贪心的思想,对于所有增广路,其中一定有一条最短的。可以证明,只要我们一直选择增广路,直到选不下去为止,不管按照什么顺序来选,都能得到正确的结果。因此,我们不妨每次都找到最短的增广路,将这条路径的贡献加入到答案中,直到无法找到下一条增广路。
代码
bool Bfs(){
for(int i=1;i<=S;i++)dep[i]=0;
queue<int>q;
q.push(s);
dep[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].to){
int y=e[i].v,z=e[i].w;
if(z>0&&dep[y]==0){
dep[y]=dep[x]+1;
if(y==t)return true;
q.push(y);
}
}
}
return false;
}
int Dfs(int x,int flow){
if(x==t)return flow;
int rest=flow;
for(int i=head[x];i;i=e[i].to){
int y=e[i].v,z=e[i].w;
if(z>0&&dep[y]==dep[x]+1){
int k=Dfs(y,min(rest,z));
rest-=k;
e[i].w-=k;
e[i^1].w+=k;
}
}
return flow-rest;
}
int Dinic(){
int res=0;
while(Bfs())res+=Dfs(s,INF);
return res;
}
优化
肉眼可见的,这样的算法理论的复杂度并不优,我们发现,虽然每一次最短路的长度都会 \(+1\),但是对于两条有共同交点的最段路来说,一次并不能将最短路的贡献算完,所以我们不能通过 \(vis\) 数组求解,不加 \(vis\) 数组的 dfs 啥也不是,复杂度是指数级的,但这并不意味着 Dinic 算法非常垃圾,我们可以添加一些优化。
无用节点删除
考虑这样一个问题,如果我们将一个非 \(0\) 的流量给了一个节点,但是我们被告知从这个节点出发无法增广,那我们就没有必要每次都考虑走这个节点了,所以它是无用的,我们可以通过将 \(dep\) 数组的值修改为 \(0\),使得不存在节点的 \(dep+1=0\),相当于这个点被无视了。
剩余流量判断
考虑这样一个问题,你收到了一些流量,如果你在增广中已经将这些流量全部增广完成,那么你还有必要继续增广吗?显然没有,这时直接退出即可。
当前弧优化
继续考虑一个问题,你每一次的增广路都尽可能的将所有流量用光,也就是说,一些你走过的点显然无法继续走,因为你已经将它增广完了。当你的流量耗尽的时候,你最后一个走过的点可能是因为你的剩余流量过少从而不能增广完全,但对于这个点以前的点来说,它们一定无用了,下一次如果再到当前点的时候,就可以直接从最后一个点开始继续遍历,那么我们可以用一个数组存储这样的信息。
通过上面三个优化,我们重新考虑时间复杂度。对于一次 dfs,我们会找到不超过 \(E\) 条的最短路,每条最短路都需要回溯不超过 \(V\) 次,有因为当前弧优化保证了不会重复遍历节点,所以单次 dfs 的时间复杂度是 \(O(VE)\) 的。而优化后,每一次最短路的长度都会至少 \(+1\),因此至多进行 \(V\) 次最短路的寻找,所以复杂度是 \(O(V^2E)\) 的。
事实上,在我们分析复杂度的时候有很多 不超过
类的词眼,因此这个上界复杂度很松。因此,我们通常认为 Dinic 的时间复杂度是 \(O(玄学)\)。
优化代码
int Dfs(int x,int flow){
if(x==t)return flow;
int rest=flow;
for(int i=cnr[x];i&&rest;i=e[i].to){
cnr[x]=i;
int y=e[i].v,z=e[i].w;
if(z>0&&dep[y]==dep[x]+1){
int k=Dfs(y,min(rest,z));
rest-=k;
e[i].w-=k;
e[i^1].w+=k;
}
}
if(rest==flow)dep[x]=0;
return flow-rest;
}
Some Saying
很多人可能会觉得 Dinic 的上界还是有点抽象,可以去学 ISAP。但是我听说 ISAP 在实际运用上并没有 Dinic 灵活,感兴趣的可以去了解,反正 Dinic 不会被卡(因为出题人敢卡就会被骂死)。
常用场景
事实上,最大流一般用于明显有上界要求,即题目可能出现了最多
等字眼时,一般使用最大流。或者是强制性的要求一定需要多少,通过最大流中的最大限制这些条件。
最小割
定义
割,可以将一个完整的东西分成两半。同理,对于网络,一定有一些边删除后可以使得整张网络不连通,这些边可以组成一个割。事实上,对于任意一张图,也存在割。而这些边的权值和即为对应割的容量,显然,当所有边都被选中时,割最大。那么我们需要求解的就是一张网络的最小割。
最大流最小割定理
首先,我们考虑任意一个割的净流量,即从 \(s\) 所在的连通块向 \(t\) 所在的连通块所运输的流量的净值,我们会发现,这个值始终等于当前网络的流量,我们可以这么理解:假设 \(s\) 所在连通块给 \(t\) 所在连通块运输了 \(k\) 个流量,显然,如果没有流量运输回来,那么因为需要满足出入平衡,所有的流都将流入汇点,如此便不符合网络的流量限制,故而一定会运输回多余的流量,从而使得每一种割的净流量都等于当前网络的流量。又考虑到既然容量计算的上限一定大于等于净流量计算的流量,也不会像净流量计算减去某个数值,这说明一个割的容量一定大于等于当前网络的流量。显然最小割的容量对于同一网络的任何可行流都成立,因此最小割一定大于等于最大流。
如何证明能够取到最小值,我们考虑下面三个命题:
- 流 \(f\) 是图 \(G\) 的最大流。
- 图 \(G\) 的残量网络中不存在增广路。
- 流 \(f\) 的流量等于图 \(G\) 的某一个割的容量。
首先 \(1\Rightarrow2\) 是显然的,毕竟是网络流的定义。
由于最小割一定大于等于最大流,所以割的容量也一定大于等于最大流,所以如果存在相等关系,那么流 \(f\) 一定是最大流,\(3\Rightarrow1\) 得证。
对于 \(2\Rightarrow3\) 来说,我们假设此时图 \(G\) 存在割 \((S,T)\),其中 \(S\) 是 \(s\) 所在连通块,\(T\) 是 \(t\) 所在连通块。那么此时,显然有 \(\forall u\in S,v\in T,f(u,v)=c(u,v)\),即边 \(u\to v\) 的流量等于容量,否则此时存在 \(s\to u\to v\to t\) 的增广路,不满足条件。那么此时这个割的容量等于整张图的容量。所以 \(2\Rightarrow3\)。
由此我们发现最小割等于最大流,在求解时直接套用模板即可。
最小割树
事实上,我们会发现,对于同一张无向联通图中,如果我们指定的源点和汇点不同,会导致求出的最小割不同。那么如果我们想要知道每一对点对的最小割,应该怎么求解呢?这个时候就要用到最小割树了。
首先,暴力的时间复杂度是显然的 \(O(V^4E)\),这个复杂度便及其抽象。但是我们可以通过构建最小割树优化。最小割树的构建方法如下:
- 选择任意两个点 \(u,v\),求出最小割,并在新图中将 \(u,v\) 之间连接一条边权为最小割的边。
- 将整张图的节点分为两部分,使得割后一部分与 \(u\) 联通,一部分与 \(v\) 联通。
- 分别递归调用与 \(u\) 联通的部分和与 \(v\) 联通的部分。
值得注意的是,虽然调用的部分可能不是完整的,但最小割仍然需要在整张图上跑。在建好树之后,任意两点的最小割,就是他们在最小割树上的简单路径的最小值。想要了解为什么的可以去看一眼证明。
我们发现,这样子优化之后,我们只跑了 \(V\) 遍最小割,故而复杂度是 \(O(V^3E)\)。
虽然名叫最小割树,但我们没有必要将整颗树建出。首先,对于递归,我们可以将与 \(u\) 联通的点放在整个序列的左端,与 \(v\) 联通的点放在序列的右端,这样子就可以直接分两段调用了,复杂度最差是 \(O(V^2)\)。然后我们发现,对于两个联通块的最小割,根据最小割树的性质,对于 \(\forall a\in U,b\in V,c(a,b)=\min(c(a,u),c(u,v),c(v,b))\)。因为递归调用的缘故,\(c(a,u),c(v,b)\) 我们肯定已知,\(c(u,v)\) 是已经跑过最小割的所以可以直接求解,因此类似于分治的写法,我们可以直接将两点之间的最小割存储到数组中,复杂度是 \(O(V^2)\) 的,查询则是 \(O(1)\) 的,因此总复杂度是 \(O(V^3E+V^2+Q)\)。
当然也可以用树链剖分、lca 等树上算法,复杂度是 \(O(V^3E+V\log V+Q\log V)\) 的。
代码(luoguP4897)
#include <bits/stdc++.h>
using namespace std;
const int N=500,M=3000,INF=2e9;
int n,m,q,u,v;
int cut[N+5][N+5];
struct NetFlow{
int s,t,S,cnt;
int head[N+5],dep[N+5],cnr[N+5];
struct Edge{
int v,w,to,flow;
}E[(M<<1)+5];
void AddNet(int x,int y,int z){
E[cnt].v=y;
E[cnt].flow=z;
E[cnt].to=head[x];
head[x]=cnt++;
return ;
}
void Init(){
cnt=2;
S=n;
for(int i=0;i<=S;i++)head[i]=0;
return ;
}
void Build(){
Init();
int x,y,z;
for(int i=1;i<=m;i++){
scanf("%d%d%d",&x,&y,&z);
AddNet(x,y,z);
AddNet(y,x,z);
}
return ;
}
bool Bfs(){
for(int i=0;i<=S;i++){
dep[i]=0;
cnr[i]=head[i];
}
queue<int>q;
q.push(s);
dep[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i;i=E[i].to){
int y=E[i].v,z=E[i].w;
if(z>0&&dep[y]==0){
dep[y]=dep[x]+1;
if(y!=t)q.push(y);
}
}
}
return dep[t]!=0;
}
int Dfs(int x,int flow){
if(x==t)return flow;
int rest=flow;
for(int i=cnr[x];i&&rest;i=E[i].to){
cnr[x]=i;
int y=E[i].v,z=E[i].w;
if(z>0&&dep[y]==dep[x]+1){
int k=Dfs(y,min(rest,z));
rest-=k;
E[i].w-=k;
E[i^1].w+=k;
}
}
if(rest==flow)dep[x]=0;
return flow-rest;
}
void Clear(){
for(int i=2;i<cnt;i++)E[i].w=E[i].flow;
return ;
}
int Dinic(){
Clear();
int res=0;
while(Bfs())res+=Dfs(s,INF);
return res;
}
}Nf;
struct MinCutTree{
int node[N+5],tmp[N+5];
void Init(){
for(int i=0;i<=n;i++)node[i]=i;
return ;
}
void Query(int l,int r){
if(l==r)return ;
Nf.s=node[l];
Nf.t=node[r];
int k=Nf.Dinic(),s=node[l],t=node[r];
cut[s][t]=cut[t][s]=k;
int L=l,R=r;
for(int i=l;i<=r;i++){
if(Nf.dep[node[i]]!=0)tmp[L++]=node[i];
else tmp[R--]=node[i];
}
for(int i=l;i<=r;i++)node[i]=tmp[i];
Query(l,L-1);
Query(L,r);
for(int i=l;i<L;i++){
for(int j=L;j<=r;j++)cut[node[i]][node[j]]=cut[node[j]][node[i]]=min(min(cut[node[i]][s],cut[s][t]),cut[t][node[j]]);
}
return ;
}
void Build(){
Init();
for(int i=0;i<=n;i++)cut[i][i]=INF;
Query(0,n);
return ;
}
}Mct;
int main(){
scanf("%d%d",&n,&m);
Nf.Build();
Mct.Build();
scanf("%d",&q);
while(q--){
scanf("%d%d",&u,&v);
printf("%d\n",cut[u][v]);
}
return 0;
}
最大权闭合子图
定义
在一张有向图中,每一个点都有对应的点权,这张图的闭合子图 \(S\) 定义为 \(\forall_{u\in S,(u,v)\in E},v\in S\),这个闭合子图的权值定义为 \(\sum_{u\in S}a_u\),最大权闭合子图就是权值和最大的那一个。
关联
事实上,求解最大权闭合子图问题需要用到最小割。我们考虑这样的思路,为了让整张图的权值和最大,我们忽略限制条件的最优解肯定为所有的正权值的和。但因为所有点能够到达的点也应该在子图中,因此一些点的选择与不选择是有代价的。
我们不妨令最后与源点联通的点表示选择,反之的表示不选择。如果对于一个正权值的点我们不选择,意味着我们将损失 \(a_i\),那么相当于我们将这个点的连接和源点断开,因此我们将源点和所有正权值的点连接一条边权为 \(a_i\) 的边。对于一个负权值的点我们选择,意味着我们将损失 \(-a_i\),那么相当于我们将这个点的连接和汇点断开,因此我们将汇点和所有负权值的点连接一个条边权为 \(-a_i\) 的边。同时,为了不让原图中的边被选作最小割,我们让原图的边边权为 \(\infin\)。为了让剩余最大,我们只需要让割去的最小,因此采用最小割求解。
Some saying
通过定义我们显而易见的发现,最大权闭合子图的使用场景通常在题目中出现了负权或有明显的限制条件下。
代码(P1225. [NOI2009] 植物大战僵尸)
#include <bits/stdc++.h>
using namespace std;
const int N=600,M=359400,INF=2e9;
int n,m,cnt,ans;
int head[N+5],deg[N+5],scr[N+5];
struct Edge{
int v,to;
}e[(M<<1)+5];
struct NetFlow{
static const int N=602,M=359400;
int s,t,S,cnt;
int head[N+5],dep[N+5],cnr[N+5];
struct Edge{
int v,w,to;
}e[(M<<1)+5];
void AddEdge(int x,int y,int z){
e[cnt].v=y;
e[cnt].w=z;
e[cnt].to=head[x];
head[x]=cnt++;
return ;
}
void AddNet(int x,int y,int z){
AddEdge(x,y,z);
AddEdge(y,x,0);
return ;
}
void Init(){
cnt=2;
s=n*m+1;
S=t=n*m+2;
for(int i=1;i<=S;i++)head[i]=0;
return ;
}
void show(){
for(int x=1;x<=S;x++){
printf("%d:",x);
for(int i=head[x];i;i=e[i].to)printf("(%d,%d)",e[i].v,e[i].w);
printf("\n");
}
}
bool Bfs(){
for(int i=1;i<=S;i++){
dep[i]=0;
cnr[i]=head[i];
}
queue<int>q;
q.push(s);
dep[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].to){
int y=e[i].v,z=e[i].w;
if(z>0&&dep[y]==0){
dep[y]=dep[x]+1;
if(y==t)return true;
q.push(y);
}
}
}
return false;
}
int Dfs(int x,int flow){
if(x==t)return flow;
int rest=flow;
for(int i=cnr[x];i&&rest;i=e[i].to){
cnr[x]=i;
int y=e[i].v,z=e[i].w;
if(z>0&&dep[y]==dep[x]+1){
int k=Dfs(y,min(rest,z));
rest-=k;
e[i].w-=k;
e[i^1].w+=k;
}
}
if(flow==rest)dep[x]=0;
return flow-rest;
}
int Dinic(){
int res=0;
while(Bfs())res+=Dfs(s,INF);
return res;
}
}Nf;
void Init(){
cnt=1;
for(int i=1;i<=n;i++)head[i]=0;
return ;
}
void AddEdge(int x,int y){
e[cnt].v=y;
e[cnt].to=head[x];
head[x]=cnt++;
return ;
}
void Topo(){
Nf.Init();
queue<int>q;
for(int i=1;i<=n*m;i++){
if(deg[i]==0)q.push(i);
}
while(!q.empty()){
int x=q.front();
q.pop();
if(scr[x]>=0){
ans+=scr[x];
Nf.AddNet(Nf.s,x,scr[x]);
}else Nf.AddNet(x,Nf.t,-scr[x]);
for(int i=head[x];i;i=e[i].to){
int y=e[i].v;
if(--deg[y]==0)q.push(y);
}
}
for(int x=1;x<=n*m;x++){
if(deg[x]==0){
for(int i=head[x];i;i=e[i].to){
int y=e[i].v;
if(deg[y]==0)Nf.AddNet(y,x,INF);
}
}
}
return ;
}
int main(){
scanf("%d%d",&n,&m);
Init();
for(int i=1;i<=n;i++){
int w;
for(int j=1;j<=m;j++){
scanf("%d%d",&scr[(i-1)*m+j],&w);
if(j>1){
AddEdge((i-1)*m+j,(i-1)*m+j-1);
deg[(i-1)*m+j-1]++;
}
int r,c;
for(int k=1;k<=w;k++){
scanf("%d%d",&r,&c);
r++;
c++;
AddEdge((i-1)*m+j,(r-1)*m+c);
deg[(r-1)*m+c]++;
}
}
}
Topo();
ans-=Nf.Dinic();
printf("%d",ans);
return 0;
}
平面图与对偶图
定义
平面图指的是存在一种点的位置,在不弯曲边的情况下,能够使得所有边的交点只存在于节点上。例如节点数 \(>3\) 的完全图不是平面图,环是平面图。容易发现平面图通过点和边,将整个平面分成了多个不重叠图形。
对偶图相对于平面图存在,每一个平面图都对应着一个对偶图。我们将平面图分成的多个图形看作一个点,一对图形可以通过他们相邻的边连点,边权就是相邻的边的边权。一般来说,对于整个图外侧的区域也应当算做一个图形,对于这个图形的设点,应当根据具体案例具体分析。
关联
事实上,利用对偶图能够求解一些用最小割求解的问题,最常见的问题如对于一张平面图最外侧的某两个点的最小割。(参考 P730 狼抓兔子)
由于 Dinic 算法的 \(O(玄学)\) 复杂度,导致在 \(V=10^6,E=3\times 10^6\) 的情况下,依旧以充分低于时限的速度通过了此题,但是本题也可以通过对偶图求解。
显然,题目中给出的图是平面图,如果我们从矩形的左下侧,穿过一个个图形,到达了矩形的右上侧,就相当于我们将整张图分割开了。更形象的说,就像将整张图砍了一刀,分成了左上角和右下角两个不联通的部分,可以在脑海中想象或画图自行理解。那么这时候我们经过的边,就是我们在整张图中割取的边,所以这要求我们经过的边的和尽可能小,很明显这是最短路算法。我们可以利用 SPFA 或 dijkstra 求解。事实上因为 SPFA 的优秀玄学复杂度,依旧成功的通过了此题。三种方法的速度为 Dinic \(<\) dijkstra \(<\) SPFA。
下给出 dijkstra 求解的代码。
代码
#include <bits/stdc++.h>
using namespace std;
//这是对偶图+堆优化的Dijkstra
int n,m;
struct NetFlow{
static const int N=1996004,M=2995003,INF=2e9;
int S,s,t,cnt,ans;
int head[N+5],dis[N+5];
bool vis[N+5];
struct edge{
int v,w,to;
}e[(M<<1)+5];
void AddEdge(int x,int y,int z){
e[cnt].v=y;
e[cnt].w=z;
e[cnt].to=head[x];
head[x]=cnt++;
return ;
}
void AddNet(int x,int y,int z){
AddEdge(x,y,z);
AddEdge(y,x,z);
return ;
}
void Init(){
cnt=1;
s=(n-1)*(m-1)*2+1;
S=t=s+1;
for(int i=1;i<=S;i++)head[i]=0;
return ;
}
void Build(){
Init();
int x;
for(int i=1;i<=n;i++){
for(int j=1;j<m;j++){
scanf("%d",&x);
if(n==1)AddNet(s,t,x);
else if(i==1)AddNet(s,(j<<1)-1,x);
else if(i==n)AddNet(((n-2)*(m-1)+j)<<1,t,x);
else AddNet(((i-2)*(m-1)+j)<<1,(((i-1)*(m-1)+j)<<1)-1,x);
}
}
for(int i=1;i<n;i++){
for(int j=1;j<=m;j++){
scanf("%d",&x);
if(m==1)AddNet(s,t,x);
else if(j==1)AddNet(((i-1)*(m-1)+1)<<1,t,x);
else if(j==m)AddNet(s,((i*(m-1))<<1)-1,x);
else AddNet(((i-1)*(m-1)+j)<<1,(((i-1)*(m-1)+j-1)<<1)-1,x);
}
}
for(int i=1;i<n;i++){
for(int j=1;j<m;j++){
scanf("%d",&x);
AddNet((((i-1)*(m-1)+j)<<1)-1,((i-1)*(m-1)+j)<<1,x);
}
}
return ;
}
int Dijkstra(){
for(int i=1;i<=S;i++){
dis[i]=-INF;
vis[i]=false;
}
priority_queue<pair<int,int> >q;
q.push(make_pair(0,s));
dis[s]=0;
while(!q.empty()){
int x=q.top().second;
q.pop();
if(vis[x])continue;
vis[x]=true;
for(int i=head[x];i;i=e[i].to){
int y=e[i].v,z=e[i].w;
if(dis[x]-z>dis[y]){
dis[y]=dis[x]-z;
if(!vis[y])q.push(make_pair(dis[y],y));
}
}
}
return -dis[t];
}
}Nf;
int main(){
scanf("%d%d",&n,&m);
Nf.Build();
printf("%d",Nf.Dijkstra());
return 0;
}
常用场景
事实上,在最小割的题目中也常常询问最大,这种题目通常拥有代价,即不怎么做会损失 \(x\) 元,不怎么做会损失 \(y\) 元。这种情况下,我们可以先将所有报酬拿到手,然后利用最小割将最小的损失算出来,用总和减去即可。还有一种就是普通的最小割,求解封锁一些边的最小花费。
最小(大)费用最大流
显然,一张网络的最大流不一定只有一个,如果每条边的单位流量都有费用,那么怎么样选择最大流的花费最小呢?
EK+SPFA
我们首先考虑反向边的改变,显然在撤回的时候,应当一并撤销对费用的影响,那么我们发现反向边的费用应当设置为正向边的相反数。
接着我们考虑,对于我们选出的增广路,每一条边都需要增加费用,对答案的贡献所有边费用的和乘上这条增广路的流量。我们发现对于一条费用和更小的增广路,我们肯定愿意它的流量更大,而为了上费用和越小的增广路对应的流量越大,这说明我们不能让其他增广路抢占它的流量,因此费用和越小的增广路要越早走,那么我们发现我们需要最短路,又因为反向边有负边权,因此需要用到 SPFA(是的,SPFA 活了)。当然如果你觉得 SPFA 实在过于拉跨,你也可以将每个边加上一个大常数让所有边边权为正,然后跑 dijkstra,但考场上没人卡 SPFA 费用流(别问,问就是江湖规矩)。并且 SPFA 同样适用于最大费用最大流,只需要将最短路改为最长路即可,而 dijkstra 无法跑最长路。
众所周知 SPFA 是 \(O(VE)\) 的,而我们可以搭配 EK 算法,由于 EK 算法因为负权边导致了最差会跑 \(VE\) 次 SPFA,所以上界复杂度是 \(O(V^2E^2)\)。此外还有一些优化,如消圈定理,但因为出题人基本不会选择卡掉 EK+SPFA 的费用流,所以已经足够了。可以学习 zkw 费用流从而得到一个依旧很松的 \(O(VE^2)\) 的上界。
另外一提,处理完之后依旧需要将每条边的流修改,并且 EK+SPFA 能够同时求解出最大流。
代码
bool SPFA(){
for(int i=1;i<=S;i++){
dis[i]=INF;
vis[i]=false;
}
queue<int>q;
q.push(s);
dis[s]=0;
inr[s]=INF;
vis[s]=true;
while(!q.empty()){
int x=q.front();
vis[x]=false;
q.pop();
for(int i=head[x];i;i=E[i].to){
int y=E[i].v,z=E[i].flow,w=E[i].dis;
if(z>0&&dis[x]+w<dis[y]){
dis[y]=dis[x]+w;
inr[y]=min(inr[x],z);
pre[y]=i;
if(!vis[y]){
vis[y]=true;
q.push(y);
}
}
}
}
return dis[t]<INF;
}
void MCMF(){
while(SPFA()){
MaxFlow+=inr[t];
MinCost+=inr[t]*dis[t];
int i;
for(int x=t;x!=s;x=E[i^1].v){
i=pre[x];
E[i].flow-=inr[t];
E[i^1].flow+=inr[t];
}
}
return ;
}
常用场景
费用流一般用于很明显的一些东西可以重复使用,每次使用有固定花费的题型,并且要求出入平衡。
上下界网络流
我们可以发现,普通的网络流只有上界,那么如果给每一条边要求一个下界,即每一条边的流量应当在上下届之间有该如何呢?
前言
事实上,我们也可以理解为,普通网络流是下界为 \(0\) 的上下届网络流。
无源汇上下界可行流
我们首先可以发现,对于一张每一条边都有上下届的网络来说,不一定存在合法的流,那么我们该如何判断这一点呢?
我们考虑到一个流可行的前提是所有边的流量均到达下界,我们可以先让流满足这一点,再考虑如何将流修改为出入平衡。那么我们首先将每一条边的流量提升至下界,很明显这样不一定满足出入平衡,有些点的入量会大于出量,有些点的出量会大于入量,很明显这些点的净流量加起来的和为 \(0\)。接着我们考虑如何修改,我们可以人为的给予出度不足的那些点相应的流量,让它们在不超出每条边上界的前提下将这些边流出,而所有入度不足的点,也应当有对应的流量流入。因此我们可以新建一个源点,连接所有出度不足的点,边权为它们对应所缺的流量,新建一个汇点,连接所有入度不足的点,边权为他们对应所却的流量,其余的点之间按照原图连边,边权为上下界之差。
如此一来,我们就可以通过流的形式将这些流量补回来,因为出度不足可以将源点给的流量运出去从而补齐出度,入度不足可以当运过来的流量接受从而补齐入度,而出入已经平衡的点可以直接将流量送出去,依旧不影响出入平衡。
接着考虑什么时候可行,什么时候不可行。显然,如果可行的话,所有从源点送出去的流量应当一个不落的回到汇点,因为所有点的净流量相加为 \(0\),但是可能因为有一些边不超过上界无法完全运输从而无法达到最大流。因此如果重新建好的图的最大流等于所有出度不足的点的净流量和,那么说明所有送出去的流量回到了汇点,也就是可行,反之则不可行。
如果可行的话,每条边的最终权值即为下界加上新建的图中对应边的流量。
代码(LOJ#115)
#include <bits/stdc++.h>
using namespace std;
const int N=200,M=10400;
int n,m,ans;
int flow[N+5],u[M+5],v[M+5],l[M+5],r[M+5];
struct NetFlow{
static const int N=202,M=10400,INF=2e9;
int s,t,S,cnt;
int head[N+5],dep[N+5],cnr[N+5];
struct Edge{
int v,w,to;
}e[(M<<1)+5];
void AddEdge(int x,int y,int z){
e[cnt].v=y;
e[cnt].w=z;
e[cnt].to=head[x];
head[x]=cnt++;
return ;
}
void AddNet(int x,int y,int z){
AddEdge(x,y,z);
AddEdge(y,x,0);
return ;
}
void Init(){
cnt=2;
s=n+1;
S=t=s+1;
for(int i=1;i<=S;i++)head[i]=0;
return ;
}
void Build(){
Init();
for(int i=1;i<=m;i++){
scanf("%d%d%d%d",&u[i],&v[i],&l[i],&r[i]);
flow[v[i]]+=l[i];
flow[u[i]]-=l[i];
AddNet(u[i],v[i],r[i]-l[i]);
}
for(int i=1;i<=n;i++){
if(flow[i]>0){
AddNet(s,i,flow[i]);
ans+=flow[i];
}else if(flow[i]<0)AddNet(i,t,-flow[i]);
}
return ;
}
bool Bfs(){
for(int i=1;i<=S;i++){
dep[i]=0;
cnr[i]=head[i];
}
queue<int>q;
q.push(s);
dep[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].to){
int y=e[i].v,z=e[i].w;
if(z>0&&dep[y]==0){
dep[y]=dep[x]+1;
if(y==t)return true;
q.push(y);
}
}
}
return false;
}
int Dfs(int x,int flow){
if(x==t)return flow;
int rest=flow;
for(int i=cnr[x];i&&rest;i=e[i].to){
int y=e[i].v,z=e[i].w;
if(z>0&&dep[y]==dep[x]+1){
int k=Dfs(y,min(rest,z));
rest-=k;
e[i].w-=k;
e[i^1].w+=k;
}
}
if(rest==flow)dep[x]=0;
return flow-rest;
}
int Dinic(){
int res=0;
while(Bfs())res+=Dfs(s,INF);
return res;
}
}Nf;
int main(){
scanf("%d%d",&n,&m);
Nf.Build();
if(ans==Nf.Dinic()){
printf("YES\n");
for(int i=1;i<=m;i++)printf("%d\n",l[i]+Nf.e[(i<<1)+1].w);
}else printf("NO");
return 0;
}
有源汇上下界可行流
事实上,有源汇上下界可行流和无源汇上下界可行流相差不大,我们只需要加入一条从汇点到源点的范围为 \([0,+\infin)\) 的边,就可以将其转化为无源汇上下界可行流,此时整张图的流量为新增的边的流量。这是因为汇点作为只入不出的点一定连接了新增的源点,而因为汇点只有这一条新增的边向外,因此所有点传向汇点的流量为了平衡都会传回源点。
代码
#include <bits/stdc++.h>
using namespace std;
const int N=200,M=10400;
int n,m;
int flow[N+5],u[M+5],v[M+5],l[M+5],r[M+5];
struct NetFlow{
static const int N=202,M=10400,INF=2e9;
int s,t,s2,t2,S,cnt,ans;
int head[N+5],dep[N+5],cnr[N+5];
struct Edge{
int v,w,to;
}e[(M<<1)+5];
void AddEdge(int x,int y,int z){
e[cnt].v=y;
e[cnt].w=z;
e[cnt].to=head[x];
head[x]=cnt++;
return ;
}
void AddNet(int x,int y,int z){
AddEdge(x,y,z);
AddEdge(y,x,0);
return ;
}
void Init(){
cnt=2;
s2=n+1;
S=t2=s2+1;
for(int i=1;i<=S;i++)head[i]=0;
return ;
}
void Build(){
Init();
for(int i=1;i<=m;i++){
scanf("%d%d%d%d",&u[i],&v[i],&l[i],&r[i]);
flow[v[i]]+=l[i];
flow[u[i]]-=l[i];
AddNet(u[i],v[i],r[i]-l[i]);
}
AddNet(t,s,INF);
for(int i=1;i<=n;i++){
if(flow[i]>0){
AddNet(s2,i,flow[i]);
ans+=flow[i];
}else if(flow[i]<0)AddNet(i,t2,-flow[i]);
}
return ;
}
bool Bfs(int s,int t){
for(int i=1;i<=S;i++){
dep[i]=0;
cnr[i]=head[i];
}
queue<int>q;
q.push(s);
dep[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].to){
int y=e[i].v,z=e[i].w;
if(z>0&&dep[y]==0){
dep[y]=dep[x]+1;
if(y==t)return true;
q.push(y);
}
}
}
return false;
}
int Dfs(int x,int flow,int t){
if(x==t)return flow;
int rest=flow;
for(int i=cnr[x];i&&rest;i=e[i].to){
cnr[x]=i;
int y=e[i].v,z=e[i].w;
if(z>0&&dep[y]==dep[x]+1){
int k=Dfs(y,min(rest,z),t);
rest-=k;
e[i].w-=k;
e[i^1].w+=k;
}
}
if(rest==flow)dep[x]=0;
return flow-rest;
}
void Dinic(){
int res=0;
while(Bfs(s2,t2))res+=Dfs(s2,INF,t2);
if(ans!=res)printf("No");
else{
ans=e[((m+1)<<1)+1].w;
printf("Yes\n%d",ans);
}
return ;
}
}Nf;
int main(){
scanf("%d%d%d%d",&n,&m,&Nf.s,&Nf.t);
Nf.Build();
Nf.Dinic();
return 0;
}
有源汇上下界最大流
首先,为了求解最大流,我们需要先求出一组可行流。因为可行流已经满足出入平衡,所以我们只需要在原图的残量网络上找到一组出入平衡的流即可,又为了使整张图的流量最大,我们就可以在原图上跑一遍最大流,然后将流量加上可行流的流量即为答案。
注意,因为只能在原图的边上跑最大流,所以需要屏蔽掉我们新增的边。事实上,因为新增的源点与汇点不连通,因此只需要删除从原汇点到原源点的边即可。
代码(LOJ#116)
#include <bits/stdc++.h>
using namespace std;
const int N=200,M=10400;
int n,m;
int flow[N+5],u[M+5],v[M+5],l[M+5],r[M+5];
struct NetFlow{
static const int N=202,M=10400,INF=2e9;
int s,t,s2,t2,S,cnt,ans;
int head[N+5],dep[N+5],cnr[N+5];
struct Edge{
int v,w,to;
}e[(M<<1)+5];
void AddEdge(int x,int y,int z){
e[cnt].v=y;
e[cnt].w=z;
e[cnt].to=head[x];
head[x]=cnt++;
return ;
}
void AddNet(int x,int y,int z){
AddEdge(x,y,z);
AddEdge(y,x,0);
return ;
}
void ClearEdge(int id){
e[id].v=e[id].w=0;
return ;
}
void ClearNet(int id){
ClearEdge(id);
ClearEdge(id^1);
return ;
}
void Init(){
cnt=2;
s2=n+1;
S=t2=s2+1;
for(int i=1;i<=S;i++)head[i]=0;
return ;
}
void Build(){
Init();
for(int i=1;i<=m;i++){
scanf("%d%d%d%d",&u[i],&v[i],&l[i],&r[i]);
flow[v[i]]+=l[i];
flow[u[i]]-=l[i];
AddNet(u[i],v[i],r[i]-l[i]);
}
AddNet(t,s,INF);
for(int i=1;i<=n;i++){
if(flow[i]>0){
AddNet(s2,i,flow[i]);
ans+=flow[i];
}else if(flow[i]<0)AddNet(i,t2,-flow[i]);
}
return ;
}
bool Bfs(int s,int t){
for(int i=1;i<=S;i++){
dep[i]=0;
cnr[i]=head[i];
}
queue<int>q;
q.push(s);
dep[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].to){
int y=e[i].v,z=e[i].w;
if(z>0&&dep[y]==0){
dep[y]=dep[x]+1;
if(y==t)return true;
q.push(y);
}
}
}
return false;
}
int Dfs(int x,int flow,int t){
if(x==t)return flow;
int rest=flow;
for(int i=cnr[x];i&&rest;i=e[i].to){
cnr[x]=i;
int y=e[i].v,z=e[i].w;
if(z>0&&dep[y]==dep[x]+1){
int k=Dfs(y,min(rest,z),t);
rest-=k;
e[i].w-=k;
e[i^1].w+=k;
}
}
if(rest==flow)dep[x]=0;
return flow-rest;
}
void Dinic(){
int res=0;
while(Bfs(s2,t2))res+=Dfs(s2,INF,t2);
if(ans!=res)printf("please go home to sleep");
else{
ans=e[((m+1)<<1)+1].w;
res=0;
ClearNet((m+1)<<1);
while(Bfs(s,t))res+=Dfs(s,INF,t);
ans+=res;
printf("%d",ans);
}
return ;
}
}Nf;
int main(){
scanf("%d%d%d%d",&n,&m,&Nf.s,&Nf.t);
Nf.Build();
Nf.Dinic();
return 0;
}
有源汇上下界最小流
事实上,因为每条边都有下界,并且不一定可行,因此求解最小流便有了意义。具体的方法思路和有源汇上下界最大流相似,只不过我们考虑将可行流多余的部分退回,也就是从源汇点到原源点跑一遍最大流,用可行流的流量减去这个最大流即可。
代码(LOJ#117)
#include <bits/stdc++.h>
using namespace std;
int n,m;
struct NetFlow{
static const int N=50005,M=175007,INF=2147483647;
int s1,s2,t1,t2,S,cnt,ans;
int head[N+5],dep[N+5],cnr[N+5],flow[N+5];
struct Edge{
int v,to,w;
}e[(M<<1)+5];
void AddEdge(int x,int y,int z){
e[cnt].v=y;
e[cnt].w=z;
e[cnt].to=head[x];
head[x]=cnt++;
return ;
}
void AddNet(int x,int y,int z){
AddEdge(x,y,z);
AddEdge(y,x,0);
return ;
}
void ClearEdge(int id){
e[id].v=e[id].w=0;
return ;
}
void ClearNet(int id){
ClearEdge(id);
ClearEdge(id^1);
return ;
}
void Init(){
cnt=2;
scanf("%d%d",&s1,&t1);
s2=n+1;
S=t2=s2+1;
for(int i=1;i<=S;i++)head[i]=0;
return ;
}
void Build(){
Init();
int x,y,l,r;
AddNet(t1,s1,INF);
for(int i=1;i<=m;i++){
scanf("%d%d%d%d",&x,&y,&l,&r);
flow[x]-=l;
flow[y]+=l;
AddNet(x,y,r-l);
}
for(int i=1;i<=n;i++){
if(flow[i]>0){
AddNet(s2,i,flow[i]);
ans+=flow[i];
}else if(flow[i]<0)AddNet(i,t2,-flow[i]);
}
return ;
}
bool Bfs(int s,int t){
for(int i=1;i<=S;i++){
dep[i]=0;
cnr[i]=head[i];
}
queue<int>q;
q.push(s);
dep[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].to){
int y=e[i].v;
long long z=e[i].w;
if(z>0&&dep[y]==0){
dep[y]=dep[x]+1;
if(y==t)return true;
q.push(y);
}
}
}
return false;
}
int Dfs(int x,int flow,int t){
if(x==t)return flow;
int rest=flow;
for(int i=cnr[x];i&&rest;i=e[i].to){
cnr[x]=i;
int y=e[i].v,z=e[i].w;
if(z>0&&dep[y]==dep[x]+1){
int k=Dfs(y,min(rest,z),t);
rest-=k;
e[i].w-=k;
e[i^1].w+=k;
}
}
if(flow==rest)dep[x]=0;
return flow-rest;
}
void Dinic(){
int res=0;
while(Bfs(s2,t2))res+=Dfs(s2,INF,t2);
if(ans!=res)printf("please go home to sleep");
else{
ans=e[3].w;
res=0;
ClearNet(2);
while(Bfs(t1,s1))res+=Dfs(t1,INF,s1);
ans-=res;
printf("%d",ans);
}
return ;
}
}Nf;
int main(){
scanf("%d%d",&n,&m);
Nf.Build();
Nf.Dinic();
return 0;
}
有源汇上下界最小(大)费用可行流
考虑每一条边不止有上下界,还有单位流量的费用,那么能否求出这个网络可行流的最小费用呢?答案是可以的。相似的,所有边到达下界的流量所需的费用是一定需要的,可以提前加入到答案里。接下来我们只需要让流变得可行即可,同时为了让总费用最小,我们将最大流改为最小费用最大流即可。
代码(P1229. [AHOI2014] 支线剧情)
#include <bits/stdc++.h>
using namespace std;
int n;
struct NetFlow{
static const int N=303,M=45451,INF=2e9;
int s1,s2,t1,t2,S,cnt,ans;
int head[N+5],dis[N+5],inr[N+5],pre[N+5],flow[N+5];
bool vis[N+5];
struct Edge{
int v,to,dis,flow;
}e[(M<<1)+5];
void AddEdge(int x,int y,int z,int w){
e[cnt].v=y;
e[cnt].flow=z;
e[cnt].dis=w;
e[cnt].to=head[x];
head[x]=cnt++;
return ;
}
void AddNet(int x,int y,int z,int w){
AddEdge(x,y,z,w);
AddEdge(y,x,0,-w);
return ;
}
void Init(){
cnt=2;
s1=1;
t1=n+1;
s2=t1+1;
S=t2=s2+1;
for(int i=1;i<=S;i++)head[i]=0;
return ;
}
void Build(){
Init();
int k,b,t;
AddNet(t1,s1,INF,0);
for(int i=1;i<=n;i++){
scanf("%d",&k);
for(int j=1;j<=k;j++){
scanf("%d%d",&b,&t);
AddNet(i,b,INF-1,t);
flow[i]--;
flow[b]++;
ans+=t;
}
AddNet(i,t1,INF,0);
}
for(int i=1;i<=t1;i++){
if(flow[i]>0)AddNet(s2,i,flow[i],0);
else if(flow[i]<0)AddNet(i,t2,-flow[i],0);
}
return ;
}
bool SPFA(int s,int t){
for(int i=1;i<=S;i++){
dis[i]=INF;
vis[i]=false;
}
queue<int>q;
q.push(s);
dis[s]=0;
vis[s]=true;
inr[s]=INF;
while(!q.empty()){
int x=q.front();
q.pop();
vis[x]=false;
for(int i=head[x];i;i=e[i].to){
int y=e[i].v,z=e[i].flow,w=e[i].dis;
if(z>0&&dis[x]+w<dis[y]){
dis[y]=dis[x]+w;
pre[y]=i;
inr[y]=min(inr[x],z);
if(!vis[y]){
vis[y]=true;
q.push(y);
}
}
}
}
return dis[t]<INF;
}
void MCMF(){
while(SPFA(s2,t2)){
ans+=dis[t2]*inr[t2];
int i;
for(int x=t2;x!=s2;x=e[i^1].v){
i=pre[x];
e[i].flow-=inr[t2];
e[i^1].flow+=inr[t2];
}
}
return ;
}
}Nf;
int main(){
scanf("%d",&n);
Nf.Build();
Nf.MCMF();
printf("%d",Nf.ans);
return 0;
}