【8】网络流学习笔记

前言

网络流是图论中博大精深的一个分支,我自己没有学得很精通,所以这篇博客只能讲一部分内容。

网络流算法本身不会被太多考察,重点还是在于建图的思维能力。

最大流


给出一个包含 n 个点和 m 条边的有向图(下面称其为网络) G=(V,E),该网络上所有点分别编号为 1n,所有边分别编号为 1m,其中该网络的源点为 s,汇点为 t,网络上的每条边 (u,v) 都有一个流量限制 w(u,v)

你需要给每条边 (u,v) 确定一个流量 f(u,v),要求:

  1. 0f(u,v)w(u,v)(每条边的流量不超过其流量限制);
  2. p{V{s,t}}(i,p)Ef(i,p)=(p,i)Ef(p,i)(除了源点和汇点外,其他各点流入的流量和流出的流量相等);
  3. (s,i)Ef(s,i)=(i,t)Ef(i,t)(源点流出的流量等于汇点流入的流量)。

定义网络 G 的流量 F(G)=(s,i)Ef(s,i)

你需要求出该网络的最大流,即使 F(G) 最大。


FF 算法

一切伟大的算法,都有一个微不足道的开始。

首先,我们考虑一种朴素的思路。使用 DFS 算法找到一条不包含边权为 0 的边的 st通路,根据每条边的流量不超过其流量限制,这条通路的流量不超过最小边的流量限制,为各边上边权的最小值。然后,将路径上的边边权减去这个这条通路的流量,表示用这条路流了这些流量。边权为 0 的边表示流满的边。重复这个过程,知道找不出符合条件的通路。

这个算法看起来没有什么问题,但是它是错的。在下面这个例子中,如果第一次的通路为 1234,最后求出的流量只有 1,但是这个图的最大流量显然为 2

解决这个问题的方法,是在流完之后路径上的边增加反向边,反向边边权为这次流的流量。感性理解一下,因为走反向边就相当于反悔贪心,能考虑到所有情况。如果情况不好,可以接着反悔回来,结果依旧是对的。

基于此,我们给出一些常用的定义:


残余网络:在一个网络流图上,找到一条源到汇的路径(即找到了一个流量)后,对路径上所有的边,其容量都减去此次找到的流量,对路径上所有的边,都添加一条反向边,其容量也等于此次找到的流量,这样得到的新图,就称为原图的残余网络

增广路径: 每次寻找新流量构造新残余网络的过程,就叫做寻找流量的增广路径,也叫增广


当然,这也带来了一些副作用,比如极高的时间复杂度。因为我们平时不会把这个算法写出来,所以代码和时间复杂度就不管了。

Edmonds-Karp算法(EK算法)

DFS 行不通,那就试试 BFS。

在寻找增广路径时,不一定要使用 DFS 算法,还可以使用BFS 算法。把每一次增广路的 DFS 改为 BFS,我们就得到了 Edmonds-Karp算法

这个算法在时间复杂度上有很大的进步,为 O(nm2),其中 n 为节点数,m 为边数,一般可以处理 103104 的网络。

Dinic算法

一次寻找多条增广路径,是一个优化方向。

DFS 算法有一个巨大的优势,就是可以回溯,借助回溯,就有可能实现一次寻找多条增广路。Dinic 算法就是使用了 BFS+DFS 达到了以上的思路,完成了算法复杂度的进一步下降。

每一次求增广路的过程分为以下 3 步:

1:使用 BFS 算法,对原图进行分层。一个点的层数便是它离源点最近距离

2:通过 DFS 算法寻找增广路,但是每一步必须走向下一层的节点

3:一次增广路求完之后,如果某条边尚未流满,则回溯到这条边的终点,向另一个下一层的节点继续搜索,直到这条边流满。

重复这个求增广路的过程,直到找不到增广路,也就是从源点无法到达汇点。

这样使时间复杂度得到了进一步优化,为 O(n2m),其中 n 为节点数,m 为边数,一般可以处理 104105 的网络。

Dinic 算法还可以进行当前弧优化。对于一个节点,当我们在 DFS 中遍历到第 i 条弧时,前 i1 条弧必定流满,下一次访问时可以直接跳过,对应到代码中,就是 pre 数组。

另外还有两个小剪枝,如果当前边已经流满直接返回,因为没办法增加流量了,继续遍历没有意义。

如果当前边没有任何流出流量,证明该节点增广完毕,显然不会对结果再有贡献,不用再次遍历。将层次标记为 0,下一次就不会搜索到这个点了。

在代码中使用了一种成对存储技术来存储每条边以及其反向边。第一条边的编号为 2,且每条边的反向边的编号比该边编号大 1。这样,无论是正向边 i 还是反向边 ii 异或 1 就是其反向边存储的位置。

代码中 now 表示当前点的编号,flow 表示剩余流量,out 表示流出流量。

bool bfs()
{
	memset(dis,0,sizeof(dis));
	l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
	while(l<=r)
	   {
	   	long long now=q[l];
	   	for(int i=h[now];i;i=e[i].next)
	   	    {
	   	    if(!dis[e[i].v]&&e[i].dis>0)
    	   	    {
    	   	    dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
    	   	    if(e[i].v==t)return 1;
    	   	    }
	   	    }
	   	l++;
	   }
	return 0;
}

long long dinic(long long now,long long flow)
{
	if(now==t)return flow;
	long long rest=flow,out=0;
	for(int i=pre[now];i;i=e[i].next)
	    {
	    pre[now]=i;
	    if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
	       {
	       	long long k=dinic(e[i].v,min(rest,e[i].dis));
	       	flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
	       	if(!flow)break;
		   }
	    }
	if(out==0)dis[now]=0;
	return out;
}
      
while(bfs())ans+=dinic(s,9999999999);

最小割


:对于一个网络流图 G=(V,E),一种点的划分方式把整个图划分为了 ST=VS 两个集合,且源点 sS汇点 tT,这种划分方式称作这个图的

割的容量:割 (S,T) 的容量 c(S,T) 表示离开 S 的边的权重之和

最小割:就是求得一个割 (S,T),使得割的容量 c(S,T) 最小


最大流最小割定理

在任何网络中,最大流的值等于最小割的容量

这里给出一个比较感性的证明:

考虑求最大流的过程,本质上就是寻找了多条 st通路并删去最小边的边权。由一个显然易见的贪心,最小割也是对于每一个st 的通路,找到通路上最小边删去。可见,这两个求法本身就是类似的。

对于有多条通路经过同一条边的情况,求最小割时有两种选择方式:割掉公共边和分别割掉每一条路径上的最小边,当然是哪种容量小选择哪种。最大流的求解恰好有两种对应的情况:公共边流满了每一条路径上的最小边流满了,根据最大流的定义,这两种肯定是取较小的,恰好符合求最小割的两种选择方式取较小值。

综上所述,经过感性理解,最大流的值等于最小割的容量。所以,以后碰到需要求最小割的题目时,直接求最大流即可

最小费用最大流


给出一个包含 n 个点和 m 条边的有向图(下面称其为网络) G=(V,E),该网络上所有点分别编号为 1n,所有边分别编号为 1m,其中该网络的源点为 s,汇点为 t,网络上的每条边 (u,v) 都有一个流量限制 w(u,v) 和单位流量的费用 c(u,v)

你需要给每条边 (u,v) 确定一个流量 f(u,v),要求:

  1. 0f(u,v)w(u,v)(每条边的流量不超过其流量限制);
  2. p{V{s,t}}(i,p)Ef(i,p)=(p,i)Ef(p,i)(除了源点和汇点外,其他各点流入的流量和流出的流量相等);
  3. (s,i)Ef(s,i)=(i,t)Ef(i,t)(源点流出的流量等于汇点流入的流量)。

定义网络 G 的流量 F(G)=(s,i)Ef(s,i),网络 G 的费用 C(G)=(i,j)Ef(i,j)×c(i,j)

你需要求出该网络的最小费用最大流,即在 F(G) 最大的前提下,使 C(G) 最小。


首先,既然要求出最大流,那么肯定考虑增广路。只要我们找到一条通路,就可以流一些流量(要建立反向边)。根据最大流算法,每次随便找一条通路,增加一些流量,直到没有通路为止

我们可以考虑每一次找一条特殊的通路,来达到费用最小。很显然,这条通路就是每次 st最短路。因为最后无论怎么流,总是可以流到最大流。那我们优先寻找费用低的通路,因为这会占用一些流量,就可以避免走一些费用高的通路。

每一次寻找增广路的时候,使用 SPFA 算法,把每条边的费用当作边权。记录每一个节点最短路上的前驱,求解完最短路后直接倒推处理流量。注意剩余流量为 0 边依旧不能走

不能使用 Dijistra,因为费用可能为负数。

bool spfa(long long s,long long t)
{
	for(int i=1;i<=n;i++)d[i]=(long long)9999999999,pre[i]=f[i]=vis[i]=0;
	hp=1,tp=0,q[++tp]=s,d[s]=0,f[s]=(long long)9999999999;
	while(hp<=tp)
	   {
	   	long long now=q[hp];
	   	vis[now]=0;
	   	for(int i=h[now];i;i=e[i].nxt)
	   	    {
	   	    if(e[i].d==0)continue;
	   	    if(d[e[i].v]>d[now]+e[i].x)
	   	       {
			   d[e[i].v]=d[now]+e[i].x,f[e[i].v]=min(f[now],e[i].d),pre[e[i].v]=i;
			   if(vis[e[i].v]==0)q[++tp]=e[i].v,vis[e[i].v]=1;
		       }
	   	    }
	    hp++;
	   }
	if(d[t]==(long long)9999999999)return 0;
	else return 1;
}

void mcmf(long long s,long long t)
{
	while(spfa(s,t))
	   {
	   	mf+=f[t],mc+=d[t]*f[t];
	   	long long now=t;
	   	while(now!=s)
	   	    {
	   	    e[pre[now]].d-=f[t],e[pre[now]^1].d+=f[t];
	   	    now=e[pre[now]^1].v;
			}
	   }
}

例题

例题 1

P3376 【模板】网络最大流

几乎是双倍经验(我也不知道为什么我写的两份代码不一样)

P2740 [USACO4.2] 草地排水Drainage Ditches

最大流模板题,不多赘述。

P3376

#include <bits/stdc++.h>
using namespace std;
struct edge
{
	long long v,next,dis;
}e[12000];
long long n,m,s,t,u,v,d,h[300],q[300],dis[300],pre[300],l,r,flow=0,ans=0,cnt=1;
void add_edge(long long u,long long v,long long dis)
{
	e[++cnt].next=h[u];
	e[cnt].v=v;
	e[cnt].dis=dis;
	h[u]=cnt;
}

bool bfs()
{
	memset(dis,0,sizeof(dis));
	l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
	while(l<=r)
	   {
	   	long long now=q[l];
	   	for(int i=h[now];i;i=e[i].next)
	   	    {
	   	    if(!dis[e[i].v]&&e[i].dis>0)
    	   	    {
    	   	    dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
    	   	    if(e[i].v==t)return 1;
    	   	    }
	   	    }
	   	l++;
	   }
	return 0;
}

long long dinic(long long now,long long flow)
{
	if(now==t)return flow;
	long long rest=flow,out=0;
	for(int i=pre[now];i;i=e[i].next)
	    {
	    pre[now]=i;
	    if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
	       {
	       	long long k=dinic(e[i].v,min(rest,e[i].dis));
	       	flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
	       	if(!flow)break;
		   }
	    }
	if(out==0)dis[now]=0;
	return out;
}

int main()
{
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
	for(int i=1;i<=m;i++)
	    {
	    	scanf("%lld%lld%lld",&u,&v,&d);
	    	add_edge(u,v,d);add_edge(v,u,0);
		}
	while(bfs())ans+=dinic(s,9999999999);
    printf("%lld",ans);
	return 0;
}

P2740

#include <bits/stdc++.h>
using namespace std;
struct edge
{
	long long v,next,dis;
}e[12000];
long long n,m,s,t,u,v,d,h[300],q[300],dis[300],pre[300],l,r,flow=0,ans=0,cnt=1;
void add_edge(long long u,long long v,long long dis)
{
	e[++cnt].next=h[u];
	e[cnt].v=v;
	e[cnt].dis=dis;
	h[u]=cnt;
}

bool bfs()
{
	memset(dis,0,sizeof(dis));
	l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
	while(l<=r)
	   {
	   	long long now=q[l];
	   	if(now==t)return 1;
	   	for(int i=h[now];i;i=e[i].next)
	   	    if(!dis[e[i].v]&&e[i].dis>0)dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
	   	l++;
	   }
	return 0;
}

long long dinic(long long now,long long flow)
{
	if(now==t)return flow;
	long long rest=flow;
	for(int i=pre[now];i;i=e[i].next)
	    {
	    pre[now]=i;
	    if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
	       {
	       	long long k=dinic(e[i].v,min(rest,e[i].dis));
	       	rest-=k,e[i].dis-=k,e[i^1].dis+=k;
		   }
	    }
	return flow-rest;
}

int main()
{
	scanf("%lld%lld",&m,&n);
	s=1,t=n;
	for(int i=1;i<=m;i++)
	    {
	    	scanf("%lld%lld%lld",&u,&v,&d);
	    	add_edge(u,v,d);add_edge(v,u,0);
		}
	while(bfs())
	   {
	   	flow=-1;
	   	while(flow!=0)
	   	   {
	   	   flow=dinic(s,99999999);
		   ans+=flow;
	       }
       }
    printf("%lld",ans);
	return 0;
}

例题 2

P1345 [USACO5.4] 奶牛的电信Telecowmunication

很显然的最小割,但是我们之前讲的最小割是割边,这个题是割点。

考虑把点拆成边,对于一个点 i,拆成入点 i 和出点 i+n,连边 ii+n,边权为 1。断掉这条边,就相当于在原图中删掉了这个点。注意 c1,c2 两点连接 c1+n,c2+n 的边边权为正无穷,因为题目中说 c1,c2 不能被删掉。

考虑将原图中的边转化到图中,对于一条有向边 uv,建边 u+nv,这样就保证从 u 还是可以走到 v。由于题目中给的是无向边,所以 vu 还要再这样建一次。这个建边的边权为正无穷,因为不能删除原图中的边。

最后,建立 0 号超级源点 sn×2+1 号超级汇点 t。连边 sc1,c2+nt,通过 s,t 的最小割来表示切断 c1,c2 之间的通路。显然,为了不影响结果,这两条边的边权应该是正无穷。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
	long long v,next,dis;
}e[80000];
long long n,m,s,t,u,v,h[1000],q[1000],dis[1000],pre[1000],l,r,flow=0,ans=0,cnt=1;
void add_edge(long long u,long long v,long long dis)
{
	e[++cnt].next=h[u];
	e[cnt].v=v;
	e[cnt].dis=dis;
	h[u]=cnt;
}

bool bfs()
{
	memset(dis,0,sizeof(dis));
	l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
	while(l<=r)
	   {
	   	long long now=q[l];
	   	for(int i=h[now];i;i=e[i].next)
	   	    {
	   	    if(!dis[e[i].v]&&e[i].dis>0)
    	   	    {
    	   	    dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
    	   	    if(e[i].v==t)return 1;
    	   	    }
	   	    }
	   	l++;
	   }
	return 0;
}

long long dinic(long long now,long long flow)
{
	if(now==t)return flow;
	long long rest=flow,out=0;
	for(int i=pre[now];i;i=e[i].next)
	    {
	    pre[now]=i;
	    if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
	       {
	       	long long k=dinic(e[i].v,min(rest,e[i].dis));
	       	flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
	       	if(!flow)break;
		   }
	    }
	if(out==0)dis[now]=0;
	return out;
}

int main()
{
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
    add_edge(0,s,9999999999),add_edge(s,0,0);
    add_edge(t+n,n*2+1,9999999999),add_edge(n*2+1,t+n,0);
    for(int i=1;i<=n;i++)
        if(i!=s&&i!=t)add_edge(i,i+n,1),add_edge(i+n,i,0);
        else add_edge(i,i+n,9999999999),add_edge(i+n,i,0);
	for(int i=1;i<=m;i++)
	    {
	    	scanf("%lld%lld",&u,&v);
	    	add_edge(u+n,v,9999999999);add_edge(v,u+n,0);
            add_edge(v+n,u,9999999999);add_edge(u,v+n,0);
		}
    s=0,t=n*2+1;
	while(bfs())ans+=dinic(s,9999999999);
    printf("%lld",ans);
	return 0;
}

例题 3

P3381 【模板】最小费用最大流

最小费用最大流模板题,不多赘述。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
	long long v,d,x,nxt;
}e[100010];
long long n,m,s,t,u,v,x,y,h[100010],d[100010],f[100010],vis[100010],pre[100010],q[500010],hp,tp,cnt=1,mc=0,mf=0;
void add_edge(long long u,long long v,long long d,long long x)
{
	e[++cnt].nxt=h[u];
	e[cnt].v=v;
	e[cnt].d=d;
	e[cnt].x=x;
	h[u]=cnt;
}

bool spfa(long long s,long long t)
{
	for(int i=1;i<=n;i++)d[i]=(long long)9999999999,pre[i]=f[i]=vis[i]=0;
	hp=1,tp=0,q[++tp]=s,d[s]=0,f[s]=(long long)9999999999;
	while(hp<=tp)
	   {
	   	long long now=q[hp];
	   	vis[now]=0;
	   	for(int i=h[now];i;i=e[i].nxt)
	   	    {
	   	    if(e[i].d==0)continue;
	   	    if(d[e[i].v]>d[now]+e[i].x)
	   	       {
			   d[e[i].v]=d[now]+e[i].x,f[e[i].v]=min(f[now],e[i].d),pre[e[i].v]=i;
			   if(vis[e[i].v]==0)q[++tp]=e[i].v,vis[e[i].v]=1;
		       }
	   	    }
	    hp++;
	   }
	if(d[t]==(long long)9999999999)return 0;
	else return 1;
}

void mcmf(long long s,long long t)
{
	while(spfa(s,t))
	   {
	   	mf+=f[t],mc+=d[t]*f[t];
	   	long long now=t;
	   	while(now!=s)
	   	    {
	   	    e[pre[now]].d-=f[t],e[pre[now]^1].d+=f[t];
	   	    now=e[pre[now]^1].v;
			}
	   }
}

int main()
{
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
	for(int i=1;i<=m;i++)
        {
        	scanf("%lld%lld%lld%lld",&u,&v,&x,&y);
        	add_edge(u,v,x,y);add_edge(v,u,0,-y);
		}
	mcmf(s,t);
	printf("%lld %lld",mf,mc);
	return 0;
}

例题 4

UVA1660 电视网络 Cable TV Network

本质上就是要求最少删除多少个点可以使得图不连通。

首先,如果把这个图每个点都删掉,这个图一定不连通,故可以把答案的初始值赋值为点数。

由于数据范围很小,考虑枚举任意两个点,求在不删除这两个点的情况下,最少删除多少点可以使这两个点不连通。因为如果这两个点不连通,整个图一定不连通。仔细看看,这就是例题 2

所以,我们只需要求 n2 次例题 2,每一次的结果和答案取最小值即可。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
	long long v,next,dis;
}e[80000];
long long n,m,s,t,u[1000],v[1000],h[1000],q[1000],dis[1000],pre[1000],l,r,flow=0,ans=0,cnt=1;
void add_edge(long long u,long long v,long long dis)
{
	e[++cnt].next=h[u];
	e[cnt].v=v;
	e[cnt].dis=dis;
	h[u]=cnt;
}

void init()
{
	memset(h,0,sizeof(h));
	cnt=1,ans=0;
}

bool bfs()
{
	memset(dis,0,sizeof(dis));
	l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
	while(l<=r)
	   {
	   	long long now=q[l];
	   	for(int i=h[now];i;i=e[i].next)
	   	    {
	   	    if(!dis[e[i].v]&&e[i].dis>0)
    	   	    {
    	   	    dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
    	   	    if(e[i].v==t)return 1;
    	   	    }
	   	    }
	   	l++;
	   }
	return 0;
}

long long dinic(long long now,long long flow)
{
	if(now==t)return flow;
	long long rest=flow,out=0;
	for(int i=pre[now];i;i=e[i].next)
	    {
	    pre[now]=i;
	    if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
	       {
	       	long long k=dinic(e[i].v,min(rest,e[i].dis));
	       	flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
	       	if(!flow)break;
		   }
	    }
	if(out==0)dis[now]=0;
	return out;
}

int main()
{
	while(scanf("%lld%lld",&n,&m)!=-1)
		{
		long long mi=9999999999;
		for(int i=1;i<=m;i++)scanf(" (%lld,%lld)",&u[i],&v[i]),u[i]++,v[i]++;
		mi=n;
		s=0,t=n*2+1;
		for(int s=1;s<=n;s++)
		    for(int t=s+1;t<=n;t++)
				{
				init();
				for(int i=1;i<=m;i++)
				    {
				    add_edge(u[i]+n,v[i],9999999999);add_edge(v[i],u[i]+n,0);
			        add_edge(v[i]+n,u[i],9999999999);add_edge(u[i],v[i]+n,0);
					}
				add_edge(0,s,9999999999),add_edge(s,0,0);
			    add_edge(t+n,n*2+1,9999999999),add_edge(n*2+1,t+n,0);
			    for(int i=1;i<=n;i++)
			        if(i!=s&&i!=t)add_edge(i,i+n,1),add_edge(i+n,i,0);
			        else add_edge(i,i+n,9999999999),add_edge(i+n,i,0);
				while(bfs())ans+=dinic(s,9999999999);
				mi=min(mi,ans);
			    }
	    printf("%lld\n",mi);
	    }
	return 0;
}

例题 5

P2891 [USACO07OPEN] Dining G

由于本题拥有较多的限制,考虑图论建模跑网络流来满足这些性质。

首先,建立超级源点,并将超级源点连向每一种食物所代表的点。建立超级汇点,将每一种饮料代表的点连向超级汇点。以上边权均为 1,因为每种食物或饮料只能供一头牛享用。

这样,当我们从超级源点向超级汇点跑网络流时,就可以经过每一种食品或饮料表示使用,且满足了每种食物或饮料的数量限制。接下来,需要满足牛的限制。

把每一头牛拆成两个点,牛头和牛尾,牛头以边权 1 连向牛尾。这里不能不拆,因为需要这个 1 来限制每头牛只享用一种食物和一种饮料。然后,根据网络流向在食物和牛头,饮料和牛尾之间连边,使网络流可以正常流通。接下来,跑一遍最大流即可求出答案。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
	long long v,next,dis;
}e[60000];
long long n,f,d,nf,nd,t,u,h[600],q[600],dis[600],pre[600],l,r,flow=0,ans=0,cnt=1;
void add_edge(long long u,long long v,long long dis)
{
	e[++cnt].next=h[u];
	e[cnt].v=v;
	e[cnt].dis=dis;
	h[u]=cnt;
}

bool bfs()
{
	memset(dis,0,sizeof(dis));
	l=1,r=0,q[++r]=0,dis[0]=1,pre[0]=h[0];
	while(l<=r)
	   {
	   	long long now=q[l];
	   	for(int i=h[now];i;i=e[i].next)
	   	    {
	   	    if(!dis[e[i].v]&&e[i].dis>0)
    	   	    {
    	   	    dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
    	   	    if(e[i].v==t)return 1;
    	   	    }
	   	    }
	   	l++;
	   }
	return 0;
}

long long dinic(long long now,long long flow)
{
	if(now==t)return flow;
	long long rest=flow,out=0;
	for(int i=pre[now];i;i=e[i].next)
	    {
	    pre[now]=i;
	    if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
	       {
	       	long long k=dinic(e[i].v,min(rest,e[i].dis));
	       	flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
	       	if(!flow)break;
		   }
	    }
	if(out==0)dis[now]=0;
	return out;
}

int main()
{
	scanf("%lld%lld%lld",&n,&f,&d);
	t=n*2+f+d+1;
	for(int i=1;i<=n;i++)
	    {
	    	scanf("%lld%lld",&nf,&nd);
	    	for(int j=1;j<=nf;j++)
	    	    {
	    	    	scanf("%lld",&u);
	    	    	add_edge(u,i+f,9999999999);add_edge(i+f,u,0);
				}
	    	for(int j=1;j<=nd;j++)
	    	    {
	    	    	scanf("%lld",&u);
	    	    	add_edge(i+f+n,u+2*n+f,9999999999);add_edge(u+2*n+f,i+f+n,0);
				}
			add_edge(f+i,f+i+n,1);add_edge(f+i+n,f+i,0);
		}
	for(int i=1;i<=f;i++)add_edge(0,i,1),add_edge(i,0,0);
	for(int i=1;i<=d;i++)add_edge(i+n*2+f,n*2+f+d+1,1),add_edge(n*2+f+d+1,i+n*2+f,0);
	while(bfs())ans+=dinic(0,9999999999);
    printf("%lld",ans);
	return 0;
}

例题 6

P4177 [CEOI2008] order

开始上难度了。

由于限制依旧很多,考虑网络流。首先考虑从正面解决这个问题,发现无法通过图论来约束租和买两种操作,无法解决。考虑反向思考。

可以先假设所有任务全部都做,不计算机器的费用,然后就是要求一个最小的值来减少机器的费用。对于求最小值,我们考虑最小割。

那么,我们建立超级源点 s 和超级汇点 t,用每条 st 的路径的割断表示选择何种方式。对于每一个任务,我们有两种方式对待它:做或者不做。如果不做,我们会失去这个任务带来的收益,但是不用考虑其他的费用。我们对于每一个任务 a,建立 sa,边权为完成 a 带来的收益,然后通过某种办法做到 at,这样割断 sa 就可以表示不做任务 a。根据最小割的性质,就不用继续考虑 at 的费用了,因为不做这个任务,自然不用考虑做这个任务的费用。

考虑租用只能用于一个任务,而购买可以用于所有任务,把需要用到机器 i 的任务直接连向机器 i,边权为租用这台机器的费用,使机器 i 直接连向 t,边权为购买这台机器的费用。这样,割掉这条边时,就相当于买了这台机器,经过这些任务的边就已经断了,不会再求其他边的影响了,就相当于是不用考虑租机器。

当割掉机器与任务之间的边时,就相当于租用机器。

由于最小割会自动选择最小的情况,所有求出来的就是费用的最小值。用所有任务的总收益减去,就是最终答案。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
	long long v,next,dis;
}e[3000000];
long long n,m,s,t,x,v,a,b,h[30000],q[30000],dis[30000],pre[30000],l,r,sum=0,flow=0,ans=0,cnt=1;
void add_edge(long long u,long long v,long long dis)
{
	e[++cnt].next=h[u];
	e[cnt].v=v;
	e[cnt].dis=dis;
	h[u]=cnt;
}

bool bfs()
{
	memset(dis,0,sizeof(dis));
	l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
	while(l<=r)
	   {
	   	long long now=q[l];
	   	for(int i=h[now];i;i=e[i].next)
	   	    {
	   	    if(!dis[e[i].v]&&e[i].dis>0)
    	   	    {
    	   	    dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
    	   	    if(e[i].v==t)return 1;
    	   	    }
	   	    }
	   	l++;
	   }
	return 0;
}

long long dinic(long long now,long long flow)
{
	if(now==t)return flow;
	long long rest=flow,out=0;
	for(int i=pre[now];i;i=e[i].next)
	    {
	    pre[now]=i;
	    if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
	       {
	       	long long k=dinic(e[i].v,min(rest,e[i].dis));
	       	flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
	       	if(!flow)break;
		   }
	    }
	if(out==0)dis[now]=0;
	return out;
}

int main()
{
	scanf("%lld%lld",&n,&m);
	s=0,t=n+m+1;
	for(int i=1;i<=n;i++)
	    {
	    	scanf("%lld%lld",&x,&v);
	    	sum+=x,add_edge(s,i,x),add_edge(i,s,0);
	    	for(int j=1;j<=v;j++)
	    	    {
	    	    	scanf("%lld%lld",&a,&b);
	    	    	add_edge(i,n+a,b),add_edge(n+a,i,0);
				}
		}
	for(int i=1;i<=m;i++)
	     {
	     	scanf("%lld",&x);
	     	add_edge(n+i,t,x),add_edge(t,n+i,0);
		 }
	while(bfs())ans+=dinic(s,9999999999);
    printf("%lld",sum-ans);
	return 0;
}

例题 7

P2774 方格取数问题

如果要取某一个方格,那么周围四个方格都不能取,不限制其他方格。这个正向也不太好维护,考虑先选出所有方格,然后减去一批方格,使剩余的方格没有公共边。

考虑相邻的两个点的横纵坐标之和的奇偶性不同,把图中的点划分为两类:横纵坐标之和为奇数的和横纵坐标之和为偶数的。

考虑最小割,并将割定义为删去的方格,这样我们就可以用最小割表示删去方格数的和的最小值。由于每个点只能删一次,建立超级源点 s,对于每一个横纵坐标之和为偶数的方格连边,边权为这个方格的正整数。在最小割中割掉这条边,相当于删去这个方格。建立超级汇点 t,对于每一个横纵坐标之和为奇数的方格连边,边权为这个方格的正整数。与上面同理,这样就能使每个方格最多被删除一次。

相邻的方格不能选,所有相邻的方格应该有连边,这样跑最小割的时候就会把两边的方格割断一个。对于每一个横纵坐标之和为偶数的方格,连边向与其四周相邻的点,边权为正无穷,因为这个约束关系不能被删除。

最小割会自动选择割掉哪些边,也就是在原方格图中删掉哪些点使费用最小,并根据方格之间的连边,满足相邻的方格不能选的约束关系。求出最小割之后,用方格图中数的总和减去最小割就是最后答案。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
	long long v,next,dis;
}e[4000000];
long long n,m,s,t,h[40000],q[40000],dis[40000],pre[40000],a[200][200],l,r,sum=0,flow=0,ans=0,cnt=1,inf=1e15;
long long dx[4]={0,0,1,-1};
long long dy[4]={1,-1,0,0};
void add_edge(long long u,long long v,long long dis)
{
	e[++cnt].next=h[u];
	e[cnt].v=v;
	e[cnt].dis=dis;
	h[u]=cnt;
}

bool bfs()
{
	memset(dis,0,sizeof(dis));
	l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
	while(l<=r)
	   {
	   	long long now=q[l];
	   	for(int i=h[now];i;i=e[i].next)
	   	    {
	   	    if(!dis[e[i].v]&&e[i].dis>0)
    	   	    {
    	   	    dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
    	   	    if(e[i].v==t)return 1;
    	   	    }
	   	    }
	   	l++;
	   }
	return 0;
}

long long dinic(long long now,long long flow)
{
	if(now==t)return flow;
	long long rest=flow,out=0;
	for(int i=pre[now];i;i=e[i].next)
	    {
	    pre[now]=i;
	    if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
	       {
	       	long long k=dinic(e[i].v,min(rest,e[i].dis));
	       	flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
	       	if(!flow)break;
		   }
	    }
	if(out==0)dis[now]=0;
	return out;
}

int main()
{
	scanf("%lld%lld",&n,&m);
	s=0,t=n*m+1;
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=m;j++)
	        {
	        scanf("%lld",&a[i][j]);
	        sum+=a[i][j];
	        }
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=m;j++)
	        if((i+j)%2==0)
	           {
	           	add_edge(s,m*(i-1)+j,a[i][j]),add_edge(m*(i-1)+j,s,0);
	           	for(int k=0;k<4;k++)
	           	    {
	           	       int tx=i+dx[k],ty=j+dy[k];
	           	       if(tx>n||ty>m||tx<=0||ty<=0)continue;
	           	       add_edge(m*(i-1)+j,m*(tx-1)+ty,inf),add_edge(m*(tx-1)+ty,m*(i-1)+j,0);
					}
			   }
			else if((i+j)%2==1)
			   	add_edge(m*(i-1)+j,t,a[i][j]),add_edge(t,m*(i-1)+j,0);
	while(bfs())ans+=dinic(s,inf);
    printf("%lld",sum-ans);
	return 0;
}

后记

相关法律规定,网络流题目不能卡 Dinic 算法。

网络流一堆神仙思维题。

posted @   w9095  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示