网络流基础

网络流。全是板子,没有应用。

网络流

什么是网络流?网络瘤
(以下定义复制自oiwiki)
网络是指一个有向图 \(G=(V,E)\) ,每条边 \((u,v)\in E\) 都有一个权值 \(c(u,v)\) ,称为容量。其中有两个特殊的点:源点 \(s\in V\) ,汇点 \(t\in V\)
流函数 \(f(u,v)\) 是满足下面三个条件的实数函数:

  1. 容量限制:对于每条边,流经该边的流量不得超过边的容量,即 \(f(u,v)\le c(u,v)\)
  2. 斜对称性:每条边的流量与其相反边的流量和为 \(0\),即 \(f(u,v)=-f(v,u)\)
  3. 流守恒性:流出的流量等于流入的流量,即 \(\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)\)。当然,为了达到这个复杂度,我们需要加个优化:当前弧优化。

当前弧优化,就是记录每个点走过了哪些没有流量的边,下次来直接忽略。实现直接把邻接表加个取引用就行。

还有一些其他优化:

  1. 点优化:加入一个点流不出流量,就把 \(dis\) 变成 \(-1\) ,这样再也不会过来了。
  2. 不完全 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;
}
posted @ 2022-12-10 17:24  gtm1514  阅读(71)  评论(0编辑  收藏  举报