2023-10-27 17:15阅读: 305评论: 16推荐: 8

「学习笔记」网络流

「学习笔记」网络流

点击查看目录

定义部分名词括号内写个英文是为了方便起函数名变量名。

\rvalue/\rvalue/\rvalue/\rvalue/\rvalue/

仅提供大致思路,想看详细点的看上面链接。

建议学的时候自己多手画几个图模拟一下。

常见建模套路,打算以后单开一个博写,因为还有好多套路没有学。

知识点

一些基础定义

主要来自 OI-wiki。

  • 「网络(Flow Network)」:指一张有向图 G=(V,E)

  • 「容量(Capacity)」:在一个网络中,对于每一组边 (u,v)E 都有一个权值 c(u,v) 被称为容量。若 (u,v)Ec(u,v)=0

  • 「源点(Source)与汇点(Sink)」:两种存在于网络中的比较特殊的点,所有流从源点 s(sV) 流出,最后流入汇点 t(tV)st

  • 「流(Flow)」:若函数 f(u,v) 满足以下三点性质:

    1. 容量限制:对于每条边,流经该边的流量不得超过该边的容量,即 f(u,v)c(u,v)
    2. 斜对称性:每条边的流量与其相反边的流量之和为 0,即 f(u,v)=f(v,u)
    3. 流守恒性:从源点流出的流量等于汇点流入的流量,即 (s,u)Ef(s,u)=(u,t)Ef(u,t)

    f 是网络 G 的流函数。

    对于每一条边 (u,v)Ef(u,v) 被称为边 (u,v)「流量」c(u,v)f(u,v) 被称为边 (u,v)「剩余流量」

    所有流从源点 s(sV) 流出,因此整个网络流量为 (s,u)Ef(s,u)

最大流

  • 「最大流(Max-flow)」:给定一个网络,求一个流函数使这个网络的总流量最大。

Ford-Fulkerson 算法(增广路算法)

  • 「增广路(Augmenting Path)」:一条起点为源点,终点为汇点的剩余流量不为空的边的路径。

这里的增广路与二分图中的不是一个,但是是一个思想。

考虑不断从源点出发寻找增广路并将其跑满,但是这样是错的,因为你增广出来的不一定是最优解里需要的,考虑使这个操作「可撤销」。

解决办法是建立反向边,原来的边流量减多少反向边加多少(流的斜对称性),可以把原来的边上不优的流通过反向边推到另一条边上。

下图是从 OI-wiki 拿的,可以感性理解一下:

image

但是这玩意复杂度保证不了啊!有可能会出现下图这种情况(from rvalue):

23333333

23333333

1

23333333

23333333

1

2

s

t

可能会出现流在容量为 1 的那条边被反复推的情况,这个时候复杂度就取决于值域了。

Edmonds-Karp 算法

简称 EK 算法。

考虑每次找增广路时找最短的增广路。这个直接 01 最短路找,时间复杂度 O(E)

不难发现这样找到的增广路长度单调不减,最长为 V,那么考虑迭代次数即可算出时间复杂度。

每次跑增广路都会有一条边流量被跑满,我们称这样的边为「关键边」。每次跑增广路都会出现一条关建边,那么所有边成为「关键边」次数之和即为迭代次数。一条边成为关键边之后暂时会从图中消失,再次出现必须跑其反向边,这时增广路长度必然增加,那么每条边最多 V 会成为「关键边」,总迭代次数是 VE 次。

因此总时间复杂度是 O(VE2)

感觉还是不够快,观察发现原因是每次最短的增广路有很多条,但是每次只增广一条。

继续优化。

Dinic 算法

思考如何一次性把所有最短增广路跑了。

按与源点的距离建立分层图,然后跑一遍 DFS 把全图的最短路跑满。

乍一看感觉层数严格递增,DFS 遍历所有点,复杂度是 O(VE) 的,但是 DFS 中一个点会重复经过所以不能保证其复杂度。

考虑两个优化:

  • 无用点优化
    如果有流量流向一个点的时候这个点流不动了,说明它在当前分层图上不再能做出贡献,可以暂时删去。
  • 当前弧优化
    决定复杂度,不会负优化,你慢了说明你挂了。
    如果当前到点 u 的流在 u 遍历完其指向的所有点时用完了,我们记录一下是推向哪条边时用完的,下次再搜索到 u 时直接从这条边开始推,因为之前的肯定推满了。

考虑计算时间复杂度。

参考 EK 算法时间复杂度的证明,每次找增广路最多找 E 条,长度最多为 V,分层图层数严格递增,最多会建 V 次分层图,时间复杂度上限为 O(V2E)

注意到计算复杂度时用词都是「最多」,稍微想一想就会发现不可能跑满,这个复杂度上限是非常松的,一般出题人不会卡你,卡了也没关系,因为这样干就不会有什么人能过题了。

下面是一份实现:

namespace GRAPH {
	class Edge { public: ll v, w, r; };
	class Graph {
	private:
		ll n, s, t, c[N][N];
		std::vector <Edge> tu[N];
	public:
		inline void Init (ll _n, ll _s, ll _t) { 
			n = _n, s = _s, t = _t;
			return;
		}
		inline void AddEdge (ll u, ll v, ll w) {
			c[u][v] += w;
			ll p1 = tu[u].size (), p2 = tu[v].size ();
			tu[u].push_back ((Edge){v, w, p2});
			tu[v].push_back ((Edge){u, 0, p1});
			return;
		}
	private:
		ll dep[N], cur[N];
	public:
		ll maxflow;
	private:
		inline bool DinicBFS () {
			memset (dep, 0, sizeof (dep));
			std::queue <ll> q;
			q.push (s), dep[s] = 1, cur[s] = 0;
			while (!q.empty ()) {
				ll u = q.front (); q.pop ();
				far (p, tu[u]) {
					if (!p.w || dep[p.v]) continue;
					dep[p.v] = dep[u] + 1, cur[p.v] = 0;
					if (p.v == t) return true;
					q.push (p.v);
				}
			}
			return false;
		}
		inline ll DinicDFS (ll u, ll flow) {
			if (u == t || flow <= 0) return flow;
			ll sum = 0, sz = tu[u].size () - 1;
			for (ll &i = cur[u]; i <= sz; ++i) {
				Edge &p = tu[u][i];
				if (!p.w || dep[p.v] != dep[u] + 1) continue;
				ll k = DinicDFS (p.v, std::min (flow, p.w));
				if (k <= 0) dep[p.v] = 0;
				p.w -= k, tu[p.v][p.r].w += k;
				sum += k, flow -= k;
				if (flow <= 0) break;
			}
			return sum;
		}
	public:
		inline void Dinic () {
			maxflow = 0;
			while (DinicBFS ()) maxflow += DinicDFS (s, inf);
			return;
		}
	};
}

最小割

删去一些边,使得 st 不连通,求这些边的最小容量和。

形式化一点:

  • 「割(Cut)」:将网络 G 分为 ST=VS 两个点集,满足 sS,tT,则称 (S,T)G 的一个割。
  • 「割的容量(Cut capacity)」:对于网络 G 的一个割 (S,T),其容量 c(S,T)uS,vT,(u,v)Ec(u,v)
  • 「最小割(Min-cut)」:求一个割 (S,T),使 c(S,T) 最小。

最大流最小割定理:最大流等于最小割的容量。

稍加思考还是比较好想到证明的。

跑最大流时跑满容量的边全割掉就是一个合法的割了,如果割完后图还联通就说明还有增广路,那就不是最大流了。既然这已经是一种合法的割了那最小割就不可能大于最大流。

如果最小割小于最大流,显然割掉的边是必由之路,应该割掉的边跑满了之后没有增广路了,那就不可能有更多的流流到汇点,假设不成立。

不大于且不小于等价于等于。

费用流

  • 「费用(Cost)」:每条边新增一个权值「费用」,即 1 单位的流经过这条边所需的费用。单位为 a 的流经过一条费用和为 b 的路径所需费用为 a×b
  • 「最小费用最大流(Min-cost max-flow)」:满足流最大的前提下使费用最小。

EK 费用流

仍使用 EK 算法的思路,但是在求最短路的过程,边长度不再是 0/1,而是费用。

注意反向边费用为负,原因考虑反向推流的过程。

ZKW 费用流

Dinic 求分层图换成 SPFA 即可。

但一个比较恶心的事情是,这样不能当前弧优化,无法保证复杂度。

Update:交流发现处理之后可以当前弧优化,有空需要研究一下 .

Update2:写了,全开 int LOJ #102 能跑到 271ms,相当高效了 .

但是上界依然很松,所以随便用!

一份实现:

const ll inf = 1ll << 60;
namespace GRAPH {
	const ll V = N;
	class Edge {
	public:
		int v, r; ll w, c;
		Edge () = default;
		Edge (int v, ll w, ll c, int r) : v (v), r (r), w (w), c (c) {}
	};
	class Graph {
	private:
		std::vector <Edge> tu[V];
	public:
		void AddEdge (int u, int v, ll w, ll c) {
			int p1 = tu[u].size (), p2 = tu[v].size ();
			tu[u].emplace_back (v, w, c, p2);
			tu[v].emplace_back (u, 0, -c, p1);
			return;
		}
	private:
		ll dis[V]; int cur[V]; bool vis[V];
		bool DinicBFS (int S, int T) {
			memset (vis, 0, sizeof (vis));
			memset (dis, 0x3f, sizeof (dis));
			std::queue <int> q;
			q.push (S), dis[S] = 0, vis[S] = true, cur[S] = 0;
			while (!q.empty ()) {
				int u = q.front (); q.pop ();
				far (p, tu[u]) {
					if (!p.w || dis[u] + p.c >= dis[p.v]) continue;
					dis[p.v] = dis[u] + p.c, cur[p.v] = 0;
					if (!vis[p.v]) q.push (p.v), vis[p.v] = 1;
				}
				vis[u] = false;
			}
			return dis[T] < inf;
		}
		ll DinicDFS (int u, int T, ll flow) {
			if (u == T || flow <= 0) return flow;
			ll sum = 0; vis[u] = true;
			int sz = tu[u].size () - 1;
			for (int& i = cur[u]; i <= sz; ++i) {
				Edge& p = tu[u][i];
				if (!p.w || dis[p.v] != dis[u] + p.c || vis[p.v]) continue;
				ll k = DinicDFS (p.v, T, std::min (flow, p.w));
				sum += k, flow -= k;
				p.w -= k, tu[p.v][p.r].w += k;
				if (flow <= 0) break;
			}
			return sum;
		}
	public:
		pll Dinic (int S, int T) {
			ll maxflow = 0, mincost = 0;
			while (DinicBFS (S, T)) {
				ll f = DinicDFS (S, T, inf);
				maxflow += f, mincost += f * dis[T];
			}
			return pll (maxflow, mincost);
		}
	};
}

例题

[SCOI2007] 蜥蜴

拆点后成板子题。

[SDOI2015] 星际战争

二分时间,然后建二分图,在激光武器和巨型机器人之间连容量为无限的边。

源点到激光武器容量为时间乘攻击力,巨型机器人到汇点容量为装甲值。

如果最大流等于总装甲值说明合法。

注意精度问题,建议乘 10000

士兵占领

最少很难求,于是转化为每个格都放了士兵之后,最多能去掉多少个士兵。

源点向代表每一行的点连边,容量为这一行最多能删除的士兵数量,列同理。

如果某一个点无障碍,则将其所属行和所属列连边。

[HNOI2007] 紧急疏散EVACUATE

二分时间,然后把一个门拆成多个门,代表不同时间。

然后就比较好想了,但是不好预处理。

[SDOI2009] 晨跑

费用流板子题。

[SDOI2016] 数字配对

比较重要的一个性质是,如果 aiaj 可以配对,那么两者质因数个数刚好相差一。

那么可以根据质因数个数奇偶性建出二分图然后跑费用流。

[SCOI2007] 修车

发现这个费用很难计算啊。

但是我们有拆点!谁说只能拆成出入点的?

image

你把第 i 个师傅拆成 j 个师傅,其中的第 ki 号师傅表示这个师傅倒数第 k 次修车,这个时候花费用的时间要乘 k,因为后面修的 k1 车都会被这次耽误。

然后跑一个费用流。

[NOI2012] 美食节

和修车差不多,但是交上去会发现 T 了!怎么回事呢?

点太多了。

你发现一次用不到很多点,所以你考虑动态加点,一个师傅被增广过就给他再拆一个点。

[AHOI2009] 最小割

首先根据证明最大流最小割定理的过程,可行边必须满流。

然后你发现如果残量网络中仍有 uv 的路径则 (u,v) 也不是可行边,因为砍了没用。

剩下都是可行边。

然后发现若残量网络 S 能到 uv 能到 T,那么边 (u,v) 是一条必经边。

否则经过这条边的增广路径上还会有与这条边容量一样的边,割掉后效果相同。

考虑跑 Tarjan 后使用 SCC 判定连通。

本文作者:K8He

本文链接:https://www.cnblogs.com/K8He/p/Flow.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   K8He  阅读(305)  评论(16编辑  收藏  举报
评论
收藏
关注
推荐
深色
回顶
收起
点击右上角即可分享
微信分享提示