首页 > 其他分享 >网络流学习笔记

网络流学习笔记

时间:2024-08-17 21:05:16浏览次数:13  
标签:增广 int 网络 笔记 最小 学习 dep 流量 rightarrow

网络流学习笔记

一、定义

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

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

这句话的意思是对于网络流中的反向边,流量应为 \(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)\)。

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

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

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

对于网络 \(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) 增广路

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

(2) FF 算法

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

(3) 反向边与撤销影响

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

考虑这样一张网络:图1
显然最优方案是 \(s\rightarrow a \rightarrow t,s\rightarrow b\rightarrow t\)。但如果找到的增广路是 \(s\rightarrow a \rightarrow b \rightarrow t\) 呢?

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

考虑这样做的实际意义:\(b\rightarrow t\) 的流量实际由 \(s\rightarrow b\) 来最终承担了,而 \(a\rightarrow t\) 的流量实际由 \(s\rightarrow a\) 承担。\(a\rightarrow b\) 本身不承担任何意义,只是过渡使用。

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

(4) EK 算法

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

(5) Dinic 算法

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

  1. 对原图依据每个点到源点的距离 \(dep_x\) 进行 bfs 分层,暂时删除 \(dep_v<dep_u\) 的边 \((u, v)\)

  2. 对原图进行 dfs 找到增广路

  3. 重复上面算法直到源汇点不再连通。

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

(6) Dinic 算法的优化

  • 无用节点删除:若从一个点 \(x\) 出发无法继续增广,令 \(dep_x=\infty\),即变相删除 \(x\)。

  • 当前弧优化:从一个点 \(x\) 增广后可能有很多点 \(y\) 有\(c(x,y)=0\),显然这些点在后续中无用,那么我们每次增广时标记 \(x\) 点之后第一个有用的点记为 \(cur_y\),下次增广时从它开始遍历即可。

经过优化后的 Dinic 时间复杂度 上界 是 \(O(V^2E)\),但实际运用中往往远远不可达这个上界,因此引用 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\) 的边,同一个门每个时间之间连流量为 \(\infty\) 的边即可。

按照时间拆点不常见,但是要掌握这个 \(\text{trick.}\)

(2) 二分图

例1:SDOI2015 星际战争

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

例2:SCOI2012 奇怪的游戏

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

注意数学问题 按照奇偶性分类是一个极为常用的做法,本质目的是为了构成一个二分图好跑网络流。

三、最小割问题

1. 最大流最小割定理

(1) 内容

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

(2) 证明

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

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

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

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

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

2. 最小割建模技巧

(1) 环式建模

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

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

a. 双环建模

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

图3

其中 \(i,j\) 是原图中需要建的点,\(p,q\) 是辅助点,用来描述 \(i,j\) 之间处于同一侧的权值。具体地,\(s\rightarrow i,i\rightarrow t,s\rightarrow j,j\rightarrow t\) 分别赋予 \(i,j\) 不处于 \(s\) 或 \(t\) 集合的流量,\(s\rightarrow p,q\rightarrow t\) 连流量为 \(i,j\) 不同时属于 \(s\) 或 \(t\) 集合的损失的边,\(p\rightarrow i,i\rightarrow q,p\rightarrow j,j\rightarrow q\) 赋予 \(\infty\) 的流量,这样建图 一目了然,十分直观,不容易出锅 ;但缺点是若原先就有 \(n\) 个点,这样建图之后会有约 \(n^2\) 级别的点,复杂度不优。

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

b. 单环建模

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

图4

我们分别计算切断每组边的损失。令 \(s\) 为文科,\(t\) 为理科,选文科的收益分别为 \(p_i,p_j\),选理科的收益分别为 \(q_i,q_j\),都选理科的收益为 \(m\),都选文科的收益为 \(n\)。

由题意可以列出方程:

\[\begin{cases} a+c&=q_i+q_j+m \\ b+d&=p_i+p_j+n \\ a+e+d&=q_i+p_j+m+n \\ b+e+c&=p_i+q_j+m+n \end{cases} \]

结合不难得到

\[\begin{cases} a=q_i+\frac{m}{2}\\ b=p_i+\frac{n}{2}\\ c=q_i+\frac{m}{2}\\ d=p_i+\frac{n}{2}\\ e=\frac{m+n}{2} \end{cases} \]

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

(2) 切糕模型

给出 \(n\) 个变量 \(x_i\),其中每个变量的取值范围是\([1,m]\)。对于一个变量 \(x_i\),取 \(j\) 的代价为 \(a_{i,j}\)。同时,给出若干限制条件 \((u,v,w)\),表示 \(x_u-x_v\le w\) 或 \(x_u-x_v\ge w\)。

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

将每个变量 \(x_i\) 拆成 \(p_{i,1},p_{i,2}\cdots p_{i,m}\),表示每个变量选 \([1,m]\) 的情况。对于每个变量建出一条链,\(S\rightarrow p_{i,1}\rightarrow p_{i,2}\rightarrow \cdots \rightarrow p_{i,m}\rightarrow T\),相邻两点之间的流量是 \(a_{i,j}\),连接源汇点的边流量为 \(\infty\)。那么断掉哪一条边,就表示该变量选择了哪个取值。

同时对于 \(x_u-x_v\ge w\),连接边 \(p_{v,i}\rightarrow p_{u,i+w}\),流量为 \(\infty\)。一般地,最小割中流量为 \(\infty\) 的边表示改变无法断开。具体到本题,表示若选择违背了限制,显然 \(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(V^4E)\) 的。采用最小割树可优化到 \(O(V^3E)\)。

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

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

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

四、费用流

(1) 最小费用最大流

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

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

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

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

代码:

int dis[N];
int inr[N];
int vis[N];
int pre[N];
int s, t;
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();
		q.pop();
		vis[x] = 0;
		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()) {
		mincost += dis[t] * inr[t];
		if (mincost > 0) {
			mincost -= dis[t] * inr[t];
			mincost = -mincost;
			maxflow += mincost / dis[t];
			return;
		}
		maxflow += inr[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) 费用流常见建模

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

未完待续~~~

标签:增广,int,网络,笔记,最小,学习,dep,流量,rightarrow
From: https://www.cnblogs.com/Rock-N-Roll/p/18364972

相关文章

  • 如何用ChatGPT 4.0创作高质量的小红书笔记?
    小红书(RED)作为一个内容分享平台,吸引了大量年轻用户,他们在这里寻找生活灵感和消费建议。要在这个竞争激烈的平台上脱颖而出,高质量的笔记至关重要。ChatGPT4.0作为一款强大的AI工具,可以帮助创作者生成内容并优化笔记,提高曝光率和互动率。本文将详细介绍如何利用ChatGPT4.0创作......
  • 学习Java的第七周
    第七周的学习记录来啦,历时许久终于接触到了一点课程核心,本周的学习从面向对象开始,所谓更符合人类思维习惯的、使编程更简单的Java核心……学习了获取已有对象并使用,如何让自己设计对象并使用(语法),重心更倾向于后者,这一周前一半内容主要在封装、成员和局部、构造方法、标准的javabea......
  • 以node / link文件表征的道路网络-----dijkstra算法yyds-----基于南京公路公开数据做
    前文已经基于公开数据,获得了南京的全域高速公路的路网数据,这些以node/link文件表征的道路网络不仅延续了osm地图中所包含的经纬度、名称、容量等信息,还包含了一个重要的道路等级字段“link_type_name”。交通部门一般以高速公路、国省干道、城市道路、乡道农路作为区分......
  • 每周JAVA学习汇总
    在Java学习中,字符串比较、集合、静态变量、继承与子类是几个重要的概念。以下是对这些概念的汇总:字符串比较(1)使用equals()方法:比较字符串内容是否相同,区分大小写。javaStringstr1="Hello";Stringstr2="hello";booleanresult=str1.equals(str2);//返回false(2)使......
  • 如何高效记录并整理编程学习笔记
     对于学习编程语言的我们,需要不断的学习,与时俱进才能跟上现在社会的高速发展,但学习的同时还需要不断巩固之前学习的知识,对于如何学习我个人总结了以下方法共大家参考学习如何做笔记和整理学习资料:选择合适的笔记方式,如手写、博客或电子笔记等。抓住重点,记录老师强调的内容、......
  • 【C++进阶学习】第十三弹——C++智能指针的深入解析
    前言:在C++编程中,内存管理是至关重要的一个环节。传统的手动内存管理方式容易导致内存泄漏、悬挂指针等问题。为了解决这些问题,C++引入了智能指针。本文将详细讲解C++中智能指针的概念、种类、使用方法以及注意事项。目录一、引言二、智能指针的原理及目的2.1智能指针......
  • 算法学习笔记之树链剖分
    算法学习笔记之(熟练跑分)树链剖分PART1首先是第一部份,也就是熟练跑分最最最基础的用法——求\(LCA\)首先是树链剖分//图片出自董晓算法大概就是这样本质就是根据子树大小将一颗树剖分成若干条链然后更加方便地处理/加速处理信息所以直接上代码?不,还要证明树链剖......
  • opencascade Adaptor3d_Curve源码学习
    opencascadeAdaptor3d_Curve前言用于几何算法工作的3D曲线的根类。适配曲线是曲线提供的服务与使用该曲线的算法所需服务之间的接口。提供了两个派生具体类:GeomAdaptor_Curve,用于Geom包中的曲线Adaptor3d_CurveOnSurface,用于Geom包中表面上的曲线。用于评估BSpline......
  • Datawhale X魔搭 AI夏令营 第四期 AIGC方向 task03笔记
    一、ComfyUI  1、这次课首先介绍了ComfyUI,什么是ComfyUI?ComfyUI主要用于生成图像。它采用基于节点的工作流程,为用户提供更大的控制力和灵活性。用户可以通过连接不同的节点来直观地构建工作流程,并且允许对图像进行高级定制。用户还可以拖放节点、轻松调整参数,并实时查看......
  • Web 端项目系统访问页面很慢,后台数据返回很快,网络也没问题,是什么导致的呢?
    一、前端方面可能的原因1.页面加载过多资源•可能页面中包含了大量的图片、视频、脚本等资源,这些资源的加载会占用大量时间。可以检查页面的资源大小和数量,看是否有可以优化的地方,比如压缩图片、合并脚本等。2.前端代码效率问题•检查前端代码是否存在性能瓶颈。例如,Ja......