网络流(最大流、最小费用流、上下界网络流)学习笔记(费用流、上下界以及它们的建模还没写)

网络流(最大流、最小费用流、上下界网络流)学习笔记

基本概念

网络

网络是一个有向图 G=(V,E),其中每条边 (u,v)E 都有一个容量 c(u,v),若 (u,v)Ec(u,v)=0

网络图中有两个特殊的点:源点 sV 和汇点 tVst)。

f(u,v)V×VR 的函数,且满足如下性质:

  • 容量限制:f(u,v)c(u,v)
  • 斜对称性:f(u,v)=f(v,u)
  • 流量守恒:uV{s,t},fin(u)=(v,u)Ef(v,u)=(u,w)Ef(u,w)=fout(u)

则称 f 是网络 G 的流函数。f(u,v) 称为边 (u,v) 的流量,c(u,v)f(u,v) 称为边 (u,v) 的剩余容量。网络的流量记作 V(f)=(s,u)Ef(s,u)=(v,t)Ef(v,t)

最大流

简介

最大流问题是求源点到汇点最大流量的问题。

Ford-Fulkerson 方法

FF 方法思路

FF 方法的核心思路是构建一个初始的流,不断进行改进,直到得到最大流。

定义 bottleneck(p,f) 为流 f 状态下,路径 p 经过的所有边中的最小剩余容量。

那么我们不断增广流 f 中的源点到汇点的路径 p,使得流量增加 bottleneck(p,f)

但是这样是有问题的,例如对于这张网络:(边上标注:流量/容量)

显然,它的最大流如下图所示:

但是,如果第一次找到的增广路是这样的,则源点和汇点被阻塞,无法继续增广,所得流量错误:

我们发现,现在的方法会错走不该走的路径,因此我们引入反向边和剩余图用于退流。

对于有向图 G=(V,E) 和流 f,我们定义它的剩余图 Gf=(V,E)。对于原图中每条边 (u,v)E,在剩余图中按如下规则添加至多两条边:

  • 正向边:如果 f(u,v)<c(u,v),则添加一条边 (u,v),容量 c(u,v)=c(u,v)f(u,v)
  • 反向边:如果 f(u,v)>0,则添加一条边 (v,u),容量 c(v,u)=f(u,v)

例如,对于上面这个走了不该走的路径的流:

它的剩余图如下:

就可以继续增广了。

总结 FF 方法流程

  • 初始时所有 f(u,v)=0
  • Gf 中存在源点到汇点的路径,任选一条路径进行增广。
  • Gf 中不存在源点到汇点的路径,此时的 f 即为最大流,方法结束。

复杂度和缺陷说明

(一)当容量 c(u,v)N+

我们很容易构造一张网络把 FF 方法卡掉:

如果依次增广 suvt,svut,suvt,,显然需要增广 2×10100 次。

具体地,假设 n,m,f 为点数、边数、最大流流量,则 FF 方法在容量为整数时的复杂度为 O(mf)

(二)当容量 c(u,v)R+

构造下面这张网络:

其中 C 是足够大的整数(例如 10100),r=5120.618,显然有 r2=1r<r4<r3<r2<r<1

如果依次增广 suvwxt,swvut,suvwxt,sxwvt,suvwxt,,则方法最终会收敛到一个小于最大流的流,方法错误。

具体地,假设增广了 1+4k 次,则收敛到的流:

f=1+2i=12kri1+2i=1+ri=1+2r1r<5

而最大流显然为 2C+1

闲话

虽然 FF 方法有种种缺陷,但是 CF 上还真有一道卡掉了后面要讲的 EK、Dinic 而正解 FF 的题(其实是先 Dinic 然后再 FF):CF1383F Special Edges

Edmonds-Karp 算法

EK 算法

相比于 FF 方法,EK 算法在寻找增广路时并不是随便找一条,而是通过 BFS 的方式找最短的一条。

复杂度

EK 算法的时间复杂度为 O(nm2)

这里简单说一下证明方法:

发现 EK 算法执行过程中,任何点在剩余图中的层数单调不降。

对于每条边 (u,v),其连续作为 bottleneck 时,u 所在层数至少增加 2。又显然层数最多为 n

因此,对于每条边 (u,v),它作为 bottleneck 的次数不超过 n2 次。

m 条边,最多增广 O(nm) 次,每次 BFS 找增广路复杂度为 O(m),故总复杂度为 O(nm2)

代码

好久没写过最大流的 EK 了,放个几年前的代码凑合看吧:

#include <bits/stdc++.h>
using namespace std;

#define inf 1073741823

int n, m, s, t;

struct Node
{
	int v;
	int val;
	int next;
}node[201010];

int top = 1, head[101010];

inline void addedge(int u, int v, int val)
{
	node[++top].v = v;
	node[top].val = val;
	node[top].next = head[u];
	head[u] = top;
}

inline int Qread(void)
{
	int x = 0;
	char c = getchar();
	while(c > '9' || c < '0') c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
	return x;
}

int inque[101010];

struct Pre
{
	int v;
	int edge;
}pre[101010];

inline bool BFS(void)
{
	queue<int> q;
	memset(inque, 0, sizeof inque);
	memset(pre, -1, sizeof pre);
	inque[s] = 1;
	q.push(s);
	while(!q.empty())
	{
		int u = q.front();
		q.pop();
		for(int i=head[u];i;i=node[i].next)
		{
			int d = node[i].v;
			if(!inque[d] && node[i].val)
			{
				pre[d].v = u;
				pre[d].edge = i;
				if(d == t) return true;
				inque[d] = 1;
				q.push(d);
			}
		}
	}
	return false;
}

int EK(void)
{
	int ans = 0;
	while(BFS())
	{
		int minx = inf;
		for(int i=t;i!=s;i=pre[i].v)
			minx = min(minx, node[pre[i].edge].val);
		for(int i=t;i!=s;i=pre[i].v)
		{
			node[pre[i].edge].val -= minx;
			node[pre[i].edge^1].val += minx;
		}
		ans += minx;
	}
	return ans;
}

int main(void)
{
	register int i;
	n = Qread();
	m = Qread();
	s = Qread();
	t = Qread();
	int u, v, w;
	for(i=1;i<=m;i++)
	{
		u = Qread();
		v = Qread();
		w = Qread();
		addedge(u, v, w);
		addedge(v, u, 0);
	}
	cout<<EK()<<endl;
	return 0;
}

Dinic 算法

Dinic 算法

Dinic 算法通过在剩余图 Gf 上建立分层网络 GL,充分利用了 BFS 的信息。

首先定义剩余图 Gf 上的分层网络 GL:在 Gf 上进行 BFS 分层,从层 d 节点连向层 d+1 节点的边被保留在 GL 中。容易发现 GL 中包含了所有 su 的最短路。

Dinic 算法的一个阶段执行如下操作:

  • Gf 上做 BFS 建立 GL
  • GL 上做 DFS 寻找阻塞流 f,使用 f 增广 f

其中阻塞流 f 满足它的剩余图中源点和汇点被阻塞,也就是不存在其他增广路。

复杂度

Dinic 算法的时间复杂度为 O(n2m)

这里简单说一下证明方法:

每个算法阶段至少使得汇点 t 的层次增加 1,因此最多有 O(n) 个算法阶段。

每个算法阶段中:

  • BFS 构建分层网络 GL 复杂度为 O(m)
  • 寻找阻塞流复杂度为 O(nm)
    • 其中,DFS 寻找一条路径复杂度为 O(n)
    • 每条路径中,至少一条边作为 bottleneck 会满流,因此最多 O(m) 条路径。

特别地,Dinic 算法解决二分图最大匹配问题的时间复杂度为 O(mn),优于匈牙利算法的 O(nm)

算法优化

Dinic 算法有两个常见的优化:

  • 多路增广:每找到一条增广路,如果剩余容量没有用完,可以继续找增广路。
  • 当前弧优化:在一张 GL 中,如果一条边被增广过,它就没有可能被增广第二次,下次增广时可以不必考虑这些边。

代码

//By: Luogu@rui_er(122461)
#include <bits/stdc++.h>
#define rep(x,y,z) for(ll x=y;x<=z;x++)
#define per(x,y,z) for(ll x=y;x>=z;x--)
#define debug printf("Running %s on line %d...\n",__FUNCTION__,__LINE__)
#define fileIO(s) do{freopen(s".in","r",stdin);freopen(s".out","w",stdout);}while(false)
using namespace std;
typedef long long ll;
const ll N = 205, M = 1e4+5, inf = 0x3f3f3f3f3f3f3f3fll;

ll n, m, s, t, dis[N], now[N];
queue<ll> q;
template<typename T> void chkmin(T& x, T y) {if(x > y) x = y;}
template<typename T> void chkmax(T& x, T y) {if(x < y) x = y;}
struct Edge {
	ll v, w, nxt;
	Edge(ll a=0, ll b=0, ll c=0) : v(a), w(b), nxt(c) {}
	~Edge() {}
}e[M];
ll h[N], ne = 1;
void add(ll u, ll v, ll w) {
	e[++ne] = Edge(v, w, h[u]); h[u] = ne;
	e[++ne] = Edge(u, 0, h[v]); h[v] = ne;
}
ll dfs(ll u, ll lim) {
	if(!lim || u == t) return lim;
	ll res = 0;
	for(ll i=now[u];i;now[u]=i=e[i].nxt) {
		ll v = e[i].v, w = e[i].w;
		if(dis[v] == dis[u] + 1) {
			ll qwq = dfs(v, min(lim, w));
			res += qwq; lim -= qwq;
			e[i].w -= qwq; e[i^1].w += qwq;
			if(!lim) break;
		} 
	}
	return res;
}
bool bfs() {
	memset(dis, 0x3f, sizeof(dis));
	memcpy(now, h, sizeof(h));
	while(!q.empty()) q.pop();
	dis[s] = 0;
	q.push(s);
	while(!q.empty()) {
		ll u = q.front(); q.pop();
		for(ll i=h[u];i;i=e[i].nxt) {
			ll v = e[i].v, w = e[i].w;
			if(w && dis[v] == inf) {
				dis[v] = dis[u] + 1;
				q.push(v);
			}
		}
	}
	return dis[t] < inf;
}
ll dinic() {
	ll flow = 0;
	while(bfs()) flow += dfs(s, inf);
	return flow;
}

int main() {
	scanf("%lld%lld%lld%lld", &n, &m, &s, &t);
	rep(i, 1, m) {
		ll u, v, w;
		scanf("%lld%lld%lld", &u, &v, &w);
		add(u, v, w);
	}
	printf("%lld\n", dinic());
	return 0;
}

其他最大流算法

其他最大流算法还有 MPM(O(n3))、ISAP(O(n2m))、Push-Relabel 预流推进(O(n3))、HLPP 最高标号预流推进(O(n2m))、近期发明的近线性网络流算法(O(m1+o(1)) 等,但是 Dinic 一般跑不满,实现相对容易且效率较高,在一般的算法竞赛题里完全够用了,因此本博客不会介绍。好吧其实是我不会。

最小割

定义

一个 st 的切割是点集 V 的一个划分 (A,B),其中 sA,tB。定义切割 (A,B) 的容量为 c(A,B)=uA,vB,(u,v)Ec(u,v),也就是所有从 A 指向 B 的边的容量之和(注意从 B 指向 A 的不算)。

例如对于下图:

如果令 A={s,v},B={u,t},则 c(A,B)=c(s,u)+c(v,t)=2+1=3

顾名思义,最小割就是要求解割的最小容量。

引理一

f 为一个流,(A,B) 为任意一个 st 切割,则横跨切割的流量与网络的流量相等,即 V(f)=fout(A)fin(A)

例如下面这张图中,V(f)=2+11=2

证明:

V(f)=fout(s)=fout(s)fin(s)fin(s)=0=uA(fout(u)fin(u))流量守恒=(e:ABf(e)+e:AAf(e))(e:BAf(e)+e:AAf(e))=fout(A)fin(A)

引理二

f 为一个流,(A,B) 为任意一个 st 切割,则 V(f)c(A,B)

证明:

V(f)=fout(A)fin(A)引理一fout(A)fin(A)0=e:ABf(e)e:ABc(e)f(e)c(e)=c(A,B)

最大流最小割定理

fG 的最大流 存在切割 (A,B) 使得 V(f)=c(A,B)

证明:

FF 方法结束后,Gf 中没有 st 路径。

AGf 上从 s 可达的点,B=VA,则 (A,B) 为一个切割。

考虑横跨切割的边 (u,v)E

  • uA,vB,则 f(u,v)=c(u,v)
  • vA,uB,则 f(u,v)=0

因此:

V(f)=fout(A)fin(A)=fout(A)fin(A)=0=e:ABf(e)=e:ABc(e)f(e)=c(e)=c(A,B)

最小割问题

因此最小割问题可以转化为最大流问题进行求解,我们也可以根据上面给出一种构造。

最大流与最小割建模

二分图最大匹配 | P2756 飞行员配对方案问题

匈牙利算法的时间复杂度为 O(n3)O(nm)

考虑这样一种建模:

  • 源点向二分图左部点连容量为 1 的边。
  • 二分图右部点向汇点连容量为 1 的边。
  • 二分图左部点向与之相连的二分图右部点连容量为 1 的边。

时间复杂度为 O(mn)

这个是最简单的最大流建模之一。

二者选其一问题 | P2057 [SHOI2007] 善意的投票 / [JLOI2010] 冠军调查

将若干个物品分为 A,B 两类,其中有三种代价:

  • i 个物品分到 A 类,产生 ai 的代价。
  • i 个物品分到 B 类,产生 bi 的代价。
  • ui 个物品和第 vi 个物品分到不同类,产生 ci 的代价。

要使得总代价最小。

考虑这样一种建模:

  • 源点向每个物品连容量为 bi 的边。
  • 每个物品向汇点连容量为 ai 的边。
  • uivi 互相连容量为 ci 的边。

在 Dinic 算法结束后,点被分为两类:从源点可达的、从源点不可达的。它们对应了 A,B 两类。容易发现,一个划分的割就是这种分类方案的代价,我们求最小割即可。

P1402 酒店之王

n 个人,每个人有若干个想住的房间、若干道想吃的菜。每个人住一个房间,吃一道菜。一个房间只能住一个人,一道菜也只能一个人吃,求最多满足多少人要求。

考虑这样一种建模:

  • 源点向每个房间连容量为 1 的边。
  • 每道菜向汇点连容量为 1 的边。
  • 每个房间向每个想住这个房间的人连容量为 1 的边。
  • 每个人想这个人想吃的菜连容量为 1 的边。

但是这个建模是错误的,原因是没有限制一个人只能住一个房间,吃一道菜。我们考虑拆点(这是非常重要的建模思想),将每个人拆成两个点,两点之间连容量为 1 的边,这样就限制了每个人只能住一个房间,吃一道菜。

posted @   rui_er  阅读(321)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示