网络流学习笔记

网络流基础

基本概念

  • 源点(source) s s s,汇点 t t t

  • 容量:约等于边权。不存在的边流量可视为 0 0 0 ( u , v ) (u,v) (u,v) 的流量通常记为 c ( u , v ) c(u,v) c(u,v)(capacity)。

  • 流(flow):每条边上的流不能超过它的容量,注意这个限制是所有经过路径的边共享的。除了源点和汇点,其他所有点流入的流量都等于流出的流量。通常用 f f f 表示。

  • 割:把结点分成两部分 { S , T } \{S,T\} {S,T},且满足 s ∈ S , t ∈ T s\in S,t\in T sS,tT { S , T } \{S,T\} {S,T} 是图的一个 s s s- t t t 割, s s s- t t t { S , T } \{S,T\} {S,T} 的容量为 ∑ u ∈ S ∑ v ∈ T c ( u , v ) \sum\limits_{u\in S}\sum\limits_{v\in T}c(u,v) uSvTc(u,v)

  • 残留网络:有源点、汇点,且每条边都有残留容量的网络。

  • 增广路:从残留网络的源点到汇点的路径。对于增广路,给每一条边都加上等量流量,此过程称为增广。

常见问题

  • 最大流问题:给定每条边的流量,求得尽可能大的流量。
  • 最小割问题:给定每条边的流量,求一个容量尽可能大的 s s s- t t t { S , T } \{S,T\} {S,T}
  • 最小费用最大流问题:给定每条边的流量和权值(费用),求对于所有可能的最大流,费用最小的一个。
  • 上下界网络流问题:给定每条边的流量上界和下界,求一种可行的流使得满足限制。

最大流问题

以下代码均为 P3376 【模板】网络最大流 代码。

先介绍一种思想——Ford–Fulkerson 增广(FF 增广)。即不断在残留网络中找一条增广路,向汇点发送可能的最大流量,得到新的残留网络,不断寻找增广路,直到没有增广路为止。此时有最大流。

在 FF 增广的过程中,为了保证正确性,我们要引入反向边。对于每一条边 ( u , v ) (u,v) (u,v),建一条 c ( v , u ) = 0 c(v,u)=0 c(v,u)=0 的反向边。反向边其实相当于一种撤回操作,因此在增广的过程中,给正向边减去流量的同时要给反向边加上流量。

网络流学习笔记_第1张图片

反向边的“抵消”操作使得在错误的增广路选择顺序下也可以得到正确答案。

FF 增广的时间复杂度为 O ( E f max ⁡ ) O(Ef_{\max}) O(Efmax)

EK 算法

通过 BFS 实现的 FF 增广过程。最坏时间复杂度为 O ( V E 2 ) O(VE^2) O(VE2),一般可以处理 1 0 4 10^4 104 规模的网络。

注意这里的链前 cnt 初始值要设定为 1 1 1,方便通过异或操作查找反向边(这样可以使第一条边的编号为偶数, 2 n ⊕ 1 = 2 n + 1 , ( 2 n + 1 ) ⊕ 1 = 2 n 2n \oplus 1=2n+1,(2n+1)\oplus 1=2n 2n1=2n+1,(2n+1)1=2n)。用 pre 数组记录当前的增广路,flow 数组记录当前增广路上的流量。

#include 
using namespace std;
typedef long long ll;

const ll maxn=5005;
int n,m,s,t,cnt=1,head[maxn],pre[maxn];
ll flow[maxn]/*记录到x的最大可行流*/,ans;
struct edge{int to,nxt;ll c;}e[maxn*2];
bool vis[maxn],flag[205][205];

void add(int x,int y,ll z){e[++cnt]={y,head[x],z},head[x]=cnt;}

bool bfs()
{
    for(int i=1;i<=n;i++) vis[i]=0;
    queue<int> q;
    vis[s]=1,q.push(s),flow[s]=LLONG_MAX;
    while(!q.empty())
    {
        int x=q.front();q.pop();
        for(int i=head[x];i;i=e[i].nxt)
        {
            if(!e[i].c) continue;
            if(vis[e[i].to]) continue;
            flow[e[i].to]=min(flow[x],e[i].c),pre[e[i].to]=i,q.push(e[i].to),vis[e[i].to]=1;
            if(e[i].to==t) return 1;
        }
    }
    return 0;
}

void ek()
{
    int x=t;
    while(x!=s) e[pre[x]].c-=flow[t],e[pre[x]^1].c+=flow[t],x=e[pre[x]^1].to;
    ans+=flow[t];
}

int main()
{
    cin>>n>>m>>s>>t;
	for(int i=1,u,v,w;i<=m;i++) 
	    cin>>u>>v>>w,add(u,v,w),add(v,u,0);
	while(bfs()) ek();
	cout<<ans;
	return 0;
}

Dinic 算法

先通过 BFS,把图根据结点到源点的距离分层,只按照层数递增的方向增广。注意每次增广后都要重新将图分层。

为了保证 Dinic 算法的时间复杂度正确性,我们需要引入当前弧优化。如果一条边 ( u , v ) (u,v) (u,v) 的容量已经用完,或 v v v 的后侧已经增广至阻塞,则 u u u 的流量无需流向出边 ( u , v ) (u,v) (u,v)。对于每个结点,维护它的出边中第一个需要尝试流出的出边。维护的这个指针称为当前弧。由于我们的边是顺次遍历的,所以当遍历到第 i i i 条边时,前面的边一定已经不能继续流,直接修改新的当前弧 now[x]=i

还可以用多路增广的方法优化时间复杂度。在某点找到一条增广路后,如果还有剩余流量,继续从该点寻找增广路。

DFS 过程中,对于当前结点 x x x,它可以分给后面结点最多 f max ⁡ f_{\max} fmax 流量;对于当前访问的边 ( u , v ) (u,v) (u,v),分配的流量是最大流量与已经用的流量之差与边的容量取 min ⁡ \min min 的结果。

最坏时间复杂度为 O ( V 2 E ) O(V^2E) O(V2E)

#include 
using namespace std;
#define int long long
typedef long long ll;

const int maxn=5005;
int n,m,s,t,cnt=1,head[maxn],now[maxn];
ll flow[maxn],ans,dis[205];
struct edge{int to,nxt;ll c;}e[maxn*2];

void add(int x,int y,ll z){e[++cnt]={y,head[x],z},head[x]=cnt;}

bool bfs()
{
    for(int i=1;i<=n;i++) dis[i]=LLONG_MAX;
    queue<int> q;
    q.push(s);dis[s]=0,now[s]=head[s];
    while(!q.empty())
    {
        int x=q.front();q.pop();
        for(int i=head[x];i;i=e[i].nxt)
            if(e[i].c>0&&dis[e[i].to]==LLONG_MAX)
            {
                q.push(e[i].to),now[e[i].to]=head[e[i].to],dis[e[i].to]=dis[x]+1;
                if(e[i].to==t) return 1;
            }
    }
    return 0;
}

int dfs(int x,ll mxf)//mxf是能给后面点分配的最大流量
{
    if(x==t) return mxf;
    ll sum=0;//sum是从x点实际分配出的流量
    for(int i=now[x];i;i=e[i].nxt)
    {
    	now[x]=i;
    	if(e[i].c>0&&dis[e[i].to]==dis[x]+1)
    	{
    		int ff=dfs(e[i].to,min(mxf-sum,e[i].c));
    		e[i].c-=ff,e[i^1].c+=ff,sum+=ff;
    		if(ff==mxf) return ff;
    	}
    }
    return sum;
}

signed main()
{
    cin>>n>>m>>s>>t;
	for(int i=1,u,v,w;i<=m;i++) 
	    cin>>u>>v>>w,add(u,v,w),add(v,u,0);
	while(bfs()) ans+=dfs(s,LLONG_MAX);
	cout<<ans;
	return 0;
}

最小费用最大流问题

( u , v ) (u,v) (u,v) 流量为 f ( u , v ) f(u,v) f(u,v) 时,花费的费用为 f ( u , v ) × w ( u , v ) f(u,v)\times w(u,v) f(u,v)×w(u,v),要求在最大化 ∑ ( u , v ) ∈ E f ( u , v ) \sum\limits_{(u,v)\in E}f(u,v) (u,v)Ef(u,v) 的情况下最小化 ∑ ( u , v ) ∈ E f ( u , v ) × w ( u , v ) \sum\limits_{(u,v)\in E} f(u,v)\times w(u,v) (u,v)Ef(u,v)×w(u,v),该问题即最小费用最大流问题。

SSP 算法

SSP(Successive Shortest Path)算法,思想是每次寻找费用最小的增广路进行增广,直到图上不存在增广路为止。

注意图中不能存在单位费用为负的圈。

具体实现就是把 EK/Dinic 算法中 BFS 找增广路的过程用 SPFA 代替,同时反向边的花费为负。时间复杂度 O ( V E f max ⁡ ) O(VEf_{\max}) O(VEfmax)

以下是基于 EK 算法的实现:

#include 
using namespace std;

const int maxn=5005,maxm=1e5+5;
struct edge{int to,nxt,c,w;}e[maxm];
int head[maxn],pre[maxn],dis[maxn],cnt=1,n,m,s,t,mxf,minc,flow[maxn];
bool vis[maxn];

void add(int x,int y,int z,int q){e[++cnt]=(edge){y,head[x],z,q},head[x]=cnt;}

bool spfa()
{
	queue<int> q;
	for(int i=1;i<=n;i++) dis[i]=INT_MAX,vis[i]=0;
	q.push(s),dis[s]=0,flow[s]=INT_MAX,vis[s]=1,pre[t]=-1;
	while(!q.empty())
	{
		int x=q.front();q.pop(),vis[x]=0;
		for(int i=head[x];i;i=e[i].nxt)
			if(e[i].c&&dis[e[i].to]>dis[x]+e[i].w)
			{
				dis[e[i].to]=dis[x]+e[i].w,pre[e[i].to]=i,flow[e[i].to]=min(flow[x],e[i].c);
				if(!vis[e[i].to]) q.push(e[i].to),vis[e[i].to]=1;
			}
	}
	return pre[t]!=-1;
}

void ek()
{
	while(spfa())
	{
		int x=t;
		mxf+=flow[t],minc+=flow[t]*dis[t];
		while(x!=s) e[pre[x]].c-=flow[t],e[pre[x]^1].c+=flow[t],x=e[pre[x]^1].to;
		// cout<
	}
}

int main()
{
	cin>>n>>m>>s>>t;
	for(int i=1,u,v,w,c;i<=m;i++) cin>>u>>v>>w>>c,add(u,v,w,c),add(v,u,0,-c);
	ek();
	cout<<mxf<<' '<<minc;
	return 0;
}

你可能感兴趣的:(#,图论,网络流)