网络流学习笔记

长期更新 但愿不咕


基本概念

这里只讨论有源汇的情况
首先有几个概念:

  • 反边:我们建完每个正边后 紧接着建一条反边并让其权值为 \(0\) 然后每次让正边流过一定流量 \(v\) 时 把正边剩余流量 \(-v\) 的同时把反边 \(+v\) 这样就形成了一个类似于“反悔”的机制 我们可以通过走反边让正边的流量还原
    正因如此 我们把正反边建边是编号为 \(1,2\) \(3,4\) \(5,6\) 这样就可以用 \(x \operatorname{xor} 1\) 来简单的查找它的反边 所以链式前向星建图 \(cnt\) 初始要等于 \(1\)
  • 残量网络:把每个流量达到满流的边删掉 就形成了一个残量网络
  • 流量守恒:除了源点和汇点 每个点流入和流出的流量一定相等
  • 最大流:一个网络的最大流量
  • 最小割:让这个网络源点和汇点分开(即让源点出发怎么流都流不到汇点)需要断开的边的容量和的最小值
    好吧我承认上面那玩意有点拗口
    就是你把要断掉的边的容量全都加起来 然后就这个和的最小值

网络最大流

这里只讲下 \(Dinic\) 算法 因为我只会这个

首先我们从原点开始 \(bfs\) 维护每个点到原点的距离 然后按这个距离对图进行分层 每个点只能走到比它层数大 \(1\) 的点
然后我们跑 \(dfs\) 把这张网络塞满
然后继续跑 \(bfs\) 把剩下的残量网络再求出来
然后继续跑 \(dfs\) 把这张残量网络塞满
重复此过程 直到源点和汇点不连通
具体看代码吧 以P3376 【模板】网络最大流为例

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

const int N = 1e4 + 0721;
const int inf = 0x7fffffff;
int dis[N], cur[N];
int head[N], nxt[N], to[N], val[N], cnt = 1;
int n, m, s, t;
ll ans;

inline void cmb(int x, int y, int z) {
	to[++cnt] = y;
	val[cnt] = z;
	nxt[cnt] = head[x];
	head[x] = cnt;
} 

bool bfs() {
	queue<int> q;
	for (int i = 1; i <= n; ++i) dis[i] = -1;
	for (int i = 1; i <= n; ++i) cur[i] = head[i]; //当前弧优化标记还原 
	dis[s] = 0;
	q.push(s);
	while (!q.empty()) {
		int now = q.front();
		q.pop();
		if (now == t) return 1; //如果还有流能流到终点 
		for (int i = head[now]; i; i = nxt[i]) {
			int y = to[i];
			if (dis[y] == -1 && val[i] > 0) { //如果是残量网络上的边 
				dis[y] = dis[now] + 1;
				q.push(y);
			}
		}
	}
	return 0;
}

int dinic(int x, ll res) { //res是当前流到这个点的流量 
	if (x == t || res == 0) return res; //搜到头或者流量没了 
	ll flow = 0, c; //flow是从这个点流出的流量 
	for (int i = cur[x]; i != 0 && res; i = nxt[i]) { //流量没了就没必要继续走了 
		cur[x] = i; //当前弧优化 
		int y = to[i];
		if (val[i] > 0 && dis[y] == dis[x] + 1) { //如果这条边流量还没满并且y是在x的下一层 
			c = dinic(y, min(res, (ll)val[i])); //搜下去 
			if (c == 0) dis[y] = -1; //流满了 剪枝优化 
			val[i] -= c; //减去这些流量 
			val[i ^ 1] += c; //反边加上这些流量方便反悔 
			flow += c;
			res -= c;
		}
	}
	return flow;
}

int main() {
	
	scanf("%d%d%d%d", &n, &m, &s, &t);
	for (int i = 1; i <= m; ++i) {
		int x, y, z;
		scanf("%d%d%d", &x, &y, &z);
		cmb(x, y, z);
		cmb(y, x, 0); //反边 
	}
	
	while (bfs()) ans += dinic(s, inf); //流到起点的流量是无限的
	
	printf("%lld", ans); 
	
	return 0;
}

值得一提的是 \(Dinic\) 算法的时间复杂度是 \(O(n^2m)\) 但是一般可以用来处理 \(10^4 \sim 10^5\) 规模的网络


网络最小割

最大流最小割定理:最大流就是最小割

没了(


最大权闭合子图

定义:给定一个有向图 图中点权有正有负 选出一个点权和最大的子图 使从子图内部任意点出发都走不出去

考虑最小割

首先我们将源点与所有点权为正的点连边 流量为该点点权 如果断开表示不选这个点
然后我们将汇点与所有点权为负的点连边 流量为该点点权的绝对值 如果断开表示选了这个点

然后对于一条边 \(x \rightarrow y\) 选了 \(x\) 不选 \(y\) 一定不合法
所以我们由 \(x\)\(y\) 连一条流量为 \(inf\) 的边 表示要么断左边要么断右边 即要么全选要么不选

那么所有正的点权和-最小割即为最大权闭合子图


最小费用最大流

最小费用最大流 就是我现在给每根边加一个“每流多少流量就要花费多少费用”的属性
然后让你求当这个网络满流的时候 最小费用是多少

那怎么办呢 我们还是基于 \(Dinic\) 算法来实现
首先 \(Dinic\) 算法是用 \(BFS\) 来判联通 然后用 \(DFS\) 一次性把当前残量网络的最大流求出来
现在为了算最小费用 所以我们不能一股脑合一起算了 我们选择分开算一条一条路的最小费用 因为一条路上的流量就是那个值 所以我们找出从起点到终点的单位费用之和最小的路径 并让其满流即可

所以就要用到 \(SPFA\) (如果担心被卡可以处理一下负权边然后跑 \(Dijkstra\)

模板:P3381 【模板】最小费用最大流
code:

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

const int N = 5e4 + 0721;
const int inf = 0x7fffffff;

int n, m, s, t;

struct network_flows {
	int minc[N], incf[N], pre[N];
	int head[N], to[N << 1], nxt[N << 1], cost[N << 1], f[N << 1];
	int cnt = 1;
	int maxflow = 0, mincost = 0;
	
	bool exist[N];
	
	inline void add_edge(int x, int y, int c, int flow) {
		to[++cnt] = y;
		cost[cnt] = c;
		f[cnt] = flow; //流量限制 
		nxt[cnt] = head[x]; //单位流量费用 
		head[x] = cnt;
	}
	
	bool spfa() {
		queue<int> q;
		for (int i = 1; i <= n; ++i) {
			minc[i] = inf;
			exist[i] = 0;
		}
		
		exist[s] = 1;
		minc[s] = 0;
		incf[s] = inf;
		q.push(s);
		
		while (!q.empty()) {
			int now = q.front();
			q.pop();
			exist[now] = 0;
			
			for (int i = head[now]; i; i = nxt[i]) {
				if (f[i] == 0) continue; //已经没有流量 不是残量网络上的边 
				int y = to[i];
				if (minc[y] > minc[now] + cost[i]) {
					minc[y] = minc[now] + cost[i];
					pre[y] = i;
					incf[y] = min(incf[now], f[i]);
					if (!exist[y]) exist[y] = 1, q.push(y);
				} 
			}
		}
		
		if (minc[t] == inf) return 0;
		else return 1;
	}
	
	void MCMF() {
		while (spfa()) { //不断寻找一条单位费用和最短的路径 并让其满流 
			int x = t;
			maxflow += incf[t];
			mincost += incf[t] * minc[t]; //因为找到的就是一条路径 自然要增加的是流量和所有边上单位费用之和 
			int i;
			while (x != s) { //把这条路上的边都减去这个流量 
				i = pre[x];
				f[i] -= incf[t];
				f[i ^ 1] += incf[t];
				x = to[i ^ 1];
			}
		} 
	}
} G;

int main() {
	scanf("%d%d%d%d", &n, &m, &s, &t);
	
	for (int i = 1; i <= m; ++i) {
		int x, y, c, flow;
		scanf("%d%d%d%d", &x, &y, &flow, &c);
		G.add_edge(x, y, c, flow);
		G.add_edge(y, x, -c, 0); //因为反向边是为了反悔嘛 所以费用也是负的 
	}
	
	G.MCMF();
	
	printf("%d %d", G.maxflow, G.mincost);

	return 0;
}

二分图最小点集覆盖

定义:选出一个最小的点集 使每条边的两个端点至少有一个被选中

二分图最小点集覆盖 = 最大匹配

我不会证 会用就行

例:POJ3041 ⼩⾏星

在⼀个N*N的矩阵中有K个⼩⾏星(N<=500,K<=10000),⼀炮可以清除⼀⾏或⼀列中所有的⼩⾏星,问⾄少开⼏炮才能清除所有的⼩⾏星。

首先我的思路是因为每一行每一列都对应一些行星的点集 所以问题就转化为选择数目最少的点集 使所有行星都被选上

当然正解也可以对于这种“每一个需要选的点都只会被两个点集包含”的问题有所启发:
把行和列分开 然后对于每个行星 把它对应的行和列连一条边
那么答案就是这个二分图的最小点集覆盖
非常显然


最小路径覆盖

定义:在一个 \(DAG\) 中选若干条不交的路径 使所有点被覆盖 并且路径总长最小

题目传送门:P2764 最小路径覆盖问题

首先我们考虑每个点只会被经过一次
这里有个很启发性的做法:如果我们想让某个点最多只会被经过 \(x\) 次 我们可以把该点拆成出点和入点 然后中间连一条流量为 \(x\) 的边

说回这题 首先我们把所有点拆成出点和入点 但是我们不使用上面那个做法
而是根据原来的 \(DAG\) 把对应边的出入点连边
那么答案即为点数-二分图最大匹配

为什么?

我们考虑 每个点只能随便找一条往外连/连到它的边
那么这个就是点数条边
但是如果对于俩点 一条边同时满足使一个点往外连 又连到另一个点
那么显然就可以减少一条边
又因为每个点只能被连一次 正好满足匹配的性质
所以能匹配多少对点 就能节省多少条边

例:P2765 魔术球问题

考虑新加进来一个球 我们把使它能接在后面的球与它连一条有向边
然后考虑一个柱子放的球一定是一条路径
并且一个球只能放在一个柱子上 所以每个点只会被经过一次
就转化为了最小路径覆盖问题


最大独立点集

定义:选出尽量多的点 使它们两两之间不连边

最大独立点集 = 点数 - 二分图最大匹配

为什么?

我们考虑 最大独立点集要满足对于一条边 两端最多选一个点
对于最小点集覆盖 我们要满足对于一条边 两端至少选一个点
发现最大独立点集正好是最小点集覆盖的补集

最大团

有两组点,同⼀组中的点两两相连,不同组的点可能相连可能不相连;尽可能多地选出⼀些点,使得这些点两两相连。

考虑反图是一个二分图
那么就转化为尽可能多地选用出一些点 使它们两两不相连
那就是最大独立点集

posted @ 2023-07-07 21:35  Steven24  阅读(37)  评论(0编辑  收藏  举报