网络流基础
网络流。全是板子,没有应用。
网络流
什么是网络流?网络瘤
(以下定义复制自oiwiki)
网络是指一个有向图 \(G=(V,E)\) ,每条边 \((u,v)\in E\) 都有一个权值 \(c(u,v)\) ,称为容量。其中有两个特殊的点:源点 \(s\in V\) ,汇点 \(t\in V\) 。
流函数 \(f(u,v)\) 是满足下面三个条件的实数函数:
- 容量限制:对于每条边,流经该边的流量不得超过边的容量,即 \(f(u,v)\le c(u,v)\)。
- 斜对称性:每条边的流量与其相反边的流量和为 \(0\),即 \(f(u,v)=-f(v,u)\)。
- 流守恒性:流出的流量等于流入的流量,即 \(\forall x\in V-{s,t},\sum_{(u,x)\in E}f(u,x)=\sum_{(x,v)\in E}f(x,v)\)。
整张网络的流量为源点流出的流量之和(或者说汇点流入的流量和)。其实可以把网络看作水流,从源点往外流水,历经整张图流向汇点。
最大流
给你一个网络,让你求源点流向汇点的最大流量。
残量网络
剩余容量: \((u,v)\) 的剩余容量为 \(c(u,v)-f(u,v)\)。
残量网络就是原图中所有点和所有剩余容量大于 \(0\) 的边所组成的子图。
增广路
增广路是从源点到汇点的一条路,路上所有的边都没有满流(剩余容量都大于 \(0\))。显然我们只需要每次找增广路就可以使流量变大。每次找到增广路,把最大流加上这条路的最小容量,把这些边的容量减去这个值,也就是更新了残量网络。
注意我们每次找到增广路之后,不仅正向边的流量增大,而且反向边的流量要减小。也就是每次找到一个流 \(c\) ,我们要使 \(f(u,v)+c,f(v,u)-c\) ,也就是 \(c(u,v)-c,c(v,u)+c\)。这个操作的另一个理解是流过反向边就相当于反悔。
反证法可以证明只要不断找增广路就可以得到最大流。于是我们有以下的算法来解决最大流问题。
Ford-Fulkerson 增广路算法
很简单,直接 dfs 找增广路。由于最大流有限,而每次都会增大流,所以复杂度是 \(O(m*值域)\) 的。
看起来没什么用。但是我们常用的一些算法都是基于它的。
Edmonds-Karp 算法
改用 bfs 找增广路,每次找到最小的一条增广。
这时候复杂度突然就值域无关了,变成了 \(O(nm^2)\)。每次 bfs 的复杂度是 \(O(m)\) ,增广路长度是 \(n\) ,最多有 \(m\) 条增广路(因为两次增广的瓶颈路一定不同)。
Dinic 算法
EK 每次即使搜到了若干增广路也只能增广一条路,效率很低。Dinic 算法通过多路增广来降低复杂度。
首先 bfs ,对残量网络分层并建立 DAG 。然后只走 DAG 上的边 dfs (这个可以通过记录一个 dis 来实现),多路增广,找不到时再次分层。
一次多路增广可以找到当前DAG中的所有增广路,复杂度 \(O(nm)\) ,分 \(n\) 次层,总复杂度 \(O(n^2m)\)。当然,为了达到这个复杂度,我们需要加个优化:当前弧优化。
当前弧优化,就是记录每个点走过了哪些没有流量的边,下次来直接忽略。实现直接把邻接表加个取引用就行。
还有一些其他优化:
- 点优化:加入一个点流不出流量,就把 \(dis\) 变成 \(-1\) ,这样再也不会过来了。
- 不完全 bfs 优化:每次 bfs 的时候找到汇点直接退出,因为后面的路径一定更长。
注意,网络流算法的复杂度都是不紧的。举个例子,dinic跑洛谷板子的 \(n=200,m=5000\) 最大的点只需要15ms。(所以如果你都决定写网络流了还管复杂度干啥)
下面是一份dinic的板子。可以通过P3376 【模板】网络最大流
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <queue>
#define int long long
using namespace std;
struct node{
int v,w,next;
}edge[100010];
int n,m,ans,s,t,tot=1,head[210],head2[210],dis[210];
void add(int u,int v,int w){
edge[++tot].v=v;edge[tot].w=w;edge[tot].next=head[u];head[u]=tot;
}
queue<int>q;
bool bfs(int st){
for(int i=1;i<=n;i++)head2[i]=head[i],dis[i]=0;
q.push(st);dis[st]=1;
while(!q.empty()){
int x=q.front();q.pop();
for(int i=head[x];i;i=edge[i].next){
if(edge[i].w&&!dis[edge[i].v]){
dis[edge[i].v]=dis[x]+1;
if(edge[i].v==t){
while(!q.empty())q.pop();
return true;
}
q.push(edge[i].v);
}
}
}
return false;
}
int dfs(int x,int flow){
if(x==t)return flow;
int sum=0;
for(int &i=head2[x];i;i=edge[i].next){
if(dis[edge[i].v]==dis[x]+1&&edge[i].w){
int ret=dfs(edge[i].v,min(flow,edge[i].w));
if(ret){
edge[i].w-=ret;edge[i^1].w+=ret;
sum+=ret;flow-=ret;
if(!flow)return sum;
}
else dis[edge[i].v]=-1;
}
}
return sum;
}
signed main(){
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);
add(u,v,w);add(v,u,0);
}
while(bfs(s))ans+=dfs(s,2147483647);
printf("%lld\n",ans);
return 0;
}
最小割
割:对一个网络流图,删掉一些边使得源点无法到达汇点,这些边就是一组割。边的容量和最小的为最小割。
最大流最小割定理:最大流=最小割。
最小割的构造:跑完最大流之后,从源点选没有满流的边(非 \(0\) 边)bfs,然后所有一端 bfs 到一遍 bfs 不到的边就是最小割。
费用流
费用流就是每条边有一个单位费用 \(w(u,v)\) ,当 \((u,v)\) 的流量为 \(f(u,v)\) 时,费用为 \(f(u,v)\times w(u,v)\),让你在最大流的前提下最大/小化费用,叫做最大/小费用最大流。
SSP 算法
一种贪心的算法,每次找到单位费用最小的增广路增广。它无法处理带负环的费用流。
实现很简单。首先把bfs换成spfa。然后如果你用EK,该怎么写怎么写,记录一个路上的费用就行了。如果你用dinic,把当前弧优化改成打visit标记就行了。
我写的dinic。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <queue>
using namespace std;
struct node{
int v,w,val,next;
}edge[120010];
int n,m,e,s,t,cost,tot=1,head[5010],dis[5010];
bool v[5010];
void add(int u,int v,int w,int val){
edge[++tot].v=v;edge[tot].w=w;edge[tot].next=head[u];head[u]=tot;
edge[tot].val=val;
}
queue<int>q;
bool spfa(int st){
memset(dis+1,0x3f,n*sizeof(int));
q.push(st);dis[st]=0;
while(!q.empty()){
int x=q.front();q.pop();v[x]=false;
for(int i=head[x];i;i=edge[i].next){
if(edge[i].w&&dis[edge[i].v]>dis[x]+edge[i].val){
dis[edge[i].v]=dis[x]+edge[i].val;
if(!v[edge[i].v]){
q.push(edge[i].v);v[edge[i].v]=true;
}
}
}
}
return dis[t]!=0x3f3f3f3f;
}
int dfs(int x,int flow){
if(x==t)return flow;
v[x]=true;
int sum=0;
for(int i=head[x];i;i=edge[i].next){
if(!v[edge[i].v]&&edge[i].w&&dis[edge[i].v]==dis[x]+edge[i].val){
int ret=dfs(edge[i].v,min(edge[i].w,flow));
if(ret){
cost+=ret*edge[i].val;
edge[i].w-=ret;edge[i^1].w+=ret;
sum+=ret;flow-=ret;
if(!flow)break;
}
else dis[edge[i].v]=-1;
}
}
v[x]=false;
return sum;
}
int read(){
int x=0;char ch=getchar();
while(!isdigit(ch))ch=getchar();
while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
return x;
}
int main(){
n=read();m=read();s=read();t=read();
for(int i=1;i<=m;i++){
int u=read(),v=read(),w=read(),val=read();
add(u,v,w,val);add(v,u,0,-val);
}
int ans=0;
while(spfa(s))ans+=dfs(s,0x3f3f3f3f);
printf("%d %d\n",ans,cost);
return 0;
}
上下界网络流
无源汇上下界可行流
上界可以是我们平时熟悉的容量。问题就是怎么把下界去掉。
可以把每条边的容量当作上界-下界,也就是默认每条边初始流量为下界。
当然这样可能不满足流量守恒。这个好搞,新建虚拟源点和汇点,对每个点记录流入和流出的流量,如果流入少,那么把虚拟源点和它连,反之它连到虚拟汇点。跑最大流,如果所有源点的出边不全满流则不可行。
有源汇上下界可行流
从汇点到源点连一条容量 \(\infty\) 的边就变成了无源汇上下界可行流。
有源汇上下界最大流
我们在可行流的基础上调整。把从汇点连到源点的边删掉,再跑一遍最大流就行了。
有源汇上下界最小流
和上面类似,就是把源汇点倒过来。
下面的代码可以通过P5192 Zoj3229 Shoot the Bullet|东方文花帖|【模板】有源汇上下界最大流。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <queue>
using namespace std;
const int inf=0x3f3f3f3f;
struct node{
int v,w,next;
}edge[1000010];
int n,m,ans,S,T,tot=1,head[1000010],head2[1000010],dis[1000010];
void add(int u,int v,int w){
edge[++tot].v=v;edge[tot].w=w;edge[tot].next=head[u];head[u]=tot;
}
queue<int>q;
bool bfs(int st){
for(int i=1;i<=T;i++)head2[i]=head[i],dis[i]=0;
q.push(st);dis[st]=1;
while(!q.empty()){
int x=q.front();q.pop();
for(int i=head[x];i;i=edge[i].next){
if(edge[i].w&&!dis[edge[i].v]){
dis[edge[i].v]=dis[x]+1;
if(edge[i].v==T){
while(!q.empty())q.pop();
return true;
}
q.push(edge[i].v);
}
}
}
return false;
}
int dfs(int x,int flow){
if(x==T)return flow;
int sum=0;
for(int &i=head2[x];i;i=edge[i].next){
if(dis[edge[i].v]==dis[x]+1&&edge[i].w){
int ret=dfs(edge[i].v,min(flow,edge[i].w));
if(ret){
edge[i].w-=ret;edge[i^1].w+=ret;
sum+=ret;flow-=ret;
if(!flow)return sum;
}
else dis[edge[i].v]=-1;
}
}
return sum;
}
int dinic(){
int ans=0;
while(bfs(S))ans+=dfs(S,0x3f3f3f3f);
return ans;
}
int in[1000010],out[1000010];
int main(){
while(~scanf("%d%d",&n,&m)){
tot=1;int sum=0;
memset(head,0,sizeof(head));
memset(in,0,sizeof(in));
memset(out,0,sizeof(out));
int s1=n+m+1,t1=n+m+2;
S=n+m+3,T=n+m+4;
for(int i=1;i<=m;i++){
int x;scanf("%d",&x);
in[t1]+=x;out[n+i]+=x;
add(n+i,t1,inf-x);add(t1,n+i,0);
}
for(int i=1;i<=n;i++){
int c,d;scanf("%d%d",&c,&d);
add(s1,i,d);add(i,s1,0);
for(int j=1;j<=c;j++){
int t,l,r;scanf("%d%d%d",&t,&l,&r);t++;
in[n+t]+=l;out[i]+=l;
add(i,n+t,r-l);add(n+t,i,0);
}
}
for(int i=1;i<=n+m+2;i++){
if(in[i]<=out[i]){
add(i,T,out[i]-in[i]);add(T,i,0);
}
else{
sum+=in[i]-out[i];
add(S,i,in[i]-out[i]);add(i,S,0);
}
}
add(t1,s1,inf);add(s1,t1,0);
if(dinic()!=sum){
puts("-1\n");continue;
}
ans=edge[tot].w;
edge[tot].w=edge[tot^1].w=0;
S=s1;T=t1;
ans+=dinic();
printf("%d\n\n",ans);
}
return 0;
}
有负圈的费用流
可以借鉴上下界网络流的思想。
遇到负权边,给它强制满流,然后加一条反向边,边权是它的相反数。也就是把原来的边 \((u,v,[0,f],w)\) 换成两条边 \((u,v,[f,f],w),(v,u,[0,f],-w)\) 。这样就没有负权边了。
然后我们这样做仍然是默认初始的一些边满流。仿照上下界的方式处理即可。
同样上一份板子代码。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <queue>
using namespace std;
const int inf=0x3f3f3f3f;
struct node{
int v,w,val,next;
}edge[120010];
int n,m,e,S,T,ans,cost,tot=1,head[5010],dis[5010];
bool v[5010];
void add(int u,int v,int w,int val){
edge[++tot].v=v;edge[tot].w=w;edge[tot].next=head[u];head[u]=tot;
edge[tot].val=val;
}
queue<int>q;
bool spfa(int st){
memset(dis,0x3f,sizeof(dis));
q.push(st);dis[st]=0;
while(!q.empty()){
int x=q.front();q.pop();v[x]=false;
for(int i=head[x];i;i=edge[i].next){
if(edge[i].w&&dis[edge[i].v]>dis[x]+edge[i].val){
dis[edge[i].v]=dis[x]+edge[i].val;
if(!v[edge[i].v]){
q.push(edge[i].v);v[edge[i].v]=true;
}
}
}
}
return dis[T]!=0x3f3f3f3f;
}
int dfs(int x,int flow){
if(x==T)return flow;
v[x]=true;
int sum=0;
for(int i=head[x];i;i=edge[i].next){
if(!v[edge[i].v]&&edge[i].w&&dis[edge[i].v]==dis[x]+edge[i].val){
int ret=dfs(edge[i].v,min(edge[i].w,flow));
if(ret){
cost+=ret*edge[i].val;
edge[i].w-=ret;edge[i^1].w+=ret;
sum+=ret;flow-=ret;
if(!flow)break;
}
else dis[edge[i].v]=-1;
}
}
v[x]=false;
return sum;
}
int dinic(){
int ans=0;
while(spfa(S))ans+=dfs(S,0x3f3f3f3f);
return ans;
}
int in[1000010],out[1000010];
int main(){
int s,t;
scanf("%d%d%d%d",&n,&m,&s,&t);T=n+1;
for(int i=1;i<=m;i++){
int u,v,w,val;scanf("%d%d%d%d",&u,&v,&w,&val);
if(val<0){
in[v]+=w;out[u]+=w;cost+=val*w;
add(v,u,w,-val);add(u,v,0,val);
}
else{
add(u,v,w,val);add(v,u,0,-val);
}
}
for(int i=1;i<=n;i++){
if(in[i]<=out[i]){
add(i,T,out[i]-in[i],0);add(T,i,0,0);
}
else{
add(S,i,in[i]-out[i],0);add(i,S,0,0);
}
}
add(t,s,inf,0);add(s,t,0,0);
dinic();
ans=edge[tot].w;
edge[tot].w=edge[tot^1].w=0;
S=s;T=t;
ans+=dinic();
printf("%d %d\n",ans,cost);
return 0;
}
最小割树
题意:多次询问,每次求两点间最小割。
构造原理很简单,每次在图上随便找两个点跑最小割,这样原图就被分成了两部分。在最小割树上添加这两个点之间的边,权值为其最小割。然后往两边分治即可。复杂度 \(O(n^3m)\),也就是最坏情况下分治 \(n\) 次 dinic。
它的性质是两点的最小割等于树上两点间最小边权。
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
struct node{
int v,w,next;
}edge[6010];
int n,m,num,S,T,tot=1,head[510],dis[510],head2[510];
void add(int u,int v,int w){
edge[++tot].v=v;edge[tot].w=w;edge[tot].next=head[u];head[u]=tot;
}
queue<int>q;
bool bfs(int st){
for(int i=0;i<=n;i++)head2[i]=head[i],dis[i]=0;
q.push(st);dis[st]=1;
while(!q.empty()){
int x=q.front();q.pop();
for(int i=head[x];i;i=edge[i].next){
if(edge[i].w&&!dis[edge[i].v]){
dis[edge[i].v]=dis[x]+1;
if(edge[i].v==T){
while(!q.empty())q.pop();
return true;
}
q.push(edge[i].v);
}
}
}
return false;
}
int dfs(int x,int flow){
if(x==T)return flow;
int sum=0;
for(int &i=head2[x];i;i=edge[i].next){
if(edge[i].w&&dis[edge[i].v]==dis[x]+1){
int ret=dfs(edge[i].v,min(flow,edge[i].w));
if(ret){
edge[i].w-=ret;edge[i^1].w+=ret;
flow-=ret;sum+=ret;
if(!flow)break;
}
else dis[edge[i].v]=-1;
}
}
return sum;
}
int ans[510][510],node[510],tmp1[510],tmp2[510];
int dinic(){
for(int i=2;i<=tot;i+=2)edge[i].w+=edge[i^1].w,edge[i^1].w=0;
int ans=0;
while(bfs(S))ans+=dfs(S,0x3f3f3f3f);
return ans;
}
void build(int l,int r){
if(l==r)return;
S=node[l],T=node[l+1];
int val=dinic(),s=node[l],t=node[l+1];
ans[S][T]=ans[T][S]=val;
int cnt1=0,cnt2=0;
for(int i=l;i<=r;i++){
if(dis[node[i]])tmp1[++cnt1]=node[i];
else tmp2[++cnt2]=node[i];
}
for(int i=1;i<=cnt1;i++)node[i+l-1]=tmp1[i];
for(int i=1;i<=cnt2;i++)node[cnt1+i+l-1]=tmp2[i];
build(l,l+cnt1-1);build(l+cnt1,r);
for(int i=1;i<=cnt1;i++){
for(int j=1;j<=cnt2;j++){
int x=node[i+l-1],y=node[j+cnt1+l-1];
ans[x][y]=ans[y][x]=min(min(ans[x][s],ans[t][y]),ans[s][t]);
}
}
}
int main(){
memset(ans,0x3f,sizeof(ans));
scanf("%d%d",&n,&m);n++;
for(int i=1;i<=m;i++){
int u,v,w;scanf("%d%d%d",&u,&v,&w);
u++;v++;
add(u,v,w);add(v,u,0);
add(v,u,w);add(u,v,0);
}
for(int i=1;i<=n;i++)node[i]=i;
build(1,n);
int q;scanf("%d",&q);
while(q--){
int x,y;scanf("%d%d",&x,&y);
x++;y++;
printf("%d\n",ans[x][y]);
}
return 0;
}