网络流学习笔记

网络流学习笔记

一、定义

网络(network)是指一个特殊的有向图 G=(V,E),其与一般有向图的不同之处在于有容量和源汇点。

  • E 中的每条边 (u,v) 都有一个被称为容量 (capacity) 的权值,记作 c(u,v)。当 (u,v)E 时,可以假定 c(u,v)=0

这句话的意思是对于网络流中的反向边,流量应为 0

  • V 中有两个特殊的点:源点(source)s 和汇点(sink)tst)。
    对于网络 G=(V,E),流(flow)是一个从边集 E 到整数集或实数集的函数,其满足以下性质。

容量限制:对于每条边,流经该边的流量不得超过该边的容量,即 0f(u,v)c(u,v)

流守恒性:除源汇点外,任意结点 u 的净流量为 0。其中,我们定义 u 的净流量为 f(u)=xVf(u,x)xVf(x,u)

流出多少,也应该流入多少。

对于网络 G=(V,E) 和其上的流 f,我们定义 f 的流量 |f|s 的净流量 f(s)。作为流守恒性的推论,这也等于 t 的净流量的相反数 f(t)

s 是源点, f(s)s 流出的流量,就等于 t 流入的流量 f(t)

斜对称性:对于每条边,f(u,v)=f(v,u)

这是后文反向边与反悔策略需要用到的。

对于网络 G=(V,E),如果 {S,T}V 的划分(即 ST=VST=),且满足 sS,tT,则我们称 {S,T}G 的一个 st 割(cut)。我们定义 s-t 割 {S,T} 的容量为 ||S,T||=uSvTc(u,v)

割通俗来讲就是将一张网络断掉一些边使得其不再连通的的边集。割的容量就是这个边集的流量和。

二、最大流问题

1. 最大流算法

(1) 增广路

增广路指的是原图中一条 st 且沿途 c(u,v)>0 的路径。

(2) FF 算法

在原图中每一次找到一条增广路进行增广,直到原图中不存在增广路。

(3) 反向边与撤销影响

是对上面的算法正确性的一个证明。显然每一次增广路的选择是偶然的。

考虑这样一张网络:图1
显然最优方案是 sat,sbt。但如果找到的增广路是 sabt 呢?

由于网络流斜对称性质,此时的流量 f(a,b)=1,f(b,a)=1。那么剩余流量 c(b,a)=c(b,a)f(b,a)=1,不难发现此时原图有一条 sbat 的增广路。

考虑这样做的实际意义:bt 的流量实际由 sb 来最终承担了,而 at 的流量实际由 sa 承担。ab 本身不承担任何意义,只是过渡使用。

于是我们证明了 FF 算法的正确性。

(4) EK 算法

每次通过 bfs 找到一条增广路,进行增广。是对 FF 算法的一个具体实现。

(5) Dinic 算法

最大流问题中最常用的算法,下面描述该算法的步骤。

  1. 对原图依据每个点到源点的距离 depx 进行 bfs 分层,暂时删除 depv<depu 的边 (u,v)
  2. 对原图进行 dfs 找到增广路
  3. 重复上面算法直到源汇点不再连通。

考虑上面算法的正确性:显然。本质上是通过临时删边的方式使得每一次找到一个流使得原图不再连通,而找到流的顺序并不影响最大流的大小。

(6) Dinic 算法的优化

  • 无用节点删除:若从一个点 x 出发无法继续增广,令 depx=inf,即变相删除 x
  • 当前弧优化:从一个点 x 增广后可能有很多点 yc(x,y)=0,显然这些点在后续中无用,那么我们每次增广时标记 x 点之后第一个有用的点记为 cury,下次增广时从它开始遍历即可。

经过优化后的 Dinic 时间复杂度 上界O(V2E),但实际运用中往往远远不可达这个上界,因此引用 Dyc780511 的一句话:

我们通常认为 Dinic 的时间复杂度是 O()

参考代码:

int cur[N], dep[N];
int bfs() {
	for (int i = 1; i <= n; i++)
		dep[i] = inf;
	queue<int>q;
	q.push(s);
	cur[s] = head[s];
	dep[s] = 0;
	while (!q.empty()) {
		int x = q.front();
		q.pop();
		for (int i = head[x]; i; i = e[i].nxt) {
			int y = e[i].to;
			if (e[i].val > 0 && dep[y] == inf) {
				q.push(y);
				cur[y] = head[y];
				dep[y] = dep[x] + 1;
				if (y == t)
					return 1;
			}
		}
	}
	return 0;
}
int dfs(int x, int sum) {
	if (x == t)
		return sum;
	int k, flow = 0;
	for (int i = cur[x]; i && sum; i = e[i].nxt) {
		int y = e[i].to;
		if (e[i].val > 0 && dep[y] == dep[x] + 1) {
			k = dfs(y, min(sum, e[i].val));
			if (k == 0)
				dep[y] = inf;
			e[i].val -= k;
			e[i ^ 1].val += k;
			sum -= k;
			flow += k;
		}
	}
	return flow;
}
int dinic() {
	int ans = 0;
	while (bfs())
		ans += dfs(s, inf);
	return ans;
}

2. 最大流建模技巧

(1) 拆点

例1:[SCOI2007] 蜥蜴

显然 ans= 总蜥蜴数 最大流。考虑如何限制每个石柱的高度。将石柱拆成入点和出点,在两点之间连流量为石柱高度的边即可。

路径问题拆出入点是一个常用的思路。

例2:[HNOI2007] 紧急疏散

考虑如何处理“每一秒只能有一个人离开”。将每个门 按照时间 拆成一些个点,把这些个点和汇点 t 连边权为 1 的边,每个人向到达门所用时间的那个点连边权为 1 的边,同一个门每个时间之间连流量为 inf 的边即可。

按照时间拆点不常见,但是要掌握这个 trick.

(2) 二分图

例1:SDOI2015 星际战争

考虑二分时间,将机器人放在一边,武器放在一边,相互连边描述每一个状态即可。

例2:SCOI2012 奇怪的游戏

方格题考虑 黑白染色。黑白染色的目的是把点分为两类,构成一个二分图来网络流。然后按照奇偶性分类,用网络流来 check 即可。

三、最小割问题

1. 最大流最小割定理

(1) 内容

对于任意一张网络,其最大流等于最小割。

(2) 证明

引理:对于一张网络,其任意一个割均 最大流。

证明:假设该网络有一个割 < 最大流。那么这个网络不再连通,无法找到增广路。由最大流的定义,该网络的最大流就是这个割。结合假设,这个假设是矛盾的,原命题成立。那么最小割一定 最大流。

现在考虑如何证明其能取到最大流。

当原图跑到最大流时,原图不存在增广路。那么在最大流的每一个流中总有流量最小的边为 该流的流量限制,这些边流量相加即为最大流,同时这些边也可以组成一个最小割。

于是当我们想求出最小割时,求出原网络最大流即可。

2. 最小割建模技巧

(1) 环式建模

以我所见,最小割的建模通常是环式建模,而环式建模通常有两种。我称一种为“双环式”,另一种为“单环式”,其中“双环式”较为简单暴力,好想但时间复杂度高;“单环式”需要解方程,时间复杂度更优。以我所见,选择“双环式”建模的人更多一些。

两种建模我均选择 [国家集训队] happiness 讲解。这道题的基本思路是先假设全部满足,一边为文科,一边为理科建出二分网络,求出最小割使得每个人只能选择其中一边。

a. 双环建模

双环建模大多数长这个样子:

图3

其中 i,j 是原图中需要建的点,p,q 是辅助点,用来描述 i,j 之间处于同一侧的权值。具体地,si,it,sj,jt 分别赋予 i,j 不处于 st 集合的流量,sp,qt 连流量为 i,j 不同时属于 st 集合的损失的边,pi,iq,pj,jq 赋予 inf 的流量,这样建图 一目了然,十分直观,不容易出锅 ;但缺点是若原先就有 n 个点,这样建图之后会有约 n2 级别的点,复杂度不优。

注意根据题意分析,有时有些边是不用连的。

b. 单环建模

单环建模大多数长这个样子:

图4

我们分别计算切断每组边的损失。令 s 为文科,t 为理科,选文科的收益分别为 pi,pj,选理科的收益分别为 qi,qj,都选理科的收益为 m,都选文科的收益为 n

由题意可以列出方程:

{a+c=qi+qj+mb+d=pi+pj+na+e+d=qi+pj+m+nb+e+c=pi+qj+m+n

结合不难得到

{a=qi+m2b=pi+n2c=qi+m2d=pi+n2e=m+n2

于是建图时将所有权值全部 ×2,计算答案时除回去即可。这样做看上去较为复杂,但套路化之后就不难了。

(2) 切糕模型

给出 n 个变量 xi,其中每个变量的取值范围是[1,m]。对于一个变量 xi,取 j 的代价为 ai,j。同时,给出若干限制条件 (u,v,w),表示 xuxvwxuxvw

请求出一种合法的赋值方案,使得代价总和最小。

将每个变量 xi 拆成 pi,1,pi,2pi,m,表示每个变量选 [1,m] 的情况。对于每个变量建出一条链,Spi,1pi,2pi,mT,相邻两点之间的流量是 ai,j,连接源汇点的边流量为 inf。那么断掉哪一条边,就表示该变量选择了哪个取值。

同时对于 xuxvw,连接边 pv,ipu,i+w,流量为 inf。一般地,最小割中流量为 inf 的边表示改变无法断开。具体到本题,表示若选择违背了限制,显然 S,T 仍然可以连通,无法产生最小割。

3. 最小割的推论

将跑完最大流的残量网络进行缩点,流量为 0 的边视为不连通,得到一个 DAG。现在分析这个 DAG 的性质。

  • S,T 必然不连通

如果 S,T 仍连通,原图上仍有增广路,并未跑满最大流。

  • 对于满流边 (u,v),若 S,u 在同一 SCC, v,T 在同一 SCC,则 (u,v) 必然在原图最小割上。

(u,v) 的流量增加,原图重新连通,则最大流流量增加,最小割容量增加,因此 (u,v) 必然出现在最小割中。

  • (u,v) 不在同一 SCC,(u,v) 可能在原图最小割中。

显然原图缩点之后得到的新图只有满流边,原图的任意一个割都是最小割,都可出现在最小割中。

4. 最小割树

求出网络上任意两点的最小割,如果暴力来求,复杂度是 O(V4E) 的。采用最小割树可优化到 O(V3E)

下面介绍最小割树的构建方法。

  1. 选择任意两个点 u,v,求出最小割,并在新图中将 u,v 之间连接一条边权为最小割的边。
  2. 将整张图的节点分为两部分 U 集合和 V 集合,使得割后一部分与 u 联通,一部分与 v 联通。
  3. 分别递归调用与 u 联通的部分和与 v 联通的部分,即 U 集合和 V 集合。

于是任意两点的最小割,就是它们在最小割树上的简单路径的最小值。具体代码的实现上,将 U 集合放在序列的左端,V 集合放在序列的右端,由于最小割树的性质,对于 aU,bV,c(a,b)=min(c(a,u),c(u,v),c(v,b))。显然 c(a,u)c(v,b) 在我们递归调用的过程中已知, c(u,v) 我们刚刚求得,于是将两点间最小割存储到数组中,复杂度为 O(V3+Q)

代码:

#include <iostream>
#include <queue>
#include <unordered_map>
#define N 855
#define M 17505
#define inf 1000000000
using namespace std;
unordered_map<int, int>mp;
int n, m;
struct Netflow {
	int s, t;
	struct Node {
		int to, nxt, val, fval;
	}e[M];
	int head[N], cnt = 1;
	void add(int u, int v, int w) {
		e[++cnt].to = v;
		e[cnt].fval = w;
		e[cnt].nxt = head[u];
		head[u] = cnt;
	} 
	void build() {
		scanf("%d%d", &n, &m);
		for (int i = 1; i <= m; i++) {
			int x, y, w;
			scanf("%d%d%d", &x, &y, &w);
			add(x, y, w);
			add(y, x, w);
		}
	}
	int cur[N], dep[N];
	int bfs() {
		for (int i = 1; i <= n; i++)
			dep[i] = inf;
		queue<int>q;		
		q.push(s);
		dep[s] = 0;
		cur[s] = head[s];
		while (!q.empty()) {
			int x = q.front();
			q.pop();
			for (int i = head[x]; i; i = e[i].nxt) {
				int y = e[i].to;
				if (e[i].val > 0 && dep[y] == inf) {
					q.push(y);
					dep[y] = dep[x] + 1;
					cur[y] = head[y];
					if (y == t)
						return 19260817;
				}
			} 
		}
		return 0;
	}
	int dfs(int x, int sum) {
		if (x == t)
			return sum;
		int k, flow = 0;
		for (int i = cur[x]; i && sum; i = e[i].nxt) {
			int y = e[i].to;
			if (e[i].val > 0 && dep[y] == dep[x] + 1) {
				k = dfs(y, min(e[i].val, sum));
				e[i].val -= k;
				e[i ^ 1].val -= k;
				sum -= k;
				flow += k; 
			}
		}
		return flow;
	}
	int dinic() {
		for (int i = 2; i <= cnt; i++)
			e[i].val = e[i].fval;
		int res = 0;
		while (bfs())
			res += dfs(s, inf);
		return res;
	}
} Nf;

struct GHT {
	int node[N], tmp[N];
	int cut[N][N];
	void solve(int l, int r) {
		if (l == r)
			return;
		Nf.s = node[l];
		Nf.t = node[r];
		int k = Nf.dinic(), s = node[l], t = node[r];
		cut[s][t] = cut[t][s] = k;
		int L = l, R = r;
		for (int i = l; i <= r; i++) {
			if (Nf.dep[node[i]] < inf)
				tmp[L++] = node[i];
			else
				tmp[R--] = node[i];
		}
		for (int i = l; i <= r; i++)
			node[i] = tmp[i];
		solve(l, L - 1);
		solve(L, r);
		for (int i = l; i < L; i++)
			for (int j = L; j <= r; j++) {
				cut[node[i]][node[j]] = cut[node[j]][node[i]] = min(min(cut[node[i]][s], cut[node[j]][t]), cut[s][t]);
				mp[cut[node[i]][node[j]]] = 1;
			}
	}
	void build() {
		for (int i = 1; i <= n; i++)
			node[i] = i, cut[i][i] = inf;
		solve(1, n);
		cout << mp.size() << "\n";
	}
} Ght;

int main() {
	Nf.build();
	Ght.build();
	return 0;
}

5. 最大权闭合子图

最大权值闭合图,即给定一张有向图,每个点都有一个权值(可以为正或负或 0),你需要选择一个权值和最大的子图,使得子图中每个点的后继都在子图中。

这个问题可以采用最大流来解决。建立超级源点 s 和超级汇点 t,若节点 u 权值为正,则 su 连一条有向边,边权即为该点点权;若节点 u 权值为负,则由 ut 连一条有向边,边权即为该点点权的相反数。原图上所有边权改为 。跑最大流,将所有正权值之和减去最大流,即为答案。

考虑证明这个做法。

  1. 最小割一定是简单割,即与 st 相连的边集组成的割。这一点是显然的。

  2. 每一个符合条件的子图都对应流量网络中的一个割。钦定闭合子图 V 和源点 s 构成 S 集,其余点和汇点 t 构成 T 集。

    • 证明闭合子图是简单割:若割 (S,T) 不是简单割,则存在 (x,y),xS,yT,dis(x,y)=。那么 y 一定不在 S 中,矛盾。
    • 证明简单割是闭合子图:对于 V 中任意一个点 uuSu 的任意一条出边 dis(u,v)=,不会在简单割的割边集中,因此 v 不属于 T,于是 vS。所以 V 的所有点均在 S 中,因此 Ss 构成的边集对应的点是闭合子图。

四、费用流

1. 最小费用最大流

显然,一张网络的最大流不一定只有一个。若每条边的单位流量都有费用,考虑寻找一个流使得在流量最大的前提下费用最小。

考虑费用流中反向边的问题。撤回流量的同时费用要同时撤回,因此反向边的费用为正向边的相反数。

显然 对于费用和最小的增广路,我们想让它的流量最大,因此要尽早走。于是我们需要求最短路。考虑到有负边权,我们使用 SPFA。一般地,我们使用 EK 单源增广寻找增广路,因此我们通常使用 EK+SPFA 的费用流。复杂度为 O(V2E2),但显然这只是上界。

注意求出一条流之后将所有边的流量恢复为原值。

代码:

int dis[N];
int inr[N];
int pre[N];
bool vis[N];
int SPFA() {
	for (int i = 1; i <= n; i++) {
		dis[i] = inf;
		vis[i] = 0;
	}
	queue<int>q;
	q.push(s);
	dis[s] = 0;
	inr[s] = inf;
	vis[s] = 1;
	while (!q.empty()) {
		int x = q.front();
		vis[x] = 0;
		q.pop();
		for (int i = head[x]; i; i = e[i].nxt) {
			int y = e[i].to;
			if (e[i].flow > 0 && dis[x] + e[i].val < dis[y]) {
				dis[y] = dis[x] + e[i].val;
				inr[y] = min(inr[x], e[i].flow);
				pre[y] = i;
				if (!vis[y]) 
					vis[y] = 1, q.push(y);
			}
		}
	}
	return dis[t] < inf;
}
int maxflow, mincost;
void MCMF() {
	while (SPFA()) {
		maxflow += inr[t];
		mincost += inr[t] * dis[t];
		int i;
		for (int x = t; x != s; x = e[i ^ 1].to) {
			i = pre[x];
			e[i].flow -= inr[t];
			e[i ^ 1].flow += inr[t];
		}
	}
}

2.费用流常见建模

其实费用流的建模没什么好讲的,掌握好最大流的建模套路其实就差不多了。

五、上下界网络流

1. 无源汇上下界网络流

首先需要明确的是上下界网络并不一定有可行流。首先一个流可行的前提是所有边的流量达到下界,于是先让流满足下界的条件,再考虑将流修改为出入平衡。提升至下界后显然每个点的出/入流量不一定平衡,但净流量相加一定为 0。于是我们人为设置源点 s 和汇点 t,对于每个点 x,若入流量不足,则连接流量为以需要的流量大小为流量的边 sx,出流量不足的点同样连接以需要的流量为大小的边 xt。其余的点之间我们两两连边,边权设置为 rl 即可。最终如果有解,那么虚拟源点 s 流出的所有流量都会流到 t,判断流到 t 的流量是否等于从 s 流出的流量之和即可。

代码 of LOJ #115. 无源汇有上下界可行流

#include <bits/stdc++.h>
#define N 205
#define M 100005
#define inf 0x3f3f3f3f
using namespace std;
int n, m;
struct Dinic {
	int n, s, t;
	struct Node {
		int to, nxt, vl;
	} e[M];
	int head[N], cnt = 1;
	int cur[N], dis[N];
	void add(int u, int v, int w) {
		e[++cnt].to = v;
		e[cnt].vl = w;
		e[cnt].nxt = head[u];
		head[u] = cnt;
	}
	void ist(int x, int y, int w) {
		add(x, y, w);
		add(y, x, 0);
	}
	int bfs() {
		for (int i = 1; i <= n; i++) dis[i] = inf;
		queue<int>q;
		q.push(s);
		dis[s] = 0;
		cur[s] = head[s];
		while (q.size()) {
			int x = q.front();
			q.pop();
			for (int i = head[x]; i; i = e[i].nxt) {
				int y = e[i].to;
				if (e[i].vl > 0 && dis[y] == inf) {
					dis[y] = dis[x] + 1;
					cur[y] = head[y];
					q.push(y);
					if (y == t) return 1;
				}
			}
		}
		return 0;
	}
	int dfs(int x, int sm) {
		if (x == t) return sm;
		int k, fl = 0;
		for (int i = cur[x]; i && sm; i = e[i].nxt) {
			int y = e[i].to;
			if (e[i].vl > 0 && dis[y] == dis[x] + 1) {
				k = dfs(y, min(sm, e[i].vl));
				if (k == 0) dis[y] = inf;
				e[i].vl -= k;
				e[i ^ 1].vl += k;
				fl += k;
				sm -= k;
			}
		}
		return fl;
	}
	int dinic() {
		int ans = 0;
		while (bfs()) ans += dfs(s, inf);
		return ans;
	}
} dc;
int l[M], r[M];
int fl[M];
int ans;
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n >> m;
	dc.s = n + 1, dc.t = dc.n = n + 2;
	for (int i = 1; i <= m; i++) {
		int x, y;
		cin >> x >> y >> l[i] >> r[i];
		fl[x] -= l[i];
		fl[y] += l[i];
		dc.ist(x, y, r[i] - l[i]);
	}
	for (int i = 1; i <= n; i++) {
		if (fl[i] > 0) dc.ist(dc.s, i, fl[i]), ans += fl[i];
		else if(fl[i] < 0) dc.ist(i, dc.t, -fl[i]);
	}
	if (ans == dc.dinic()) {
		cout << "YES\n";
		for (int i = 1; i <= m; i++) cout << l[i] + dc.e[(i << 1) + 1].vl << "\n";
	}
	else cout << "NO\n";
	return 0;
}

2. 有源汇上下界网络流

(1) 有源汇上下界可行流

考虑有了源汇点之后如何不考虑 s,t 收支平衡的限制。考虑在网络上连边 ts,流量为 [0,+),这样连边的目的是使得所有由 s 流出的流量到 t 之后回到 s,使 s,t “在名义上"收支平衡。同时这样一来网络的流量就是 ts 流过的流量,也就是残量网络上其反向边上的流量。

(2) 有源汇上下界最大流

最大流的前提是可行,于是先求出一组可行流。由于可行流已经出入平衡,于是在原图的残量网络跑最大流即可,再加上可行流的流量即可。

但是由于 st 的边是我们强加上去的,统计的时候要删去。

代码 of LOJ #116. 无源汇有上下界可行流

#include <bits/stdc++.h>
#define N 205
#define M 100005
#define inf 0x3f3f3f3f
using namespace std;
int n, m, ans;
struct Dinic {
	int n, s, t, S, T;
	struct Node {
		int to, nxt, vl;
	} e[M];
	int head[N], cnt = 1;
	int cur[N], dis[N];
	void add(int u, int v, int w) {
		e[++cnt].to = v;
		e[cnt].vl = w;
		e[cnt].nxt = head[u];
		head[u] = cnt;
	}
	void ist(int x, int y, int w) {
		add(x, y, w);
		add(y, x, 0);
	}
	void clr(int x) {
		e[x].to = e[x].vl = 0;
	}
	void del(int x) {
		clr(x);
		clr(x ^ 1);
	}
	int bfs(int s, int t) {
		for (int i = 1; i <= n; i++) dis[i] = inf;
		queue<int>q;
		q.push(s);
		dis[s] = 0;
		cur[s] = head[s];
		while (q.size()) {
			int x = q.front();
			q.pop();
			for (int i = head[x]; i; i = e[i].nxt) {
				int y = e[i].to;
				if (e[i].vl > 0 && dis[y] == inf) {
					dis[y] = dis[x] + 1;
					cur[y] = head[y];
					q.push(y);
					if (y == t) return 1;
				}
			}
		}
		return 0;
	}
	int dfs(int x, int sm, int t) {
		if (x == t) return sm;
		int k, fl = 0;
		for (int i = cur[x]; i && sm; i = e[i].nxt) {
			int y = e[i].to;
			if (e[i].vl > 0 && dis[y] == dis[x] + 1) {
				k = dfs(y, min(e[i].vl, sm), t);
				if (k == 0) dis[y] = inf;
				e[i].vl -= k;
				e[i ^ 1].vl += k;
				fl += k;
				sm -= k;
			}
		}
		return fl;
	}
	void dinic() {
		int res = 0;
		while (bfs(S, T)) res += dfs(S, inf, T);
		if (ans != res) cout << "please go home to sleep\n";
		else {
			ans = e[cnt].vl;
			del(cnt);
			res = 0;
			while (bfs(s, t)) ans += dfs(s, inf, t);
			ans -= res;
			cout << ans << "\n";
		}
	}
} dc;
int fl[M], l[M], r[M];

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n >> m >> dc.s >> dc.t;
	dc.S = n + 1, dc.T = dc.n = n + 2;
	for (int i = 1; i <= m; i++) {
		int x, y;
		cin >> x >> y >> l[i] >> r[i];
		fl[x] -= l[i];
		fl[y] += l[i];
		dc.ist(x, y, r[i] - l[i]);
	}
	for (int i = 1; i <= n; i++) {
		if (fl[i] > 0) dc.ist(dc.S, i, fl[i]), ans += fl[i];
		else if (fl[i] < 0) dc.ist(i, dc.T, -fl[i]);
	}
	dc.ist(dc.t, dc.s, inf);
	dc.dinic();
	return 0;
}

(3) 有源汇上下界最小流

这个东西和最大流是相似的,处理的方法是将多算的流量退回,于是从汇点到源点跑一遍最大流,用可行流减去最大流即可。

代码 of LOJ #117. 无源汇有上下界可行流

#include <bits/stdc++.h>
#define N 50105
#define M 1005005
#define inf 0x3f3f3f3f
using namespace std;
int n, m, ans;
struct Dinic {
	int n, s, t, S, T;
	struct Node {
		int to, nxt, fl;
	} e[M];
	int head[N], cnt = 1;
	void add(int u, int v, int w) {
		e[++cnt].to = v;
		e[cnt].fl = w;
		e[cnt].nxt = head[u];
		head[u] = cnt;
	}
	void ist(int x, int y, int w) {
		add(x, y, w);
		add(y, x, 0);
	}
	int dis[N], cur[N];
	int bfs(int s, int t) {
		for (int i = 1; i <= n; i++) dis[i] = inf;
		queue<int>q;
		q.push(s);
		dis[s] = 0;
		cur[s] = head[s];
		while (q.size()) {
			int x = q.front();
			q.pop();
			for (int i = head[x]; i; i = e[i].nxt) {
				int y = e[i].to;
				if (e[i].fl > 0 && dis[y] == inf) {
					dis[y] = dis[x] + 1;
					cur[y] = head[y];
					q.push(y);
					if (y == t) return 1;
				}
			}
		}
		return 0;
	}
	int dfs(int x, int sm, int t) {
		if (x == t) return sm;
		int k, fl = 0;
		for (int &i = cur[x]; i && sm; i = e[i].nxt) {
			int y = e[i].to;
			if (e[i].fl > 0 && dis[y] == dis[x] + 1) {
				k = dfs(y, min(e[i].fl, sm), t);
				if (k == 0) dis[y] = inf;
				e[i].fl -= k;
				e[i ^ 1].fl += k;
				fl += k;
				sm -= k;
			}
			if (sm == 0) return fl; 
		}
		return fl;
	}
	void dinic() {
		int res = 0;
		while (bfs(S, T)) res += dfs(S, inf, T);
		if (ans != res) cout << "please go home to sleep\n";
		else {
			ans = e[cnt].fl;
			res = 0;
			e[cnt].fl = e[cnt].to = e[cnt ^ 1].fl = e[cnt ^ 1].to = 0;
			while (bfs(t, s)) res += dfs(t, inf, s);
			ans -= res;
			cout << ans << "\n";
		}
	}
} dc;
int fl[N];
void add(int x, int y, int l, int r) {
	fl[x] -= l;
	fl[y] += l;
	dc.ist(x, y, r - l);
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n >> m >> dc.s >> dc.t;
	dc.S = n + 1, dc.T = dc.n = dc.S + 1;
	for (int i = 1; i <= m; i++) {
		int x, y, l, r;
		cin >> x >> y >> l >> r;
		add(x, y, l, r);
	}
	for (int i = 1; i <= n; i++) {
		if (fl[i] > 0) dc.ist(dc.S, i, fl[i]), ans += fl[i];
		else if(fl[i] < 0) dc.ist(i, dc.T, -fl[i]);
	}
	dc.ist(dc.t, dc.s, inf);
	dc.dinic();
	return 0;
}

(4) 有源汇上下界最小费用可行流

这个东西其实很简单,将所有边的下界乘上费用再跑一遍 MCMF 计入答案即可。

扔一下代码:

struct Dinic {
	int n, s, t, S, T;
	struct Node {
		int to, nxt, fl, dis;
	} e[M];
	int head[N], cnt = 1;
	void add(int u, int v, int fl, int w) {
		e[++cnt].to = v;
		e[cnt].fl = fl;
		e[cnt].dis = w;
		e[cnt].nxt = head[u];
//		if (cnt == 50) cerr << "K " << u << ' ' << v << '\n';
		head[u] = cnt;
	}
	void ist(int x, int y, int fl, int w) {
		add(x, y, fl, w);
		add(y, x, 0, -w);
	}
	int dis[N], inr[N], pre[N];
	bool vis[N];
	int SPFA(int s, int t) {
		for (int i = 1; i <= n; i++) dis[i] = inf, vis[i] = 0;
		queue<int>q;
		q.push(s);
		dis[s] = 0;
		inr[s] = inf;
		while (q.size()) {
			int x = q.front();
			q.pop();
			vis[x] = 0;
			for (int i = head[x]; i; i = e[i].nxt) {
				int y = e[i].to;
				if (e[i].fl > 0 && dis[y] > dis[x] + e[i].dis) {
					dis[y] = dis[x] + e[i].dis;
					inr[y] = min(inr[x], e[i].fl);
					pre[y] = i;
					if (!vis[y]) {
						vis[y] = 1;
						q.push(y);
					}
				}
			}
		}
		return dis[t] < inf;
	}
	void MCMF() {
		while (SPFA(S, T)) {
			ans += dis[T] * inr[T];
			int i;
			for (int x = T; x != S; x = e[i ^ 1].to) {
				i = pre[x];
				e[i].fl -= inr[T];
				e[i ^ 1].fl += inr[T];
			}
		}
	}
} dc;
posted @   长安19路  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示