网络流学习笔记

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;
}
posted @ 2023-09-30 19:09  lhc0707  阅读(13)  评论(0编辑  收藏  举报