网络流

1 网络流基础概念

网络流的概念分为网络和流。

网络是指一种特殊的有向图 G=(V,E),每条边上有容量 c(u,v) ,同时还有源汇点 s,t

对于一个网络,一个流 f 需要满足以下性质:

  • 每条边上的流量 f(u,v) 不能大于它的容量 c(u,v)
  • 每个点流入的流量等于流出的流量。

而对于整个网络和它上面的流 f,定义流的流量 |f| 为源点流出的流量之和。根据第二条性质,它也等于汇点流入的流量。

网络流有很多问题和模型,下面详细讲解。

2 最大流

2.1 问题概述

对于一个网络,找到一个流,使得流的流量最大。

通常情况下,我们使用 Dinic 算法求解最大流。在此之前需要先了解 FF 增广。

2.2 FF 算法

即 Ford-Fulkerson 算法,是一种计算最大流的算法的总称,基于贪心思想。

首先我们对于一个网络 G 和一个流 f 给出如下定义:

  • 对于一条边 (u,v),我们将其容量与流量之差称为剩余容量,记作 cf(u,v)
  • G 中所有剩余容量大于 0 的边和节点构成的子图称为残量网络,记作 Gf

我们将 Gf 上一条从源点到汇点的路径称作增广路,对于任意一条增广路,我们给每一条边都加上一个相等的流量,让整个网络的流量增加,这一过程叫做增广。

显然,我们可以将求解最大流的过程看做不断增广。因此 FF 的本质就是不断找增广路进行增广,直到找不到为止。

此时考虑这样的情况:

v2-e3aed80f1eab30d25ec9babfb897d68a_720w.webp (720×619) (zhimg.com)

假如我们此时找到的增广路为 1234,那么残量网络就会变成这样:

v2-c66b961adf5b5dcbf5bf52a91ca74c03_720w.webp (720×619) (zhimg.com)

此时已经不存在增广路了,然而最大流是 1 吗?其实不然,显然走 134,124 最大,流量为 2

为了解决这样的问题,我们引入反向边。我们约定 f(u,v)=f(v,u),即反向边的流量是正向边流量的相反数。为了保证这个性质,我们在增加 f(u,v) 值的时候,也要将 f(v,u) 减少。

可能我们会觉得负数的流量很诡异,不过我们的重点并不在于流量本身,而是残量网络。当正向边流量增加时,剩余容量减少;同时反向边流量减少,剩余容量增加。

例如下图:

v2-1c4016f73a2e94109fbb8769a1e88566_720w.webp (720×619) (zhimg.com)

我们在反方向建边权为 0 的边,此时我们再次找到 1234 这条增广路,正向的残量网络容量应该减一,而反向的残量网络容量要加一,如下图:

v2-f19ff404a0932ca1d6bdf713403142cf_720w.webp (720×619) (zhimg.com)

这时,我们还可以再找到一条增广路,即 1324

v2-b79f3fc5c921de2388012808f8bbed7d_720w.webp (720×619) (zhimg.com)

此时我们发现,2332 我们都走了,可以认为是这条边上的正向边与反向边互相抵消了,这样我们实际得出的路径就正好是 124,134 两条。

因此反向边的实质就是一种撤销,由于反向边时刻在加上正向边丢失的容量,它就代表着可以撤回的容量。

这就是 FF 算法的核心思想:残量网络和反向边。

接下来考虑如何实现这个算法,显然暴力 DFS 可行,但是时间复杂度过高,必须改进优化。

2.3 EK 算法

即 Edmonds-Karp 算法,利用 BFS 进行 FF 增广。

EK 算法的具体流程如下:

  • Gf 上如果可以从 s 出发走到 t,代表我们找到了增广路。
  • 在增广路上,求出剩余容量的最小值,给每条边的流量加上它,同时给反向边容量减去它。
  • 我们重复上述过程,直到没有增广路为止。

这就是 EK 算法。单轮 BFS 增广复杂度为 O(E),而增广轮数上界为 O(VE),那么 EK 算法的总时间复杂度就是 O(VE2)。当然这个复杂度是理论上界,不过即使这样这个复杂度也太劣了。

因此 FF 增广和 EK 算法都不是求最大流的主流算法,真正最有用的是下面这个。

2.4 Dinic 算法

Dinic 算法是对于 FF/EK 算法的优化,将两者使用的 DFS 和 BFS 相结合。

考虑到 EK 算法不优在哪里,由于每一次 BFS 都只能找到一条增广路,效率底下;同时受反向边影响,DFS 可能会来回绕圈子。

为了解决第二个问题,我们在增广前对 Gf 进行 BFS 分层,也就是根据节点 u 到源点 s 的距离 d(u) 将节点分为若干层。我们让每个节点 u 都只向自己的下一层的节点 v 进行增广,这样我们每次增广的都是原图中边数最少的路径。

接下来我们进行 DFS,为了解决第一个问题,我们采用多路增广。对于一个节点,如果从一个儿子走找到了增广路,我们不必回到 s 重新求解,而是可以从当前节点出发继续增广。

有了这样的指导,我们可以写出一个朴素的 Dinic 出来:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;

int n, m, s, t;

int head[Maxn], edgenum = 1;//和 tarjan 求桥很像,利用 edgenum=1 判断双向边 
struct node {
	int nxt, to, w;
}edge[Maxn];

void add(int from, int to, int w) {//加边要加双向边 
	edge[++edgenum] = {head[from], to, w};
	head[from] = edgenum;
	edge[++edgenum] = {head[to], from, 0};
	head[to] = edgenum;
}

int dis[Maxn];

bool bfs() {
	for(int i = 1; i <= n; i++) dis[i] = 0;
	queue <int> q;
	q.push(s);
	dis[s] = 1;
	while(!q.empty()) {
		int x = q.front();
		q.pop();
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to, w = edge[i].w;
			if(w > 0 && dis[to] == 0) {//找到还未遍历且还有剩余容量的 
				dis[to] = dis[x] + 1;//预处理距离 
				if(to == t) return 1;//找到一条 s->t 的路径 
				q.push(to);
			}
		}
	}
	return 0;//不存在增广路了 
}

int dfs(int x, int flow) {//上一层传入的流量 
	if(x == t) return flow;//找到汇点就返回 
	int rest = flow;//残量网络 
	for(int i = head[x]; i; i = edge[i].nxt) {
		int to = edge[i].to, w = edge[i].w;
		if(w > 0 && dis[to] == dis[x] + 1) {//还有剩余容量且在下一层 
			int k = dfs(to, min(rest, w));//计算下面的层能经过的流量,也就是当前点能经过的流量
			rest -= k;//剩余容量减少 
			edge[i].w -= k;
			edge[i ^ 1].w += k;//反向剩余容量增加 
		}
	}
	return flow - rest;//返回当前节点能经过的流量 
}

int ans = 0;

void dinic() {
	while(bfs()) {//重复找有无增广路,建立分层图 
		ans += dfs(s, Inf);//从源点出发找 
	}
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> s >> t;
	for(int i = 1; i <= m; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		add(u, v, w);
	}
	dinic();
	cout << ans << '\n';
	return 0;
}

我们发现,此时的 Dinic 使用的 DFS 本质上还是一个暴力,复杂度也并不优秀,因此我们还需要一个东西:当前弧优化。

观察代码,我们发现如果有一个节点 u,入边和出边都很多,那么每一次 u 接受来自入边的流量都要遍历出边决定将流量传递给谁,这样会使得时间复杂度骤增。我们考虑这样一种现象:如果对于一条边 (u,v),它已经被增广到了极限(边 (u,v) 没有剩余容量或 v 后侧已经不可能再被增广),此时再走 (u,v) 就变得毫无意义。

放到 Dinic 中来看,我们对于一个节点下一层的节点,一定会把这个节点之后的边的剩余容量榨干,此时我们就不需要再走这个节点了。因此对于每个节点 u,维护在它的出边中第一条还需要尝试的边,代码中体现为原本的 head[x] 换成这个指针 cur[x]

于是我们就可以保证 Dinic 的正确时间复杂度。对于单次 DFS,我们可以找到不超过 E 条的最短路径,每一条最短路径回溯不超过 V 次,同时当前弧优化保证不会经过相同节点。优化后每一次遍历的最短路径长度都会至少加一,因此至多寻找 V 次,所以复杂度为 O(V2E)

但是仔细思考会发现,如果一个图要满足上面提到的所有 “不超过” 条件来卡满复杂度是比较困难的。实际运用中,很少会有人专门卡 Dinic,因此这个复杂度仅仅是一个理论上限,大部分图中,Dinic 的表现都十分优秀。

当然 Dinic 还有一些别的常数优化,如下:

  • 剩余流量判断:如果上一层节点传递的流量已经消耗完了,就不用再进行 DFS。
  • 无用节点删除:如果我们将一些流量传给下一层节点,但是下一层节点返回的流量为 0,意味着这个节点无法再进行增广,我们将它删除。

所以最终版本的 Dinic 代码如下:

#include <bits/stdc++.h>
#define int long long

using namespace std;

typedef long long LL;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;

int n, m, s, t;

int head[Maxn], edgenum = 1;//和 tarjan 求桥很像,利用 edgenum=1 判断双向边 
struct node {
	int nxt, to, w;
}edge[Maxn];

int cur[Maxn];//当前弧 

void add(int from, int to, int w) {//加边要加双向边 
	edge[++edgenum] = {head[from], to, w};
	head[from] = edgenum;
	edge[++edgenum] = {head[to], from, 0};
	head[to] = edgenum;
}

int dis[Maxn];

bool bfs() {
	for(int i = 1; i <= n; i++) {
		dis[i] = 0;
		cur[i] = head[i];//当前弧优化的初始化 
	}
	queue <int> q;
	q.push(s);
	dis[s] = 1;
	while(!q.empty()) {
		int x = q.front();
		q.pop();
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to, w = edge[i].w;
			if(w > 0 && dis[to] == 0) {//找到还未遍历且还有剩余容量的 
				dis[to] = dis[x] + 1;//预处理距离 
				if(to == t) return 1;//找到一条 s->t 的路径 
				q.push(to);
			}
		}
	}
	return 0;//不存在增广路了 
}

int dfs(int x, int flow) {//上一层传入的流量 
	if(x == t) return flow;//找到汇点就返回 
	int rest = flow;//残量网络 
	for(int i = cur[x]; i && rest/*剩余容量判断*/; i = edge[i].nxt) {
		cur[x] = i;//当前弧优化 
		int to = edge[i].to, w = edge[i].w;
		if(w > 0 && dis[to] == dis[x] + 1) {//还有剩余容量且在下一层 
			int k = dfs(to, min(rest, w));//找到下面的层能经过的流量 
			if(k == 0) dis[to] = 0;//无用节点删除,dis 数组为 0 就永远不会在走了 
			rest -= k;//剩余容量减少 
			edge[i].w -= k;
			edge[i ^ 1].w += k;//反向剩余容量增加 
		}
	}
	return flow - rest;//返回当前节点能经过的流量 
}

int ans = 0;

void dinic() {
	while(bfs()) {//重复找有无增广路,建立分层图 
		ans += dfs(s, Inf);//从源点出发找 
	}
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> s >> t;
	for(int i = 1; i <= m; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		add(u, v, w);
	}
	dinic();
	cout << ans << '\n';
	return 0;
}

3 最小割

3.1 问题概述

对于一个网络 G,将其点集划分为两个点集 S,T,满足 sS,tT,则称 {S,T}G 的一个 st 割。

对于一个 st{S,T},定义其容量 ||S,T||uSvTc(u,v)

最小割问题就是求出这个容量的最小值。

3.2 最大流最小割定理

3.2.1 定理内容

对于一个网络,其最大流 f 和最小割 {S,T} 一定满足 |f|=||S,T||

3.2.2 定理证明

这个定理看上去很奇妙,下面我们尝试证明它。

首先我们先证明一个引理:对于一个网络 G,任取一个流 f 和割 {S,T},总会有 |f|||S,T||,其中等号成立当且仅当为 {(u,v),uS,vT} 都是满流而 {(u,v),uT,vS} 都是空流。下面是证明:

首先让我们定义一个点的净流量为 f(u)=f(u,x)f(x,u),显然由网络流性质 2 可得一个流的流量就是 s​​ 的净流量。然后得到下列式子:

|f|=f(s)=uSf(u)   ()=uS(f(u,v)f(v,u))=uS(vSf(u,v)+vTf(u,v)vSf(v,u)vTf(v,u))=uS(vTf(u,v)vTf(v,u))+uSvSf(u,v)uSvSf(v,u)=uS(vTf(u,v)vTf(v,u))uSvTf(u,v)   ( {(u,v),uT,vS} )uSvTc(u,v)   ( {(u,v),uS,vT} )=||S,T||

引理得证。

接下来我们需要证明第二个引理:对于一个网络,总会存在流 f 和割 {S,T},满足上面的取等条件。我们可以直接给出一种构造:

考虑 FF 增广的相关思路。

假设在几轮增广后,得到的流 f 使得在 Gf 上不存在增广路,即不存在 st 的路径。此时我们记从 s 出发可以到达的点集为 S,同时记剩下的所有点集为 T。显然此时 {S,T}Gf 的一个割,且 ||S,T||=uSvTcf(u,v)=0

接下来我们对 {(u,v),uS,vT} 中的边分为原图上的边和反向边进行讨论:

  • 对于原图上的边 (u,v):此时 cf(u,v)=c(u,v)f(u,v)=0,因此 c(u,v)=f(u,v),即满足 {(u,v),uS,vT} 都是满流。
  • 对于反向边 (v,u):此时 cf(v,u)=c(v,u)f(v,u)=0。由于反向边的 c(v,u)0,因此 f(v,u) 也都是 0,即满足 {(u,v),uT,vS} 都是空流。

因此总会存在流 f 和割 {S,T},满足 |f|=||S,T||。引理得证。

接下来我们看最大流 f 和最小割 {S,T} 的关系:

  • 由第一条引理,|f|||S,T||
  • 由第二条引理,由于 |f||f|,||S,T||||S,T||,而又必然存在 |f|=||S,T||,则可以得到 |f|||S,T||

因此对于最大流 f 和最小割 {S,T},总会有 |f|||S,T|||f|||S,T||

那么显然就可以得到 |f|=||S,T||​​,最大流最小割定理得证。

3.3 最小割树

3.3.1 概念及思想

我们知道求两点之间的最小割可以用 Dinic 算法,那如果我们要多次询问两点之间的最小割呢?

例题:【模板】最小割树(Gomory-Hu Tree)

首先明晰我们建的这个东西有什么性质,最小割树的性质就是对于任意两点 u,v,他们在图上的最小割就是最小割树上 u,v 之间简单路径的边权最小值。

这个性质听上去很美妙,现在我们看如何构建最小割树。

首先我们随意在图中选两个点 u,v,跑出他们之间的最小割 cut(u,v)。那么此时满流的边就是 u,v 的割。通过割将图划分为两部分,分别记作 U,V

接下来我们在一张新图上连接 u,v,边权就是 cut(u,v)。然后我们分别对于 U,V 递归进行上述过程,这样我们最后建出来的新图就成了一颗树。

此时我们查询树上 u,v 之间简单路径的边权最小值即可,因为在这棵树上,u,v 之间的每一条边都是一种将 u,v 割开的方案。

最后我们查询直接在树上查询即可,利用倍增或树剖。

据说好像这并不是真正的最小割树,而是一种叫等价流树的东西。

3.3.2 代码

#include <bits/stdc++.h>

using namespace std;

const int Maxn = 2e5 + 5;
const int Inf = 2e9;

int n, m, q;
int u[Maxn], v[Maxn], w[Maxn];

struct MinCut {
	int head[Maxn], edgenum = 1;
	struct node {
		int nxt, to, w;
	}edge[Maxn];
	void add(int u, int v, int w) {
		edge[++edgenum] = {head[u], v, w};
		head[u] = edgenum;
		edge[++edgenum] = {head[v], u, 0};
		head[v] = edgenum;
	}
	int dis[Maxn], cur[Maxn];
	bool bfs(int s, int t) {
		for(int i = 1; i <= n; i++) {
			dis[i] = 0;
			cur[i] = head[i];
		}
		queue <int> q;
		q.push(s);
		dis[s] = 1;
		while(!q.empty()) {
			int x = q.front();
			q.pop();
			for(int i = head[x]; i; i = edge[i].nxt) {
				int to = edge[i].to, w = edge[i].w;
				if(w > 0 && !dis[to]) {
					dis[to] = dis[x] + 1;
					if(to == t) return 1;
					q.push(to);
				}
			}
		}
		return 0;
	}
	int dfs(int x, int t, int flow) {
		if(x == t) return flow;
		int rest = flow;
		for(int i = cur[x]; i && rest; i = edge[i].nxt) {
			int to = edge[i].to, w = edge[i].w;
			if(w > 0 && dis[to] == dis[x] + 1) {
				int k = dfs(to, t, min(rest, w));
				if(k == 0) dis[to] = 0;
				rest -= k;
				edge[i].w -= k;
				edge[i ^ 1].w += k; 
			}
		}
		return flow - rest;
	}
	bool vis[Maxn];
	void solve(int x) {//从源点 s 出发,走没有满流的边,记录到的点都属于 S 集合
		vis[x] = 1;
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to, w = edge[i].w;
			if(w > 0 && !vis[to]) {
				solve(to);
			}
		}
	} 
	int dinic(int s, int t) {//注意:每次找最小割的时候都是在原图上找,不是在割下来的集合里找!
		int ans = 0;
		edgenum = 1;
		for(int i = 1; i <= n; i++) {
			head[i] = 0;
			vis[i] = 0;
		}
		for(int i = 1; i <= m; i++) {//初始化
			add(u[i], v[i], w[i]);
			add(v[i], u[i], w[i]);
		}
		while(bfs(s, t)) {
			ans += dfs(s, t, Inf);
		}
		solve(s);
		return ans;
	}
}MC;

int p[Maxn];
struct MinCutTree {
	int head[Maxn], edgenum;
	struct node {
		int nxt, to, w;
	}edge[Maxn];
	void add(int u, int v, int w) {
		edge[++edgenum] = {head[u], v, w};
		head[u] = edgenum;
	}
	int t1[Maxn], t2[Maxn];
	void build(int l, int r) {//建树
		if(l >= r) return ;
		int s = p[l], t = p[l + 1];//挑两个节点
		int res = MC.dinic(s, t);
		add(s, t, res), add(t, s, res);//连边
		int p1 = 0, p2 = 0;//下面这一部分就是一个普通的分治,很像归并排序
		for(int i = l; i <= r; i++) {
			if(MC.vis[p[i]]) {
				t1[++p1] = p[i];
			}
			else {
				t2[++p2] = p[i];
			}
		}
		for(int i = 1; i <= p1; i++) {
			p[l + i - 1] = t1[i];
		}
		for(int i = 1; i <= p2; i++) {
			p[l + p1 + i - 1] = t2[i];
		}
		build(l, l + p1 - 1);
		build(l + p1, r);
	}
	int fa[Maxn][21], minn[Maxn][21], dep[Maxn];
	void dfs(int x, int f) {//倍增预处理
		dep[x] = dep[f] + 1;
		for(int i = 1; i <= 20; i++) {
			fa[x][i] = fa[fa[x][i - 1]][i - 1];
			minn[x][i] = min(minn[x][i - 1], minn[fa[x][i - 1]][i - 1]);
		}
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to, w = edge[i].w;
			if(to == f) continue;
			fa[to][0] = x, minn[to][0] = w;
			dfs(to, x);
		}
	} 
	int query(int x, int y) {//倍增求解
		if(dep[x] > dep[y]) swap(x, y);
		int ans = 2e9;
		for(int i = 20; i >= 0; i--) {
			if(dep[fa[y][i]] >= dep[x]) {
				ans = min(ans, minn[y][i]);
				y = fa[y][i];
			}
		} 
		if(x == y) return ans;
		for(int i = 20; i >= 0; i--) {
			if(fa[x][i] != fa[y][i]) {
				ans = min(ans, min(minn[x][i], minn[y][i]));
				x = fa[x][i];
				y = fa[y][i];
			}
		}
		ans = min(ans, min(minn[x][0], minn[y][0]));
		return ans;
	}
}MCT;

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= m; i++) {
		cin >> u[i] >> v[i] >> w[i];
		u[i]++, v[i]++;
	}
	n++;//注意这道题目的特性
	for(int i = 1; i <= n; i++) {
		p[i] = i;
	}
	MCT.build(1, n);
	MCT.dfs(1, 0);
	cin >> q;
	while(q--) {
		int x, y;
		cin >> x >> y;
		x++, y++;
		cout << MCT.query(x, y) << '\n';
	}
	return 0;
}

3.4 最大权闭合子图

3.4.1 问题概述

给定一个有向图 G,定义它的一个闭合子图是该有向图的一个点集,满足这个点集中的点的出边指向的还是这个点集中的点。

在给每一个点附上点的权值之后,所有闭合子图中点权之和最大的就是最大权闭合子图。

3.4.2 解法及证明

首先我们给这个图加上两个点:源点 S 和汇点 T,让这个有向图 G 变成一个网络。具体做法如下:

  • 源点向所有点权为正的点连边,容量为点权。
  • 所有点权为负的点向汇点连边,容量为点权的绝对值。
  • 点权为 0 的点可以任选一种方式,没有影响。
  • 原图中的边不变,容量为

接下来在这个网络中跑出最大流,接着结论就是最大权闭合子图 = 所有正点权之和 - 最大流。

接下来我们证明这个结论:

首先每一个闭合子图都对应着网络中的一个割。显然一个割将网络分成了两部分,而与 S 相连的那一部分没有边指向另一部分,于是这就是一个闭合子图。

然后我们知道,我们选的最小割中的边一定和 S,T 中一者相连,否则我们就会选到中间边权为 的边,显然不符合最小割定义。

此时对于我们选择的闭合子图,权值和 = 所有正点权之和 - 我们未选择的正点权之和 + 我们选择了的负点权之和。我们考虑,没有选择的正点权与 S 连的边就会断开,而选择了的负点权就会和 T 的连边断开。接着我们就可以得到:

权值和 = 所有正点权之和 - 我们未选择的正点权之和 + 我们选择了的负点权之和

​ = 所有正点权之和 - (我们未选择的正点权之和 - 我们选择了的负点权之和)

​ = 所有正点权之和 - (我们未选择的正点权之和 + 我们选择了的负点权的绝对值之和)

​ = 所有正点权之和 - 割的容量

由于正点权之和是一定的,要让权值和最大,那么就要最小化割的容量,也就是最小割。最后再由最大流最小割定理,就可以得到最大权值和 = 所有正点权之和 - 最大流。

3.5 最大密度子图

3.5.1 问题概述

定义一个无向图 G 的密度 D 是边数 |E| 和点数 |V| 的比值,即 D=|E||V|

现在要求解一个无向图 G​ 的一个子图,要求这个子图的密度最大,这个问题就是最大密度子图问题。

3.5.2 解法

首先我们将 D 进行转化可以得到 D=eExevVxv(xe,xv{0,1})。看到这样两个数列和之比的式子,我们自然而然会想到 01 分数规划。

我们二分密度 D,现在的问题就在于如何求出 eExevV(mid×xv) 的最大值。

我们不难发现一件事,就是对于一条边 (u,v),如果它在子图中,则 u,v 也必然在子图中。所以我们建立一个新图,包括原图上的点和原图上每一条边所代表的点,按照上面的方式从边向点连边。

然后我们在看上面的式子,它告诉我们选一条边的代价是 1,选一个点的代价是 mid。所以我们将这个值赋到新图的对应点上去。

此时我们发现问题转化为,在新图上选出一个子图,满足每个点及其后继都要选,求出子图点权之和的最大值。我们惊奇的发现这就是最大权闭合子图问题,按照 3.4 讲解的内容直接求解即可。

3.6 平面图和对偶图

3.6.1 平面图

对于一张图 G,如果这个图画能在平面 S 上,即除了顶点处之外各边没有相交,则称 G 可平面嵌入 SG 就是平面图。

例:显然网格图就是一张平面图。

对于平面图 GG 的每一条边将 G 所在的平面划分为若干个区域,每个区域称为 G 的一个面 R。其中面积无限的面称为外部面,否则称为内部面。

当然上面这些定义都是枯燥的,它真正的用途还要和下面的东西有关。

3.6.2 对偶图

对偶图是和平面图有紧密联系的一个东西。

G 是一张平面图,我们可以构造一张图 G

  1. G 的每个面 Ri 中放置 G 的一个顶点 xi
  2. eG 的一条边,若 eG 的面 RiRj 的公共边界上,做 G 的一条边 ee 相交,且 e 的两个端点是 xi,xj;若 e 是一座桥(即它只在一个面 Ri)上,那么做 exi 的自环。同时 e 的边权应与 e 相同。

将图 G 就称作图 G 的对偶图。

如下图所示是一个将平面图转化为对偶图的例子:

20170914100048505 (624×498) (csdn.net)

3.6.3 平面图最小割

事实上,它被放入最小割当中就是因为它和最小割还有一丝联系。

20170914102758180 (596×577) (csdn.net)

考虑一张 st 平面图,我们要求出它的最小割。显然由于平面图的特性,我们可以看做是将 st 图横着放下来,然后随意竖着劈一刀将他隔开就是一个割。

考虑这反映到对偶图上是什么,我们将图中一些边割掉,而这些边在对偶图上则恰好是一条连续的、从 st 的路径。那么又由于平面图和对偶图交叉的路径边权相等,于是平面图上的一个割就转化为了对偶图上一条 st 的路径。

自然地,最小割就可以转化为对偶图上的最短路问题。

那么接下来我们看一道例题:[ICPC-Beijing 2006] 狼抓兔子。显然这道题是一个裸的最小割,然而点数 106,边数也差不多,Dinic 的玄学 O(n2m) 似乎有点吃力。那我们考虑转化为对偶图求最短路。

由于这道题是一个网格图,所以网格图建边还算简单,代码如下:

#include <bits/stdc++.h>

using namespace std;

const int Maxn = 9e6 + 5;
const int Inf = 2e9;

int n, m;

int head[Maxn], edgenum = 1;
struct node {
	int nxt, to, w;
}edge[Maxn];

void add(int u, int v, int w) {
	edge[++edgenum] = {head[u], v, w};
	head[u] = edgenum;
	edge[++edgenum] = {head[v], u, w};
	head[v] = edgenum;
}

int mat(int x, int y, int z) {
	return (x - 1) * (m - 1) * 2 + (y - 1) * 2 + z;
}

int s, t;
int dis[Maxn], vis[Maxn];

#define mk make_pair
void dijkstra() {
	for(int i = 1; i <= t; i++) {
		dis[i] = Inf;
		vis[i] = 0;
	}
	priority_queue <pair<int, int> > q;
	q.push(mk(0, s));
	dis[s] = 0;
	while(!q.empty()) {
		int x = q.top().second;
		q.pop();
		if(vis[x]) continue;
		vis[x] = 1;
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to, w = edge[i].w;
			if(dis[x] + w < dis[to]) {
				dis[to] = dis[x] + w;
				q.push(mk(-dis[to], to));
			}
		}
	}
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	s = mat(n - 1, m - 1, 2) + 1, t = s + 1;
	int p;
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j < m; j++) {
			cin >> p;
			if(n == 1) add(s, t, p);
			else if(i == 1) add(mat(i, j, 1), t, p);
			else if(i == n) add(s, mat(i - 1, j, 2), p);
			else add(mat(i, j, 1), mat(i - 1, j, 2), p);
		}
	}
	for(int i = 1; i < n; i++) {
		for(int j = 1; j <= m; j++) {
			cin >> p;
			if(m == 1) add(s, t, p);
			else if(j == 1) add(s, mat(i, j, 2), p);
			else if(j == m) add(mat(i, j - 1, 1), t, p);
			else add(mat(i, j - 1, 1), mat(i, j, 2), p);
		}
	}
	for(int i = 1; i < n; i++) {
		for(int j = 1; j < m; j++) {
			cin >> p;
			add(mat(i, j, 2), mat(i, j, 1), p);	
		}
	}
	dijkstra();
	cout << dis[t];
	return 0;
}

4 费用流

4.1 问题概述

给定网络 G,每条边上除了容量限制 c(u,v),还有一个单位流量的费用 w(u,v)

当一条边的流量为 f(u,v) 时,需要话费 f(u,v)×w(u,v) 的费用。

现在在满足最大流的情况下,求出总花费最少的流,这就是最小费用最大流问题;当然如果在最大流的情况下求出总花费最大的流,就是最大费用最大流问题,我们将他们统称为费用流问题。

4.2 SPFA 费用流

首先让我们回到最大流的场景,FF 算法的思路就是不断找增广路进行增广。

接下来我们考虑,对于两条可增广流量相同的两条增广路,我们一定是选费用之和较小的那一个。也就是说,对于费用和较小的增广路,我们是希望它的流量更大的,因此要先走。

所以我们考虑贪心,每次找增广路的时候找费用之和最短的一条,然后对于这条增广路进行增广。首先我们运用的还是基础的 FF 增广,因此反向边必不可少,于是费用的反向边也需要建立。

那么此时我们要在费用的图上求源点到汇点的最短路,由于反向的费用是负的,因此我们需要用到复活的 SPFA。

现在我们再关注一件事,我们上面提到是对这个费用最小的增广路增广,因此不难想到 EK 算法。于是最小费用最大流的一种简单的解决方式就是 EK + SPFA。这里只需要将 EK 中的 BFS 换成 SPFA 即可。

众所周知 SPFA 复杂度是 O(VE),而 EK 算法本身增广轮数上界是 O(VE) 次,因此时间复杂度是 O(V2E2)​。

当然这也是一个理论上界,实际运用中,同样几乎没有人卡 SPFA 费用流。

代码如下:

#include <bits/stdc++.h>

using namespace std;

const int Maxn = 2e5 + 5;
const int Inf = 2e9;

int n, m, s, t;

int head[Maxn], edgenum = 1;
struct node {
	int nxt, to, w, c;
}edge[Maxn];

void add(int u, int v, int w, int c) {
	edge[++edgenum] = {head[u], v, w, c};
	head[u] = edgenum;
	edge[++edgenum] = {head[v], u, 0, -c};
	head[v] = edgenum;
}

int dis[Maxn], pre[Maxn], minw[Maxn];
//费用之和;增广路上每一个点上一条边的编号;剩余容量最小值
bool vis[Maxn];
bool SPFA() { //普通 SPFA
	for(int i = 1; i <= n; i++) {
		dis[i] = Inf;
		vis[i] = 0;
	}
	queue <int> q;
	q.push(s);
	dis[s] = 0;
	minw[s] = Inf;
	vis[s] = 1;
	while(!q.empty()) {
		int x = q.front();
		q.pop();
		vis[x] = 0;
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to, w = edge[i].w;
			if(w > 0 && dis[x] + edge[i].c < dis[to]) {
				dis[to] = dis[x] + edge[i].c;
				minw[to] = min(minw[x], w);
				pre[to] = i;
				if(!vis[to]) {
					vis[to] = 1; 
					q.push(to);
				} 
			}
		}
	}
	return dis[t] < Inf;//说明无法到达 t 了
}

int mf, mc;//最大流量和最小费用
void EK() {
	while(SPFA()) {
		mf += minw[t];
		mc += minw[t] * dis[t];
		int i;
		for(int x = t; x != s; x = edge[i ^ 1].to) {
			i = pre[x];
			edge[i].w -= minw[t];
			edge[i ^ 1].w += minw[t];
		}
	}
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> s >> t;
	for(int i = 1; i <= m; i++) {
		int u, v, w, c;
		cin >> u >> v >> w >> c;
		add(u, v, w, c);
	}
	EK();
	cout << mf << " " << mc << '\n';
	return 0;
}

那么还有最大费用最大流,直接将 SPFA 改为求最长路的即可(这也是用 SPFA 的另一个好处,dijkstra 没法跑最长路)。

5 上下界网络流

5.1 总述

上下界网络流指的是对于一个网络,其流量不再是只有上界 c(u,v),同时拥有一个流量的下界 b(u,v)。在这种网络上的问题就是上下界网络流问题。

5.2 无源汇上下界可行流

5.2.1 问题概述

给定一个有上下界,没有源点汇点的网络 G,求是否有一个流 f,满足 b(u,v)f(u,v)c(u,v) 且流量守恒。

5.2.2 解法

不妨假设此时我们每一条边都已经流了 b(u,v) 的流量,设其为初始流。接下来我们建立一个新图,在新图上每一条边的流量设为 c(u,v)b(u,v)

此时显然我们会有不满足流量守恒的点,那么我们就尝试调整,考虑下界必须保证,那我们在新图中通过增减流量来抵消下界满流后产生的流量不守恒。我们知道,网络流中流入的流量都来自 s,流出的流量都流到 t。既然这样我们就可以再建立超源 S 和超汇 T,让这两个点来调整每个点的流量平衡。设该点初始流入流量与流出流量之差为 M,具体的:

  • M=0,不做处理。
  • M>0,表示此时原先的流入流量过大,需要在新图上增加流出量。为了增加流出量,我们就需要增加流入量。所以从超源向当前节点连边,容量为 M。这样当新图中流量平衡时,除去加上的这条边,新图上流出量就大于了流入量 M,与原先下界满流相差的正好抵消。
  • M<0,表示此时原先的流出流量过大。同上我们可以得到,此时应该从这个点向超汇连容量为 M 的边。

根据上面分析可以知道,只有当所有附加的边都跑满流的时候才能够与下界满流的差相抵消,达到流量平衡。因此我们在新图中跑最大流,如果 S 流出的边都是满流则说明存在可行流,否则不存在。

模板题:无源汇有上下界可行流,代码如下:

#include <bits/stdc++.h>

using namespace std;

const int Maxn = 2e5 + 5;
const int Inf = 2e9;

int n, m, l[Maxn], r[Maxn], fl[Maxn];
int head[Maxn], edgenum = 1;
struct node {
	int nxt, to, w;
}edge[Maxn];

int s, t;

void add(int u, int v, int w) {
	edge[++edgenum] = {head[u], v, w};
	head[u] = edgenum;
	edge[++edgenum] = {head[v], u, 0};
	head[v] = edgenum;
}

int dis[Maxn], cur[Maxn];
bool bfs() {
	for(int i = 1; i <= t; i++) {
		dis[i] = 0;
		cur[i] = head[i];
	} 
	queue <int> q;
	q.push(s);
	dis[s] = 1;
	while(!q.empty()) {
		int x = q.front();
		q.pop();
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to, w = edge[i].w;
			if(w > 0 && !dis[to]) {
				dis[to] = dis[x] + 1;
				if(to == t) return 1;
				q.push(to);
			}
		} 
	}
	return 0;
} 

int dfs(int x, int flow) {
	if(x == t) return flow;
	int rest = flow;
	for(int i = cur[x]; i && rest; i = edge[i].nxt) {
		cur[x] = i;
		int to = edge[i].to, w = edge[i].w;
		if(w > 0 && dis[to] == dis[x] + 1) {
			int k = dfs(to, min(rest, w));
			rest -= k;
			edge[i].w -= k;
			edge[i ^ 1].w += k;
		}
	}
	return flow - rest;
}

int ans = 0;
void dinic() {
	while(bfs()) {
		ans -= dfs(s, Inf);
	}
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= m; i++) {
		int u, v;
		cin >> u >> v >> l[i] >> r[i];
		add(u, v, r[i] - l[i]);//新图
		fl[u] -= l[i], fl[v] += l[i];//记录每个点的 M
	}
	s = n + 1, t = s + 1;
	for(int i = 1; i <= n; i++) {
		if(fl[i] > 0) {
			add(s, i, fl[i]);
			ans += fl[i];//看从 S 出发的边能否流满
		}
		else if(fl[i] < 0){
			add(i, t, -fl[i]);
		}
	}
	dinic();
	if(ans != 0) {
		cout << "NO\n";
	}
	else {
		cout << "YES\n";
		for(int i = 1; i <= m; i++) {
			cout << l[i] + edge[(i << 1) ^ 1].w << '\n';
 		}
	}
	return 0;
}

5.3 有源汇上下界可行流

5.3.1 问题概述

给定一个有上下界的网络 G,求是否有一个流 f,满足上下界限制。

5.3.2 解法

我们考虑沿用无源汇上下界可行流的做法,但是此时 s,t 的流量一定不守恒。不过我们知道 s 流出的流量等于 t 流入的流量,既然如此我们就给 ts 连一条边,下界为 0、上界为 。这样只要整个图满足流量守恒就一定可以得到一个可行流,而这个可行流的流量就是 ts 这条边的流量。

5.4 有源汇上下界最大流

5.4.1 问题概述

给定一个有上下界的网络 G,求出一个流 f,在满足上下界限制的情况下流量最大。

5.4.2 解法

我们先跑一遍有源汇上下界可行流,如果没有可行流就直接无解。

接下来我们关注这个新图上的残量网络,此时超源和超汇连的边应该已经流满,但是最开始图中源点和汇点之间的边显然还可能没有流满。那么我们此时再在原图的残量网络中调整,让他从可行流变成最大流。具体的,我们删去多余的附加边,然后直接在残量网络上求出原先的从源点到汇点的最大流,将可行流流量加上这个最大流流量就是答案。

模板题:有源汇有上下界最大流,代码如下:

#include <bits/stdc++.h>

using namespace std;

const int Maxn = 2e5 + 5;
const int Inf = 2e9;

int n, m;
int fl[Maxn];

int head[Maxn], edgenum = 1;
struct node {
	int nxt, to, w;
}edge[Maxn];

void add(int u, int v, int w) {
	edge[++edgenum] = {head[u], v, w};
	head[u] = edgenum;
	edge[++edgenum] = {head[v], u, 0};
	head[v] = edgenum;
}

int S, T, s, t;

int dis[Maxn], cur[Maxn];
bool bfs() {
	for(int i = 1; i <= T; i++) {
		dis[i] = 0;
		cur[i] = head[i];
	}
	queue <int> q;
	q.push(S);
	dis[S] = 1;
	while(!q.empty()) {
		int x = q.front();
		q.pop();
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to, w = edge[i].w;
			if(w > 0 && !dis[to]) {
				dis[to] = dis[x] + 1;
				if(to == T) return 1;
				q.push(to);
			}
		}
	}
	return 0;
}

int dfs(int x, int flow) {
	if(x == T) return flow;
	int rest = flow;
	for(int i = head[x]; i; i = edge[i].nxt) {
		int to = edge[i].to, w = edge[i].w;
		if(w > 0 && dis[to] == dis[x] + 1) {
			int k = dfs(to, min(rest, w));
			rest -= k;
			edge[i].w -= k;
			edge[i ^ 1].w += k;
		}
	}
	return flow - rest;
}

int dinic() {
	int res = 0;
	while(bfs()) {
		res += dfs(S, Inf);
	}
	return res;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> s >> t;
	for(int i = 1; i <= m; i++) {
		int u, v, l, r;
		cin >> u >> v >> l >> r; 
		fl[u] -= l, fl[v] += l;
		add(u, v, r - l);
	}
	S = n + 1, T = n + 2;
	add(t, s, Inf);//先跑可行流
	int cnt = 0;
	for(int i = 1; i <= n; i++) {
		if(fl[i] > 0) {
			add(S, i, fl[i]);
			cnt += fl[i];
		}
		else if(fl[i] < 0){
			add(i, T, -fl[i]);
		}
	} 
	int flow = dinic();
	if(flow != cnt) {
		cout << "please go home to sleep";
		return 0;
	}
	flow = edge[((m + 1) << 1) + 1].w;//t -> s 边上的流量就是可行流流量
	edge[((m + 1) << 1) + 1].w = edge[((m + 1) << 1)].w = 0;//注意要清空
	S = s, T = t;
	int ans = dinic();//在跑一遍最大流,将剩余容量榨干
	cout << ans + flow;
	return 0;
}

5.5 有源汇上下界最小流

5.5.1 问题概述

给定一个有上下界的网络 G,求出一个流量 f,在满足上下界条件的情况下流量最小。

5.5.2 解法

在一般的网络中,没有最小流问题,因为显然是 0。但是在上下界网络流中,我们要满足下界,自然可以延伸出最小流。

那么考虑最小流与最大流本质上还是一致的。在最大流中,我们在残量网络中由 st 跑了一遍最大流,目的是要榨干剩下的流量。而我们要求最小流,就是要尽可能将多余流量退回去。那么看到退回去会想到什么?反向边。我们只需要在残量网络中从 ts 跑一遍最大流,用可行流将这个最大流减去即可。

模板题:有源汇有上下界最小流,代码如下:

#include <bits/stdc++.h>

using namespace std;

const int Maxn = 4e5 + 5;
const int Inf = 2e9;

int n, m;
int fl[Maxn];

int head[Maxn], edgenum = 1;
struct node {
	int nxt, to, w;
}edge[Maxn];

void add(int u, int v, int w) {
	edge[++edgenum] = {head[u], v, w};
	head[u] = edgenum;
	edge[++edgenum] = {head[v], u, 0};
	head[v] = edgenum;
}

int S, T, s, t;

int dis[Maxn], cur[Maxn];
bool bfs() {
	for(int i = 1; i <= T; i++) {
		dis[i] = 0;
		cur[i] = head[i];
	}
	queue <int> q;
	q.push(S);
	dis[S] = 1;
	while(!q.empty()) {
		int x = q.front();
		q.pop();
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to, w = edge[i].w;
			if(w > 0 && !dis[to]) {
				dis[to] = dis[x] + 1;
				if(to == T) return 1;
				q.push(to);
			}
		}
	}
	return 0;
}

int dfs(int x, int flow) {
	if(x == T) return flow;
	int rest = flow;
	for(int i = cur[x]; i && rest; i = edge[i].nxt) {
		cur[x] = i;
		int to = edge[i].to, w = edge[i].w;
		if(w > 0 && dis[to] == dis[x] + 1) {
			int k = dfs(to, min(rest, w));
			rest -= k;
			edge[i].w -= k;
			edge[i ^ 1].w += k;
		}
	}
	return flow - rest;
}

int dinic() {
	int res = 0;
	while(bfs()) {
		res += dfs(S, Inf);
	}
	return res;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> s >> t;
	for(int i = 1; i <= m; i++) {
		int u, v, l, r;
		cin >> u >> v >> l >> r; 
		fl[u] -= l, fl[v] += l;
		add(u, v, r - l);
	}
	S = n + 1, T = n + 2;
	add(t, s, Inf);
	int cnt = 0;
	for(int i = 1; i <= n; i++) {
		if(fl[i] > 0) {
			add(S, i, fl[i]);
			cnt += fl[i];
		}
		else if(fl[i] < 0){
			add(i, T, -fl[i]);
		}
	} 
	int flow = dinic();
	if(flow != cnt) {
		cout << "please go home to sleep";
		return 0;
	}
	flow = edge[((m + 1) << 1) + 1].w;
	edge[((m + 1) << 1) + 1].w = edge[((m + 1) << 1)].w = 0;
	S = t, T = s;//唯一的区别
	int ans = dinic();
	cout << flow - ans;
	return 0;
}

5.6 有源汇上下界最小费用可行流

5.6.1 问题概述

给定一个有上下界,有费用的网络 G,求出一个流 f,在满足上下界条件的情况下费用最小(当然最大也同理)。

5.6.2 解法

显然我们可以先转化为有源汇上下界可行流,只是在建 uv 的时候容量为 c(u,v)b(u,v),费用为 w(u,v)。剩余附加边的费用都为 0

然后我们在新图上跑最小费用最大流,最后再加上跑满下界需要的费用就是答案。

例题:[AHOI2014/JSOI2014] 支线剧情,解法及代码如下:

对于每一条 xy 的路径设上界为 ,下界为 1,费用为时间。这样可以保证每条边走一遍。

然后对于每个点,向 t 连一条边,上界为,下界为 1。这样可以算出重复看剧情的数量。

最后我们直接跑有源汇上下界最小费用可行流即可。

代码:

#include <bits/stdc++.h>

using namespace std;

const int Maxn = 2e6 + 5;
const int Inf = 2e9;

int n;
int fl[Maxn];

int head[Maxn], edgenum = 1;
struct node {
	int nxt, to, w, c;
}edge[Maxn];

void add(int u, int v, int w, int c) {
	edge[++edgenum] = {head[u], v, w, c};
	head[u] = edgenum;
	edge[++edgenum] = {head[v], u, 0, -c};
	head[v] = edgenum;
}

int S, T, s, t;

int dis[Maxn], minw[Maxn], pre[Maxn];
bool vis[Maxn];

bool SPFA() {
	for(int i = 1; i <= T; i++) {
		dis[i] = Inf;
		vis[i] = 0;
	}
	queue <int> q;
	q.push(S);
	dis[S] = 0;
	minw[S] = Inf;
	vis[S] = 1;
	while(!q.empty()) {
		int x = q.front();
		q.pop();
		vis[x] = 0;
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to, w = edge[i].w, c = edge[i].c;
			if(w > 0 && dis[x] + c < dis[to]) {
				dis[to] = dis[x] + c;
				pre[to] = i;
				minw[to] = min(minw[x], w);
				if(!vis[to]) {
					vis[to] = 1;
					q.push(to);
				}
			}
		}
	}
	return dis[T] != Inf;
}

int ans = 0;
int EK() {
	int mc = 0;
	while(SPFA()) {
		mc += minw[T] * dis[T];
		int i;
		for(int x = T; x != S; x = edge[i ^ 1].to) {
			i = pre[x];
			edge[i].w -= minw[T];
			edge[i ^ 1].w += minw[T];
		} 
	}
	return mc;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n;
	s = 1, t = n + 1;
	for(int i = 1; i <= n; i++) {
		int k;
		cin >> k
		add(i, t, Inf, 0);
		for(int j = 1; j <= k; j++) {
			int b, t;
			cin >> b >> t;
			add(i, b, Inf, t);
			ans += t;
			fl[i]--, fl[b]++;
		}
	}
	add(t, s, Inf, 0);
	S = t + 1, T = S + 1;
	for(int i = 1; i <= t; i++) {
		if(fl[i] > 0) {
			add(S, i, fl[i], 0);
		}
		else {
			add(i, T, -fl[i], 0);
		}
	}
	ans += EK();
	cout << ans;
	return 0;
}

5.7 有源汇上下界最小费用最大流

5.7.1 问题概述

给定一个有上下界,有费用的图 G,求出一个流 f,在满足上下界条件且最大的同时,费用最小(当然费用最大和流最小都同理)。

5.7.2 解法

按照有源汇上下界最大流的做法,我们在求出最小费用可行流之后还要将剩余容量榨干。那我们直接在残量网络上从 st 跑一遍最小费用最大流即可,然后用这个费用加上可行流的费用就是答案。

Tips:显然所有有源汇的上下界网络流都可以转化为无源汇进行求解。

附 网络流建模技巧

F.1 基础技巧

  1. 多源多汇时建立超级源点和超级汇点。

F.2 拆点

  1. 对于权值在点上的题目,将每个点拆成入点和出点。
  2. 如果整张图有时间上的限制,考虑把每个点按照时间拆点。也就是把一个点拆成在不同时刻的点。
  3. 如果每一个点有不同的状态,将每一个状态拆成一个点。(其实时间也可以理解成一个点的不同状态)

F.3 网格类问题

  1. 对于网格问题,如果行列上有限制,考虑将一个点 (x,y)xy​ 连边。
  2. 对于网格问题,如果点之间会互相影响,考虑黑白染色。然后从黑点向白点连边(准确来讲并不只限制于黑白两种颜色,其实可以是红黄蓝绿等等,染色的主要目的在于让原先颜色间互相影响变为人为规定的单向影响,方便网络流连边)。

F.4 最小割类问题

  1. 如果出现类似二者选其一的模型,考虑将两者分别建立源点和汇点,然后求出最小割就可以将所有元素分成两部分。
  2. 在上一条的基础上,如果两个元素都选 / 都放在一边才能产生贡献,我们称这种结构为与结构。此时可以新建一个节点,从源(汇)点向这个点连容量为贡献的边,然后从这个点向那两个元素连容量为 ​​ 的边。这样跑最小割时就可以满足刚才说到的条件。
  3. 求解一条边能否在最小割和是否一定在最小割中的问题时,考虑在最终的残量网络上用 tarjan 求解。

F.4.1 切糕模型

有必要单独拿出来讲。该模型就是因 [HNOI2013] 切糕 考察而得名,基本模型如下:

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

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

首先我们考虑拆点,将每一个变量拆成 pi,1,pi,2pi,m,pi,m+1,就是表示每个点分别选 [1,m] 的情况。

现在我们考虑连边,首先要建出 n 条链,为 Spi,1pi,2pi,mT。其中 pi,jpi,j+1 的容量就是 ai,j,同时 Spi,1 的容量就是

这样我们断掉 pi,jpi,j+1 的边,实际的意义就是让 xi=j。接下来考虑如何描述限制条件。

我们以 xuxvw​ 举例,此时我们直接连 pv,ipu,i+w​,容量为 ​。考虑这样做的实际含义,当我们断掉 pv,ipv,i+1​,也就是 xv=i​ 时,对于所有的 pu,j,ji+k​ 都应该会被划到左边源点的集合,因此在 u​ 那条链上断掉的边一定是在 pu,i+k​ 之后,因此就满足了限制条件。

但是此时我们还可能出现这样的 bug:我们在一条链上割掉了两条边,这样显然是不符合题意的。考虑进行这样一步操作:我们再连上 pi,j+1pi,j,容量为 ​。这样就可以有效解决这个问题。

F.5 费用流类问题

F.5.1 动态加点

准确来讲这并不是一种建模思想,而是优化。

有这样一类题目,它们的数据范围比正常的题目要大,因而正常的网络流跑不过去。但是这种题目还有一个特征,那就是可以把点一个一个加入,对于最后结果没有影响。

既然这样,我们就可以在每次增广结束之后再加入需要的点,可以让整体的时间复杂度更低,达到优化的目的。

使用这种优化的注意事项:

  • 这种优化大多见于费用流,因为普通的最大流可以使用当前弧优化,而大多数人的费用流都是朴素的 SPFA + EK / Dinic。
  • 显然需要满足我们说的“可以把点一个一个加入,对于最后结果没有影响”,具体来讲,我们普通的网络流是利用反向边来实现撤销,然而你要是真的一个一个加点没有办法解决后面的点与前面的点的冲突。然而,如果能够保证后面加进来的点一定不比前面的点优,就不用考虑这个冲突,自然满足上面的条件。

F.5.2 解线性规划问题

这是一种非常暴力的建图方式,只需要你会代数推导并且题目符合一定条件,就一定能做出来。

首先是线性规划问题,我们有如下定义:

已知一组实数 ai 和一组变量 xi 有函数 f(x1xn)=i=1naixi。显然这个函数是线性的。如果 b 是实数且要满足 f(x1xn)=b,则称之为线性等式。相似的,还有线性不等式。两者统称线性约束。

线性规划就是一个线性函数的极值问题,而这个线性函数需要满足一定的线性约束。

那么考虑先将所有线性约束转化为线性等式(转化可以通过添加非负的变量来完成)。然后假设得到了 n 个式子,那么对 i[1,n+1] 的等式求出与前一个等式的差(第 n+1 个等式是 0=0)。

接下来求出的差式需要满足这样的性质:每个未知数只在两个等式中出现且一正一负。而我们要求的是线性函数的极值,自然想到费用流,然后我们就可以开始建图了:

  • 每个等式代表图中的一个顶点,同时添加源点 S 和汇点 T
  • 如果第 i 个等式右边是非负整数 c,连边 (S,i,c,0) 。否则连边 (i,T,c,0)
  • 如果一个变量 xi 在第 j 个等式中出现为 xi,在第 k 个等式中出现为 xi,同时在目标线性函数中的系数为 ci,那么连边 (j,k,+,ci)

理解一下上述的建边会发现,我们本质上就是在通过网路流的流量守恒达到等式的成立。我们可以认为是将所有项移到左边,这样正数之和就等于负数之和。容易发现,在上面的建图中,流向 i 的边代表着式子中负数的相反数,从 i 流出的边代表着正数,如果流量平衡自然就代表着等式成立。

F.5.3 平方费用费用流

我们正常的费用流都是规定走过一条边的费用是 f(u,v)×w(u,v)。那如果现在定义走过一条边的费用为 f(u,v)2×w(u,v)​,就是平方费用费用流问题。

容易发现平方费用的难点就在于这个平方,于是我们就想到将平方转化为线性的费用然后用朴素费用流求解。

如何转化?我们考虑这样一种方式:假如我们在考虑 u,v 两点,那么我们建出类似 (u,v,1,w(u,v)),(u,v,1,3w(u,v)),(u,v,1,5w(u,v)) 的边。

考虑正确性,由于费用流增广的是最短路径,所以费用小的一定选在大的之前,换句话说,我们如果要走这些边,最后走过了的边一定是从第一条边开始连续的一段。

假如我们原先要走 f(u,v) 的流量,现在就相当于走前 f(u,v) 条边。现在我们来计算一下走前 f(u,v) 条边的费用,实质上就是 i=1f(u,v)(2i1)×w(u,v)。用等差数列求和公式容易计算出,这个式子的结果其实就是 f(u,v)2×w(u,v)。这样,我们走前 f(u,v) 条边,不仅相当于原来流过的 f(u,v) 的流量,费用也就是要求的 f(u,v)2×w(u,v)。最后跑朴素费用流即可。

F.6 其他

  1. 如果图不完整,考虑二分容量限制。
  2. 当元素可以按照奇偶 / 特征值等拆成两部分时,考虑二分图,只从一部点向另一部点连边。
posted @   UKE_Automation  阅读(86)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示