图论基础——网络流与二分图初步
anjack_511 · · 算法·理论
网络流与二分图初步
我们约定,对于有向图
I 概念
(1)网络流:
在一个有向带权图上(不考虑自环和重边),与最短路类似,我们定义一个源点
使用数学的方法定义:我们定义对于有向图
-
容量限制: 每条边流过的流量不大于该条边的容量。
f(u,v) \leqslant c(u,v) -
流量守恒: 从一个非源汇结点流出的总流量等于流入这个结点的总流量。
\sum \limits_{v \in V}f(v, u) = \sum \limits_{v \in V} f(u, v) -
斜对称性: 反向边流量与正向的流量相反。
f(u,v) = -f(v, u)
我们称整条网络各条路径的流量之和等于这个网络的流量,这个流量的最大值叫做 最大流。每条边的容量减去流过的流量为剩余容量
(2)增广路:
如果我们动态地看待流的过程,我们发现对于一条边,流是可叠加的,即全局的流可以多次地流经一条边,只要保证这些流的总流量不大于这条边的容量即可。我们一次一次地从源点流向汇点,每次将各边容量减去流量得到残存网络,然后再在残存网络上寻找网络流,就是将这个流拆开的过程。每次在
(3)网络流的割:
将流网络的点集
记所有横跨切割的流量
记所有横跨切割的边容量之和为这个割的容量
例:这里是流量图。横跨切割的流量
(4)二分图:
如果一个无向图
对于二分图的一个边集
- 如果
M 中任意两条边没有公共顶点,则称M 是二分图的的一个匹配。 - 如果二分图的所有边至少有一个在
S 中,那么则称S 为二分图的一个点覆盖。 - 如果
S 中任意两点都没有边直接相连,则称S 为二分图的一个独立集。
II 网络流算法
经过前面的铺垫,我们发现一个流可以拆为很多流的叠加,那么反过来,我们要寻找最大流,其实就是在寻找很多流的组合,使这些流的总流量最大。可以看出看出,一个从源点到汇点的流最基本的就是一条简单路径上的流,其实也就是前面提到的增广路。
最大流的算法基于一个基本的过程 Floyd-Fulkerson:每次不断地寻找增广路,同时建构反向边用以抵消错误的增广路,直到找不到增广路为止,计算此时的答案。
基于这个过程,无论如何寻找增广路,复杂度总不会超过
其次的 Edmonds-Karp 算法是 FF 的 BFS 版本,时间复杂度
最后也是最常用的 Dinic 算法在 FF 基础上加入了分层网络的概念,BFS 搭配 DFS 剪掉了很多枝,具有不错的时间复杂度
同时我们给出一个基本定理,将在 II/5.算法分析 中证明。
最大流最小割定理:
-
-
残存网络
G_f 不包括任何增广路径; -
1. Floyd-Fulkerson(寻找增广路)
FF 是解决最大流问题的基本思想,该方法保证时间复杂度下限为
方法:
- 找到一条增广路,并计算出这条增广路上的流量,流量为这条路径上剩余容量的最小值。
- 对于增广路的每条边
(u,v) ,使c_f(u,v) = c_f(u,v) - f(u,v) ;并对于每条边的反向边(初始容量为0 ),使c_f(v,u) = c_f(v,u) + f(u,v) 。 - 重复 1,2 的过程,直到找不到增广路。
2. Edmonds–Karp(BFS 实现 FF 增广)
找增广路时使用 BFS,其他的和 FF 一致。
时间复杂度:
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 5050;
const long long INF = 0x7f7f7f7f;
int lk[MAXN], ltp;
struct Node{
int y, nxt;
long long cap; // 剩余容量
}e[MAXN << 1]; // 牢记两倍边空间!!因为要建反向边
void add(int x, int y, long long cap)
{
if(lk[x] == 0 && e[1].y != x) lk[x] = -1;
e[ltp] = {y, lk[x], cap};
lk[x] = ltp, ltp++;
}
/**
* 反向边的存储:e[i] 是一条原来的边的话,保证 e[i ^ 1] 是这条边的反向边
* 那么链式前向星就要发生改动:邻接链表的末尾标记要初始化 -1,
* 否则 e[0] 会引发歧义。
*/
int n, m, s, t, rt[MAXN];
long long flow[MAXN], ans;
// 点数,边数,源点,汇点,当前增广路下标数组,
// 当前增广路 f(s, i) 流量,最终总流量
/**
* rt[]的存储方式:从后往前存增广路的上一个元素,
* 这里注意存的是链式前向星的边下标
*/
bool BFS()
{
bool vis[MAXN];
fill(vis + 1, vis + 1 + n, 0); // 标记数组,防止成环
queue<int> q;
q.push(s);
flow[s] = INF, vis[s] = 1;
while(!q.empty()){
int pre = q.front();
q.pop();
for(int i = lk[pre]; i != -1; i = e[i].nxt){
if(vis[e[i].y] || e[i].cap == 0) continue;
vis[e[i].y] = 1, rt[e[i].y] = i;
flow[e[i].y] = min(flow[pre], e[i].cap);
q.push(e[i].y);
if(e[i].y == t) return true;
}
}
return false;
}
void EdmondsKarp()
{
for(;;){
bool flag = BFS();
if(!flag) return;
for(int i = t; i != s; i = e[rt[i] ^ 1].y){
e[rt[i]].cap -= flow[t];
e[rt[i] ^ 1].cap += flow[t];
}
ans += flow[t];
}
}
int main()
{
scanf("%d%d%d%d", &n, &m, &s, &t);
for(int i = 1; i <= m; i++){
int x, y;
long long cap;
scanf("%d%d%lld", &x, &y, &cap);
add(x, y, cap);
add(y, x, 0); // 建反向边
}
EdmondsKarp()
printf("%lld", ans);
return 0;
}
3. Dinic (在分层网络上的增广路)
一些基本概念:
-
层次图:在残存网络
G_f 上跑一边 BFS, BFS 中只保留每一层流向下一层的边,这些边组成了一个层次图G_L 。 -
阻塞流:在一个图中,不断进行增广直至无法增广出新路(不限定增广的方式,只要进行增广即可),此时得到的流称为阻塞流
f_B 。最大流一定是阻塞流,但阻塞流不一定是最大流。阻塞流显然要比一条增广路更优,因为它肯定是多条增广路的组合。
方法:
-
在
G_f 上 BFS 出当前的分层图G_L 。 -
使用 DFS 在
G_L 上找到一条阻塞流f_B 。 -
将
f_B 计入总贡献,并依靠其更新G_f
重复上述过程直到
DFS 寻找阻塞流也很细节。
弧优化: DFS 中,枚举某一结点的某一出边时,由于寻找阻塞流的特性,我们会榨干这条边的价值。回溯之后,寻找新的阻塞流时可能还会枚举到这个节点,这时已经被榨干的出边就不再用遍历了,这样我们 DFS 图的复杂度就降到了
时间复杂度:
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 5050;
const long long INF = 0x7f7f7f7f;
int lk[MAXN], ltp;
struct Node{
int y, nxt;
long long cap; // 剩余容量
}e[MAXN << 1]; // 牢记两倍边空间!!因为要建反向边
void add(int x, int y, long long cap)
{
if(lk[x] == 0 && e[1].y != x) lk[x] = -1;
e[ltp] = {y, lk[x], cap};
lk[x] = ltp, ltp++;
}
int n, m, s, t, dpt[MAXN], cur[MAXN];
long long ans;
// 点数,边数,源点,汇点,当前结点的分层图深度,当前弧标记
// 当前增广路 f(s, i) 流量,最终总流量
bool BFS()
{
fill(dpt + 1, dpt + 1 + n, 0); // 标记数组,防止成环
queue<int> q;
q.push(s);
dpt[s] = 1, cur[s] = lk[s];
while(!q.empty()){
int pre = q.front();
q.pop();
for(int i = lk[pre]; i != -1; i = e[i].nxt){
if(dpt[e[i].y] || e[i].cap == 0) continue;
cur[e[i].y] = lk[e[i].y], dpt[e[i].y] = dpt[pre] + 1;
q.push(e[i].y);
if(e[i].y == t) return true;
}
}
return false;
}
long long DFS(int pre, long long flow)
{
if(pre == t) return flow;
long long rtcap, rtflw = 0;
// 当前出边后阻塞流流量,该点所有出边阻塞流总流量
for(int i = cur[pre]; i != -1 && flow > 0; i = e[i].nxt){
cur[pre] = i; // 当前弧要标记在 for 循环里面,最后一条枚举到的出边上
if(e[i].cap == 0 || dpt[pre] + 1 != dpt[e[i].y]) continue;
rtcap = DFS(e[i].y, min(flow, e[i].cap));
if(rtcap == 0) continue;
e[i].cap -= rtcap, e[i ^ 1].cap += rtcap;
rtflw += rtcap, flow -= rtcap;
}
return rtflw;
}
void Dinic()
{
while(BFS())
ans += DFS(s, INF);
}
/**
* DFS 中的细节很多:
* 传入的形参 flow 实际上是流到结点 pre 时的实时流量
* 现在要找阻塞流,那么就要把这些流量用完,
* 如果用不完就返回 pre 后面层次能用的流量最大值。
* 当前弧的作用就在这儿,每次从上层流到 pre 后,pre 下
* 层的所有出边都竭尽全力地消耗流量,那么消耗殆尽的出边
* 是不参与后面的阻塞流贡献的,所以每条边最多会被枚举 1 次
*/
int main()
{
scanf("%d%d%d%d", &n, &m, &s, &t);
for(int i = 1; i <= m; i++){
int x, y;
long long cap;
scanf("%d%d%lld", &x, &y, &cap);
add(x, y, cap);
add(y, x, 0); // 建反向边
}
Dinic();
printf("%lld", ans);
return 0;
}
4. 最小割问题
根据 最大流最小割定理,我们发现只要求出了网络最大流,那么该网络的最小割也就得到了。那么跑一遍 Dinic 就行了。
如果想要进一步求出满足最小割的
5. 费用流问题
网络
这里我们解决 最小费用最大流问题
SSP 方法: 每次寻找流路径上单位费用和最小的增广路增广,直到图上不存在增广路为止。即在原先的 EK/Dinic 算法的基础上,寻找增广路时使用最短路算法。
值得注意的是,由于费用可能存在负数,不能裸着跑 Dijkstra。
Primal-Dual 原始对偶算法: 既然不能跑 Dijkstra,而 SPFA 又太慢,那能不能把边权变成正的,再跑 Dijkstra 找增广路呢?答案是可行的。我们采用上一篇博客中 Johnson全源最短路 中的方法:
从源点
增广后因为多了些反边,我们还要对新的
反边建边时也满足斜对称性
时间复杂度:
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 50050;
const ll INF = 0x7f7f7f7f;
int lk[MAXN], ltp;
struct Node{
int y, nxt;
ll cap, w; // 剩余容量,费用
}e[MAXN << 1]; // 牢记两倍边空间!!因为要建反向边
void add(int x, int y, ll cap, ll cost)
{
if(lk[x] == 0 && e[1].y != x) lk[x] = -1;
e[ltp] = {y, lk[x], cap, cost};
lk[x] = ltp, ltp++;
}
int n, m, s, t, rt[MAXN];
ll dist[MAXN], h[MAXN], flow, cost;
// 点数,边数,源点,汇点,当前增广路下标数组,
// 最短路长度,势标记,总流量,总费用;
struct Nd{
ll d; int p;
bool operator < (const Nd &x) const{
return x.d < d;
}
};
void SPFA()
{
bool vis[MAXN];
fill(vis + 1, vis + 1 + n, 0);
fill(dist + 1, dist + 1 + n, INF);
fill(h + 1, h + 1 + n, INF); // 这里三个初始化都要牢记
queue<int> q;
q.push(s);
h[s] = 0, vis[s] = 1;
while(!q.empty()){
int u = q.front();
q.pop();
vis[u] = 0;
for(int i = lk[u]; i != -1; i = e[i].nxt){
int v = e[i].y;
if(e[i].cap != 0 && h[u] + e[i].w < h[v]){
h[v] = h[u] + e[i].w;
if(!vis[v]){ q.push(v); vis[v] = 1;}
}
}
}
}
bool Dijkstra()
{
bool vis[MAXN];
fill(vis + 1, vis + 1 + n, 0); // 标记数组,防止成环
fill(dist + 1, dist + 1 + n, INF);
priority_queue<Nd> q;
q.push((Nd){0, s});
dist[s] = 0;
while(!q.empty()){
Nd tmp = q.top();
q.pop();
int u = tmp.p;
if(vis[u]) continue;
vis[u] = 1;
for(int i = lk[u]; i != -1; i = e[i].nxt){
int v = e[i].y;
ll cost = e[i].w + h[u] - h[v];
if(e[i].cap != 0 && dist[u] + cost < dist[v]){
rt[e[i].y] = i;
dist[v] = dist[u] + cost;
if(!vis[v]) q.push({dist[v], e[i].y});
}
/** 注意这里不!能!if(v==t) return,因为这是在跑最短路,松弛
* (u,t) 不代表着找到了从 s 到 t 的最短路 */
}
}
if(dist[t] == INF) return false;
return true;
}
// 注意 dijkstra 和 SPFA 的区别,不要把 vis 更改写串了
void Primal_Dual()
{
SPFA();
for(;;){
bool flag = Dijkstra();
if(!flag) return;
ll curflow = INF;
for(int i = 1; i <= n; i++) h[i] += dist[i];
for(int i = t; i != s; i = e[rt[i] ^ 1].y)
curflow = min(curflow, e[rt[i]].cap);
for(int i = t; i != s; i = e[rt[i] ^ 1].y){
e[rt[i]].cap -= curflow;
e[rt[i] ^ 1].cap += curflow;
}
flow += curflow;
cost += curflow * h[t];
/** 注意这里 *h[t],不!要!写成 *dist[t],因为 h[t] 才代表着 s 到 t
* 的费用和,而dist[t]不是原始的费用,仅代表着拓扑关系 */
}
}
int main()
{
scanf("%d%d%d%d", &n, &m, &s, &t);
for(int i = 1; i <= m; i++){
int x, y;
ll cap, cos;
cin >> x >> y >> cap >> cos;
add(x, y, cap, cos);
add(y, x, 0, -cos); // 注意这里费用是-cos!!
}
Primal_Dual();
cout << flow << " " << cost;
return 0;
}
6. 算法分析
(1)建反向边的有效性
我们考察这个简单的例子:
我们看到,蓝色流很有可能变成一个阻塞流,但是我们这里加入了反向边。在蓝色流完后,我们找到了一条红色的增广路,让我们看看它是如何抵消刚才的操作的:
两次
这是两次流一样的情况,那两次流不一样呢?实际上,我们可以将流量大的那条流看成流了两次,一次流量和小的流量相等,一次流量是这个较小的流量抵消后的剩余流量(其实就是大流量减去这个较小流量的值)。那么,问题又回到了到了一个流的问题。
(2)最大流最小割定理的证明
引理1: 跨越任何一个切割的流量都等于流的流量。
引理2: 一个流的流量总不大于任何一个割的容量。
引理1的证明: 规定
由 流量守恒 ,对于任何
由于
后面两项实际上在字母上是对称的,故而完全相等,可以消去,最后就剩下:
引理2的证明: 根据 引理1 和 容量限制:
从 引理2的证明 中可以看出,
这意味割等于流时,所有从
引理1同时也说明:流的流量不会超过最小割的容量
最大流最小割定理:
-
-
残存网络
G_f 不包括任何增广路径; -
最大流最小割定理的证明:
-
1
\Rightarrow 2: 如果残存网络中还存在增广路,那还可以通过这条增广路增加流量,这与f 是最大流矛盾,故残存网络中不存在增广路。 -
2
\Rightarrow 3:G_f 上增广路不连通,意味着一定可以将V 分为S,T 两个点集,使得S 到T 中的每条边都是满流的,根据斜对称性,此时T 到S 的所有连边只能为0 。又根据 引理2,此时|f| = c(S,T) 。 -
3
\Rightarrow 1: 根据 引理2,我们总有最大流的流量不会超过最小割的容量,故此时f 就是最大流。
(3)FF 增广的时间复杂度下限
考虑如下例子:
如果随机选择增广路,那么我们来回增广
III 二分图
1. 二分图最大匹配
方法(最大流建模): 设图
- 分别将所有
L 中的点与s 连一个容量为1 的的边, -
-
那么在这个新图上找到最大流,问题就解决了。
例:如上图,源汇点各连一条容量为 1 的边就限制了 1 个结点只能接到对面点集的 1 个结点上,保证了二分图的匹配。
那么代码跑一边 Dinic 就好了,没有必要放了。
注意:新加边和点之后数据范围可能会发生变化。
各类数组要开大!
使用 fill() 函数初始化时注意传参的范围!
由于容量限制都为 1, 最终的复杂度为
2. 二分图的相关性质
(1)König 定理
二分图最大匹配 = 二分图最小点覆盖。
证明: 按照刚才的建模,只需说明二分图的最小点覆盖与网络流的最小割等价,再根据 最大流最小割定理 证明即可。
这里我们构造
- 二分图上不应存在
A_S \rightarrow B_T 的边,如果存在,那么最小割就是\infin (建图时保证的c(A \rightarrow B) = \infin ),但是最小割等于最大流肯定不是无限大,矛盾。所以只存在A_S \rightarrow B_S 和A_T \rightarrow B_T 的边。不难发现任意边(u,v) 都有u \in A_T 或v \in B_S ,故而最小割是二分图的一个点覆盖。 - 不难发现此时最小割是
\sum\limits_{v\in A_T} c(s, v) + \sum\limits_{u\in B_S} c(u, t) ,如果还存在更小的点覆盖,那么必然要在A_T \cup B_S 中再割掉一个点,但这样最小割也就减小了,不符合前提,故而不存在更小的点覆盖。
(2)二分图的最大独立集 = |V| - 二分图的最小点覆盖。
解释: 二分图最小点覆盖的补集即为二分图的最大独立集。二分图的点覆盖意味着每条边至少有一个点被选中,也就意味着每条边至多有一个点没被选中。这就说明如果放弃掉原来的点覆盖,选中边另外一侧的点,就能保证每条边只有最多一个点被选中,也就满足了独立集的条件。上面至多至少的条件是等价的,也就永远成立,说明所有独立集都能用点覆盖生成,故而最大独立集为最小点覆盖的补集。
(3)Hall 定理
对于二分图