网络流学习笔记
啊啊是的,又来学网络流了。
网络
设有一张有向图 \(G = (V,E)\),每条边 \((u,v) \in E\) 都有一个权值 \(c(u,v)\),称为容量,当 \((u,v)\not\in E\) 的时候有 \(c(u,v) = 0\),我们称这张有向图为网络。
在这之中有两个特殊的点:源点 \(S \in V\) 和汇点 \(T \in V\)。
流
设定义域在二元组 \((u\in V, v\in V)\) 上的实数函数 \(f(u,v)\) 且满足:
- 容量限制:对于每条边,都有 \(f(u,v)\le c(u,v)\)。
- 斜对称性:对于每条边,都有 \(f(u,v) = -f(v,u)\)。
- 流守恒性:从源点流出的流量 \(=\) 汇点流入的流量,即:
我们就称 \(f\) 是网络 \(G\) 的流函数,对于 \((u,v) \in E\),我们称 \(f(u,v)\) 为边的流量,\(c(u,v) - f(u,v)\) 称为边的残余容量。整个网络的流量为 \(\sum_{(S,v)\in E}f(S,v)\),即从源点 \(S\) 发出的所有流量之和(流守恒性)。
这里再给一个酷炫的定义:
最大流
对于网络 \(G\),最大化整个网络的流量,即求解如下公式:
下面将讲几个求解最大流的算法:
Ford-Fulkerson 增广类
首先捋清楚一些定义:
定义残量网络 \(G_f\) 为所有剩余容量 \(c_f(u,v) > 0\) 的边构成的子图,即 \(G_f = (V,E_f)\),在这之中,\(E_f = \{(u,v)\mid c_f(u,v) > 0\}\)。
定义增广路为在残量网络 \(G_f\) 上的一条从源点 \(S\) 到汇点 \(T\) 的路径。
定义增广操作为给某一增广路的每一条边 \((u,v)\) 加上等量的流量使得网络的总体流量增加。
对此,我们可以发现:
最大流的求解就可以视为若干次增广操作分别得到的流的叠加。
不过,这里有一点需要注意,根据流函数的斜对称性,我们需要有一个「退流」操作,即若我们对于 \(f(u,v)\) 增加了 \(x\),则相应的,我们也要对 \(f(v,u)\) 减少 \(x\)。为了方便表述,我们将 \((u,v)\in E\) 称作 「正向边」,将 \((v,u)\in E\) 称为「反向边」。
「退流」操作实际上是产生了一个「抵消」的操作,为的是能够保证我们可以自由的原则增广增广路的顺序(即给我们一个反悔的机会)。
我们可以发现,只要 \(G_f\) 上存在增广路,那么我们增广它就必定能使得总流量增加,否则就说明我们到达了流量最大值,这也就是 Ford-Fulkerson 增广的思想。
证明?证明如下(PS:真的好想要一个和 OI-Wiki 一样的 Markdown 书写方案啊):
考虑每一次我们增广都相当于从残留网络中拿掉一条边再换上一条反向边,当残量网络中的 \(S\) 和 \(T\) 不连通的时候自然我们换掉的边集是一组割边,然后根据最大流最小割定理易得我们此时得到的就是最大流。
根据上述定义,我们不难有如下一种算法,设算法开始时我们的残留网络 \(G_f\) 是当前网络 \(G\):
- 从 \(S\) 出发进行 \(\operatorname{bfs}\) 直到点 \(T\)。
- 将最先到达点 \(T\) 的路径流上路径 \(p\) 上的最小容量,即 \(\Delta = \min_{(u,v)\in p}(c(u,v))\),然后给 \((u,v) \in p\) 增加流量 \(\Delta\),给反向边退流 \(\Delta\),最大流增加 \(\Delta\),并得到新的残量网络 \(G_f'\)。
- 对 \(G_f'\) 重复进行操作,直到我们的 \(\operatorname{bfs}\) 无法从 \(S\) 到达点 \(T\)。
这也就是我们的 Edmonds-Karp 算法,时间复杂度 \(\mathcal O(|V||E|^2)\),复杂度证明不会。
下面我们再补充定义几个东西:
设有网络 \(G_f\),我们对其进行 \(\operatorname{bfs}\) 时,所经过的点和边导出的子图叫做 \(G_f\) 的分层图 \(G_L\)。
更加形式化的,我们称 \(G_L = (V,E_L)\) 是 \(G_f = (V,E_f)\) 的分层图,其中 \(E_L = \{(u,v) \mid (u,v) \in E_f,\, d(u) + 1 = d(v)\}\)。
定义阻塞流 \(f_b\) 为分层图 \(G_L\) 上最大的增广流,且使得 \(G_L\) 找不到更大的增广流(注意,这里的增广流指的是原先定义的若干增广流的并)
这个时候我们有如下算法:
- 在 \(G_f\) 上 \(\operatorname{bfs}\) 出层次图 \(G_L\)。
- 在 \(G_L\) 上 \(\operatorname{dfs}\) 出阻塞流 \(f_b\)。
- 将 \(f_b\) 并入原先的最大流流中,即 \(f_b \to f_{\max}\),
- 重复上述过程,直到 \(G_f\) 上不存在 \(S\) 到 \(T\) 的路径。
这个算法就是大名鼎鼎的 \(\text{Dinic}\) 了。
然后就是当前弧优化,就是在 \(\operatorname{dfs}\) 的过程中,我们可能会出现某个节点含有大量入边和出边的情况,并且每次接受入边的流时都要重新便利每条出边,这显然是无法接受的。
为了避免这样的缺陷,我们对每个节点维护出边表中第一条还有必要尝试的出边,我们称这个指针为「当前弧」,称这样的做法为「当前弧优化」,这样才能保证 \(\text{Dinic}\) 算法的正确性。
时间复杂度 \(\mathcal O(|V|^2|E|)\),实际上很难卡到这样的上界。
Push-Relabel 预流推进类
预流推进的核心思想是通过对单个节点进行更新,直到没有节点需要更新来求解最大流。
首先必然是引入一些相关定义:
定义超额流 \(e(u)\) 为对于一个节点其流入节点的流超出流出节点的流时,超出的部分称为该节点的超额流。更加形式化的说,我们有:
若 \(e(u)>0\),则我们称这个节点 \(u\) 溢出(注意:当我们提到溢出节点的时候,显然不包括 \(S\) 和 \(T\))。
预流推进算法维护每个节点的高度 \(h(u)\),并且规定溢出的节点 \(u\) 如果要推送超额流,只能向高度小于自己的相邻节点推送;如果没有,则修改 \(u\) 的高度(即重新分配标签)。
我们设残量网络 \(G_f\) 上有自然数函数 \(h(u)\) 且满足:
- \(h(S) = |V|\)
- \(h(T) = 0\)
- \(\forall (u,v) \in E_f,\,h(u)\le h(v) + 1\)
则称函数 \(h\) 为残量网络 \(G_f=(V_f,E_f)\) 的高度函数。
\(\text{Lemma 1}\):
设 \(G_f\) 上的高度函数为 \(h\),对于任意两个节点 \(u,v \in V\),如果 \(h(u) \gt h(v) + 1\),则 \((u,v)\) 不是 \(G_f\) 上的边。
Tips: 准确的说,预流推进实际上维护了映射 \(h:V \to N\)。
算法只会在 \(h(u)=h(v)+1\) 的边执行推送操作。
而如果对于溢出的节点 \(u\) 进行推送操作,则需要满足以下条件:
- \((u,v)\in E_f\)
- \(c(u,v) - f(u,v) \gt 0\)
- \(h(u) = h(v) + 1\)
于是,我们希望尽可能的将超额流从 \(u\) 推送到 \(v\),推送过程中我们只关心超额流 \(c(u,v) - f(u,v)\) 的最小值,并不关心 \(v\) 是否溢出。
如果 \((u,v)\) 在推送完后满流,则将其从残量网络中删去。
而如果对于溢出的节点 \(u\) 进行 relabel
操作,则需要满足以下条件:
- \((u,v) \in E_f\)
- \(h(u) \le h(v)\)
重贴标签时,将 \(h(u)\) 更新为 \(\min_{(u,v)\in E_f} h(v) + 1\) 即可。
对于初始化操作,我们需要完成:
上述将 \((S,v)\in E\) 充满流,并将 \(h(S)\) 抬高,使得 \((S,v)\not \in E_f\),因为 \(h(S) \gt h(v)\),而且 \((S,v)\) 毕竟满流,没必要留在残留网络中;上述还将 \(e(S)\) 初始化为 \(\sum_{(S,v)\in E} f(S,v)\) 的相反数。
然后我们考虑每一次选节点的时候,都优先选择高度最高的那个溢出的节点,于是不难有算法:
- 初始化;
- 选择溢出节点中高度最高的节点 \(u\),并对它所有可以推送边进行推送;
- 如果 \(u\) 仍溢出,对它重贴标签,回到步骤 \(2\);
- 如果没有溢出的节点,则算法结束。
这也就是我们的欢乐皮皮 HLPP 算法,复杂度为 \(O(n^2\sqrt m)\)。
下面是对 HLPP 的两个优化:
\(\operatorname{BFS}\) 优化
HLPP 使用的时候卡上界卡的比较紧;我们可以在初始化高度的时候进行优化,具体的说,我们初始化 \(h(u)\) 为 \(u\) 到 \(t\) 的最短距离;特别的 \(h(S) = |V|\)。
在 \(\operatorname{BFS}\) 的同时,我们还可以检查图的连通性,排除无解的情况。
GAP 优化
HLPP 的推送条件是 \(h(u) = h(v) + 1\),然而如果在算法的某一时刻,\(h(u) = T\) 的节点个数为 \(0\),那么对于 \(h(u) \gt T\) 的节点就永远无法推送超额流到 \(T\),因此只能送回到 \(S\),那么我们就在这时直接将他们的高度变为至少 \(n+1\),以让他们尽快送回 \(S\),减少重贴标签的操作。
最小割
根据最大流最小割定理,我们可以知道最小割的代价就是最大流。
然后我们可以用最大流做最小割的问题了。
建模原则
如果有一些东西我们渴望捆绑起来,则将描述他们之间关系的边设为 \(+\infty\),表示我不想割掉这条边。
剩下的……因题目而异吧……
集合划分模型
一般来处理这样的问题:
设有集合 \(S\) 和 \(T\),每个物品都有两个属性 \(s_i\) 和 \(t_i\),表示放入集合 \(S\) 时会有 \(s_i\) 的代价,放入集合 \(T\) 时会有 \(t_i\) 的代价,还有 \(m\) 个捆绑关系,即如果 \(a\) 和 \(b\) 没有分在同一个集合内,则产生 \(q_i\) 的代价。
我们考虑将源点 \(S\) 和汇点 \(T\) 设为这两个集合,然后将第 \(i\) 个物品向 \(S\) 连一条容量为 \(t_i\) 的边,表示放入集合 \(T\) (也就是不在集合 \(S\))时产生了 \(t_i\) 的代价,向 \(T\) 连一条容量为 \(t_i\) 的边,表示放入集合 \(S\)(也就是不在集合 \(T\))所产生的代价。对于有捆绑关系的物品对,我们连一条容量为 \(q_i\) 的双向边,表示他们拒绝捆绑会产生 \(q_i\) 的代价。
费用提前计算
一般用来处理某个人在处理第 \(x\) 件事的时候费用是 \(c_x\) 这种。
我们考虑这个人可以隐分身,然后就按照正常的方式去建图就好了。