网络流:最大流以及费用流的总结

前言:

(终于学会了网络流好开心) 网络流是一类问题的统称,实际上很多问题都可以转成网络流来整,所以在此总结一下。

最大流

方法常见的是 F o r d − F u l k e r s o n Ford-Fulkerson FordFulkerson方法,这个之所以叫方法,是因为这个有很多不同版本的实现,最常见的实现是 d i n i c dinic dinic。这个方法的核心是不停的寻找増广路,直到没有增广路为止。

dinic

算法的实现是不直接像FF方法所说的以来就找增广路,而是先用bfs将整个图分层,然后再在一层图中的点不算做增广路的路径,这样子的话,避免了找很多额外的边,这样子的话,可以证明复杂度是 O ( E V 2 ) O(EV^2) O(EV2)的,但是实际上根本就跑不到这个复杂度,而且还有一些优化,导致在随机图中可以过 1 e 5 1e5 1e5的数据都没有问题。
而且我们在做的时候,还可以有一个叫做当前弧优化的大法,方法是将肯定不可能的边跳过,这样就大大增快了速度。
下面提供两个版本的 d i n i c dinic dinic,分别是前缀星以及邻接表的储存方式。(是根据《挑战算法程序竞赛》的模板改来的)
前缀星:

#include 
using namespace std;
const int N = 10005;
const int M = 100005;
const int inf = 1000000000;
struct E{int to,cap,nex;}e[M*2];int head[N],ecnt;
int dep[N];
int iter[N];
int n,m,s,t;

void adde(int fr,int to,int cap)
{
	e[ecnt] = (E){to,cap,head[fr]};
	head[fr] = ecnt++;
	e[ecnt] = (E){fr,0,head[to]};
	head[to] = ecnt++;
}

void bfs(int s)
{
	memset(dep,-1,sizeof dep);
	queue <int> q;
	dep[s] = 0;
	q.push(s);
	while (!q.empty())
	{
		int cur = q.front();q.pop();
		for (int j=head[cur];j!=-1;j=e[j].nex)
		{
			if (e[j].cap >0 && dep[e[j].to] < 0)
			{
				dep[e[j].to] = dep[cur] + 1;
				q.push(e[j].to);
			}
		}
	}
}

int dfs(int o,int t,int flow)
{
	if (o==t) return flow;
	for (int &j=iter[o];j!=-1;j=e[j].nex)
	{
		if (e[j].cap > 0 && dep[e[j].to] == dep[o] + 1)
		{
			int tmpflow = dfs(e[j].to,t,min(flow,e[j].cap));
			if (tmpflow > 0)
			{
				e[j].cap -= tmpflow;
				e[j^1].cap += tmpflow;
				return tmpflow;
			}
		}
	}
	return 0;
}

int dinic(int s,int t)
{
	int ansflow = 0;
	for (;;)
	{
		bfs(s);
		if (dep[t] < 0) return ansflow;
		for (int i=1;i<=n;i++) iter[i]=head[i];
		int f;
		while ( (f=dfs(s,t,inf)) > 0)
		{
			ansflow += f;
		}
	}
}
void init()
{
	ecnt=0;
	memset(head,-1,sizeof head);
}

邻接表:

#include 
#define pb push_back
using namespace std;
const int N = 10005;
const int inf = 1000000000;
struct E{int to,cap,rev;};
vector <E> G[N];
int dep[N];
int iter[N];
int n,m,s,t,e;

void adde(int fr,int to,int cap)
{
	G[fr].pb((E){to,cap,G[to].size()});
	G[to].pb((E){fr,0,G[fr].size()-1});
}

void bfs(int s)
{
	memset(dep,-1,sizeof dep);
	queue <int> q;
	dep[s] = 0;
	q.push(s);
	while (!q.empty())
	{
		int cur = q.front();q.pop();
		for (int i=0;i<G[cur].size();i++)
		{
			E &e = G[cur][i];
			if (e.cap >0 && dep[e.to] < 0)
			{
				dep[e.to] = dep[cur] + 1;
				q.push(e.to);
			}
		}
	}
}

int dfs(int o,int t,int flow)
{
	if (o==t) return flow;
	for (int &i=iter[o];i<G[o].size();i++)
	{
		E &e = G[o][i];
		if (e.cap > 0 && dep[e.to] == dep[o] + 1)
		{
			int tmpflow = dfs(e.to,t,min(flow,e.cap));
			if (tmpflow > 0)
			{
				e.cap -= tmpflow;
				G[e.to][e.rev].cap += tmpflow;
				return tmpflow;
			}
		}
	}
	return 0;
}

int dinic(int s,int t)
{
	int ansflow = 0;
	for (;;)
	{
		bfs(s);
		if (dep[t] < 0) return ansflow;
		memset(iter,0,sizeof iter);
		int f;
		while ( (f=dfs(s,t,inf)) > 0)
		{
			ansflow += f;
		}
	}
}

费用流

常见的费用流有两种,一种是求流量为 F F F的费用流,另外一种是求最大流最小费用流。如何做呢?我们考虑一下我们是怎么做最大流的,我们是将增广路按照距离来 b f s bfs bfs分层,那么这个我们也可以模仿此,但是每次我们怎么走呢?我们按照费用的最小来走,这样子的话,就很明显了,但是要注意,不要乱写 d i j k s t r a l dijkstral dijkstral,要写 B e l l m a n − F o r d Bellman-Ford BellmanFord,因为路上的边权可能是负的。当然也可以写 d i j k s t r a l dijkstral dijkstral,但是要用一下势函数 h h h,借助类似差分的思想将边权变为正。下面是最小费用最大流的代码( S P F A SPFA SPFA):

#include
using namespace std;
const int N = 5002;
const int M = 500005;
const int inf = 100000;
struct E
{
	int to,cap,cost,flow,next;
}e[2*M];int head[N] , ecnt;
int  pre[N];
int dis[N];
bool vis[N];
int n,m,S,T;

void Clear()
{
	ecnt = 0;
	memset(head,-1,sizeof head);
}

void adde(int fr,int to,int cap,int cost)
{
	e[ecnt]=(E){to,cap,cost,0,head[fr]};
	head[fr] = ecnt++;
	e[ecnt]=(E){fr,0,-cost,0,head[to]};
	head[to] = ecnt++;
}

bool SPFA(int s,int t)
{
	memset(vis,0,sizeof vis);
	memset(dis,127,sizeof dis);
	memset(pre,-1,sizeof pre);
	queue <int> q;
	q.push(s);dis[s] = 0;vis[s]=1;
	
	while (!q.empty())
	{
		int cur = q.front();q.pop();vis[cur] = false;
		for (int j=head[cur];j!=-1;j=e[j].next)
		{
			int to = e[j].to;
			if (dis[to] > dis[cur] + e[j].cost && e[j].cap > e[j].flow )
			{
				dis[to] = dis[cur] + e[j].cost;
				pre[to] = j;
				if (!vis[to])
				{
					q.push(to);
					vis[to] = true;
				}
			}
		}
	}
	return pre[t] != -1;
}

void MCMF (int s,int t,int &maxflow,int &mincost)
{
	maxflow = mincost = 0;
	while (SPFA(s,t))
	{
		int MIN = inf;
		for (int j=pre[t]; j!=-1;j=pre[e[j^1].to])
		{
			MIN = min(MIN,e[j].cap - e[j].flow);
		}
		for (int j=pre[t]; j!=-1;j=pre[e[j^1].to])
		{
			e[j].flow += MIN;
			e[j^1].flow -= MIN;
			mincost += MIN * e[j].cost;
		}
		maxflow += MIN;
	}
}

还有一大坑点:建图的时候要从0号边开始,不然他的反边就不是 i ⊕ 1 i ⊕ 1 i1

网络流常见套路(经典的例题):

最小割

  • 文理分科

一个点有 A , B A,B AB两种选择,分别有不同的收益。以及一个点如果与某些点同时选择一样,就会带来额外收益。

  • 分析

这种每个点两种选择的,像极了最小割中源点集以及汇点集的区分。那么先假设所有收益都能获得,然后用割的方式来决定怎么选。答案就等于 所有收益减去割 。因为要最大化答案,所以要最小化割,所以求一个最小割就行了。具体的。
比如割完之后源点集表示选择 A A A的,汇点集表示选择 B B B的。那么在源点集的点要割掉选 B B B的代价。所以连边 S → i S \rightarrow i Si A i A_i Ai i → T i\rightarrow T iT B i B_i Bi
然后考虑同时选择这些点的收益。比如同时选 择 A 择A A点的收益。我们新建一个点表示这个收益。那么,如果这个收益不被割掉,那么这几个点都选了 A A A。所以连边 i → n e w n o d e i\rightarrow newnode inewnode连收益, i → 同 时 要 选 的 点 连 i n f i\rightarrow 同时要选的点连inf iinf。意义就是说,如果有至少有一个点在 T T T集中,假设这个点为 l l l。那么表示说 l → T l\rightarrow T lT的边没被割掉。所以 i → n e w n o d e → l → T i\rightarrow newnode\rightarrow l\rightarrow T inewnodelT还有通路,所以就会割掉这个收益。

  • 选格子问题

N N N 手上有一个 M × N M×N M×N 的方格图。控制一个点要 A i j ( > 0 ) A_{ij} (> 0) Aij(>0)的代价;如果一个点被控制了,或它上下左右存在的点都被控制了,就算这个点被选择了,可以得到 B i j ( > 0 ) B_{ij} (> 0) Bij(>0)的回报。现在请你帮小 N N N 选一个最优的方案,使得回报减代价最大。

  • 分析

首先,我们明白,如果要获得一个点的收益,那么要么选它周围的四个点,要么选它。因为代价非负。
所以说,根据之前,我们还是有一个点选或者不选对应源点或者汇点集。我们有注意到这实际上黑白染色之后还是一个二分图。那么对于一个点,首先就二分图那样与源点/汇点连选的代价。然后怎么连回报?根据之前所说,只用新建一个点,分别向那五个点连边即可,意义与之前相同。

最大流问题

费用流

  • 餐巾问题

每天有几种选择:1)买餐巾,花费 a a a。2)把餐巾拿去洗,花费 b b b,要等上 c c c天才能洗好。然后每天要用一定数量的餐巾 n e e d [ i ] need[i] need[i],餐巾用了之后必须拿去洗才能再用,刚买的餐巾不用洗。问满足每天所需数量的前提下的最小代价。

  • 分析

对于每一天,我们建两个节点表示能用的纸巾与不能用的纸巾,记为 c a n i 与 c a n t i can_i与cant_i canicanti。新建源点与汇点表示买以及每天所需。什么意思?就是说,比如对于某一天中能用的纸巾,我们要么把它拿去用,要么就留到下一天。那么对应了 c a n i → T < n e e d [ i ] , 0 > can_i\rightarrow T <need[i],0> caniT<need[i],0>以及 c a n i → c a n i + 1 < i n f , 0 > can_i\rightarrow can_i+1<inf,0> canicani+1<inf,0>。对于不能用的,我们可以拿去洗,也可以留到下一天处理,注意,洗了之后就能用了,所以连边: c a n t i → c a n i + c < i n f , b > , c a n t i → c a n t i + 1 < i n f , 0 > cant_i\rightarrow can_i+c<inf,b>,cant_i\rightarrow cant_i+1<inf,0> canticani+c<inf,b>canticanti+1<inf,0>。然后还可以买,那么直接 S → c a n 1 < i n f , c > S\rightarrow can_1<inf,c> Scan1<inf,c>。但是这样每天那个用的纸巾就凭空消失了。所以从源点向 c a n t i cant_i canti < n e e d [ i ] , 0 > <need[i],0> <need[i],0>表示今天用的。

  • 扩容问题 Zjoi

给定一张有向图,每条边都有一个容量 C i C_i Ci和一个扩容费用 W i W_i Wi。这里扩容费用是指将容量扩大 1 1 1所需的费用。
求:1) 在不扩容的情况下,1到N的最大流;2) 将1到N的最大流增加K所需的最小扩容费用。

  • 分析

跑出最大流之后,在残量网络上还有些非负边,可以直接用。所以我们对于原图中的边,将其费用设为0,然后新增一条边表示给这个边扩容,那么是 < i n f , w i > <inf,w_i> <inf,wi>,然后这样不一定能保证至多 k k k次,所以在连个 k k k大小的边随便限制一下就好了。

  • 扩容问题2 Codeforces 708D

给定一张不合法的网络流图,每条边都有一个容量 C i C_i Ci和流量 f i f_i fi。可能会有流量大于容量的情况以及不满足流量平衡。现在你每次可以以1的代价将某条边容量(或流量)增加或减少1,问变成一个合法的网络流图的最小代价。

  • 分析

首先,像上下界网络流一样,根据出流量以及入流量来补流。
然后对于某一条边 ( c i , f i ) (c_i,f_i) (ci,fi),如果 c i > f i c_i > f_i ci>fi,那么答案要加上 c i − f i c_i - f_i cifi,然后根据是增加容量还是减少流量来建边。

  • 扩容问题3 Codeforces 362E

给定一张有向图,每条边都有一个容量 C i C_i Ci和一个扩容费用 W i W_i Wi。这里扩容费用是指将容量扩大 1 1 1所需的费用。
求将使用K代价最多可以扩流多少。

  • 分析
    同1。

  • bzoj1449球队收益

题面不好打,就写了题号。

  • 分析

我们假设所有队伍在接下来的比赛中都 g g gg gg,这样会得到一个收益。然后用经典的费用流的费用与流量的平方成正比的方式做就好了。

一些重要的东西

  • 不同的最小割有多少个?

首先我们知道至多有 n − 1 n-1 n1个。然后如果每个点对都跑一次的话,就是 n 2 ∗ O ( 最 小 割 ) n^2 * O(最小割) n2O(),肯定不行。考虑怎么乱搞。先随便选择一个源点 S S S一个汇点 T T T跑一下最小割,设为 F F F。然后对于一个源点集中的点 i i i以及汇点集中的点 j j j F F F可能是 i , j i,j ij的最小割。然后很明显,就像点分治那样,我们统计了 S → T S\rightarrow T ST的割,然后分治源点集与汇点集。然而我不会证明。可以通过点分治的方式意会一下正确性。

  • 那些边是一定要割的?那些边是可能被割的?

这有一个结论:
先跑一边最大流,然后在残量网络上缩点。对于一条边 u → v u\rightarrow v uv,如果 u , v u,v uv不在一个联通分量,就可能被割掉。如果还满足 u u u S S S一个分量, v v v T T T一个分量。那么就必须割掉。当然,如果这条边在这次网络流中都没被割掉,那么肯定不会被割掉。

你可能感兴趣的:(总结,省选,网络流)