算法学习笔记(16):网络流最大流
网络
是一种特殊的带权有向图, 有以下几个特殊概念:
- 容量:边的权值,流在边上的最大量。
- 源点:没有入度的点
- 汇点:没有出度的点
- 流:每一条边都有对应的流量,流量不超过边的容量。除去汇点和源点,每个点的净流量等于0
本文考虑网络最大流问题。
网络流最大流
在最大流问题中,先假设只有一个源点和一个汇点。
对于网络G,给每条边指定对应的流量,使得源点和汇点的流量最大。
此时称这个分配 ”f( 边 ) = 流量“ 为最大流。
自然思路
建立一个图,每条边的值表示这个边的空闲流量,初始化和原图一样,因为一开始没有我们设定的流量。
进行以下操作的循环:
- 找到一个从源点到汇点的简单路径(无环)(任意算法)。
- 对于这条简单路径,找到这条路中的最小容量,在空闲流量中减去这个最小流量。
- 对于剩余容量归 0 的边将其删除或视作没有边。
直到找不到简单路径为止。
缺点
然而这个思路并不正确,并不能找到真正的最大流。
反例说明
考虑一个简单的网络:
- 源点 S 到中间节点 A1 有一条容量为 5 的边。
- 源点 S 到中间节点 A2 有一条容量为 2 的边。
- A1到A2有一条容量为5的边。
- A1到 T 有一条容量为5的边。
- A1到 T 有一条容量为2的边。
方案1:
如果我们先找到 S - A2 - T ,两个都为2,所以都减去2.
再找到S- A1 - T 两个边都是5, 所以都减去5.
总流量为7 = 2 + 5
方案2:
先找到了 S - A1 - A2 - T, 5 5 2,最小为2 ,都减去2,A2-T的边没有了
在找到 S - A1 - T 3 3 5, 最小为3,都减去3
没有路径了
总流量为 5 = 2 + 3
显然方案2并不是最大流答案,这也被称为堵塞流,不能新增新的流量,当然,最大流也是堵塞流。
Ford–Fulkerson 增广
Ford–Fulkerson 增广是计算最大流的一类算法的总称。该方法运用贪心的思想,通过寻找增广路来更新并求解最大流。
这个算法可以说基于上面给的例子的自然思路。让我们找到自然思路中的问题,也就是当一条路把另一条路的容量抢走,导致不能拿到最多的量,一旦我们能撤销原来的量,就可以找到真正的最大量。
如何把这个撤销操作具现化呢?
Ford–Fulkerson 算法采用反向边的思路,例如:
对于一条边 A - B,假设我们已经找到了一条S1 - … - A1 - B1 - … - T1, 把A - B之间的流量都占用了。此时,如果我们找到一条 S2 - … - B2 - A2 - … - T2的路径。(数字序号便于区分流量)
就可以有S2 - … - (B2 = B1) - … T1 , 有 S1 - … - (A1 = A2) - … - T2.
将原本不能走的路径,通过分配变得可行。
因此Ford–Fulkerson 算法步骤为:
- 找到一个从源点到汇点的简单路径(无环)(任意算法)。
- 对于这条简单路径,找到这条路中的最小容量,在空闲流量中减去这个最小流量。
- 对于剩余容量归 0 的边将其删除或视作没有边。
- 添加原路径的反向边,其权值为减去的流量值。边如果存在,则合并权值(新增!)
直到找不到边为止。
这个算法成功解决了最大流问题,得到了真正的最大流答案
缺点
遇到最坏情况会花费很多的时间,例如例子:
- 源点 S 到中间节点 A1 有一条容量为 100 的边。
- 源点 S 到中间节点 A2 有一条容量为 100 的边。
- A1 到 A2有一条容量为 1 的边。
- A1 到 T 有一条容量为 100 的边。
- A1 到 T 有一条容量为 100 的边。
如果每次找路径都通过了这个A1 - A2的边,将每次只对最大网络流增加1,就会导致这个简单的网络流需要循环整整100次,如果最大网络流为F,最坏情况的复杂度有O(FV)
Edmonds–Karp 算法
Edmonds–Karp 算法属于Ford–Fulkerson 算法的一种特例,两者的思路一样。
不同点在于,在寻找最短路的第一步,EK算法将图看做无权图,寻找无权图中的最短路径。算法的好处在于不再依赖于最大流的大小,M为边的数量,N为点的数量,复杂度为O(M2N)。
每次找最短路使用BFS需要O(M), 被证明最多只要M*N轮(证明略)
代码:
struct Edge
{
int from, to, cap, flow;
Edge(int u, int v, int c, int f) : from(u), to(v), cap(c), flow(f) {}
};
struct EK
{
static const int maxn = 250;
int n, m; // n:点数,m:边数
vector<Edge> edges; // edges:所有边的集合
vector<int> G[maxn]; // G:点 x -> x 的所有边的编号
int a[maxn]; // a:点 x -> BFS 过程中最近接近点 x 的边给它的最大流
int pathFrom[maxn]; // p:点 x -> BFS 过程中最近接近点 x 的边
void init(int n){
for (int i = 0; i <= n; i++)
G[i].clear();
edges.clear();
}
void AddEdge(int from, int to, int cap){
edges.push_back(Edge(from, to, cap, 0));
edges.push_back(Edge(to, from, 0, 0)); // 提前建立反向边 权值是0
m = edges.size();
G[from].push_back(m - 2); // 插入了边的编号
G[to].push_back(m - 1);
}
int Maxflow(int s, int t){
int flow = 0;
while(true){
memset(a, 0, sizeof(a));
queue<int> BfsQ;
BfsQ.push(s);
a[s] = INF;
while (not BfsQ.empty()){
int x = BfsQ.front();
BfsQ.pop();
for (int edgeNo : G[x]){ // 遍历以 x 作为起点的边
Edge &e = edges[edgeNo];
if (!a[e.to] && e.cap > e.flow){
pathFrom[e.to] = edgeNo; // edgeNo 是最近接近点 e.to 的边
a[e.to] = min(a[x], e.cap - e.flow); // 最近接近点 e.to 的边赋给它的流
BfsQ.push(e.to);
}
}
if (a[t])
break; // 如果汇点接受到了流,就退出 BFS
}
if (!a[t])
break; // 如果汇点没有接受到流,说明源点和汇点不在同一个连通分量上
for (int u = t; u != s; u = edges[pathFrom[u]].from){ // 通过 u 追寻 BFS 过程中 s -> t 的路径
edges[pathFrom[u]].flow += a[t]; // 增加路径上边的 flow 值
edges[pathFrom[u] ^ 1].flow -= a[t]; // 减小反向路径的 flow 值
}
flow += a[t];
}
return flow;
}
};
增广成为了算法的重要概念,找增广路也就是不断地从反悔边和可走边中找到一条简单路径,而只要不断去寻找增广路,就可以得到正确的答案。
Dinic 算法
dinic算法修改了获得增广路的方式以优化复杂度,使得复杂度更多基于点。
引入了新概念分层图和堵塞流。
堵塞流前面已经提过,也就是一个流使得网络不能再新增流量,最大流一定是堵塞流,但堵塞流不一定是最大流。
分层图将图分成多个层,每个层表示为从起点出发,走几步可以到达的点。
类似于BFS分层,分层图不保留一层之间的点的边,只保留不同层之间的边,并且还是从小层到大层的边。
算法步骤:
- 建立剩余流量图的分层图
- 找到分层图的一个堵塞流
- 在剩余流量图中进行对应的减少以及建立反向边
直到分层图找不到堵塞流为止。
为什么使用分层图
我们已知算法的本质是基于不断寻找增广路而得到,根据EK算法,为了提高效率,我们通常选择最短路作为我们的增广路,如果使用分层图,可以使得我们获得的堵塞流中建立的增广路都是最短路径,通过一次预处理的分层而大大降低DFS寻找堵塞流的复杂度。
当前弧优化
在找到分层图的堵塞流中,我们需要提高效率以保证复杂度。
我们通常需要反复寻找增广路径。当前弧优化通过记录每个节点当前正在尝试的出边(称为“当前弧”),以避免重复检查已经确定不能作为增广路径的一些边,从而加速算法的执行。
对于原本的dfs,可能会重复尝试已经无效了的边,使用当前弧优化,如下面代码的current数组,始终指向有效的边,避免dfs再从已经无效的边进行检查。
这个被维护的current数组也就是下一个有效边的指针,我们又称作有效弧指针,故作有效弧优化。
代码:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int MAX_NODES = 250;
const int INF = 0x3f3f3f3f;
const int MAX_EDGES = 10000;
struct MaxFlow {
// 边结构体
struct Edge {
int to, next, capacity, flow;
} edges[MAX_EDGES];
int head[MAX_NODES], edge_count = 0;
int node_count, source, sink;
long long max_flow = 0;
int depth[MAX_NODES], current[MAX_NODES];
// 初始化
void init() {
memset(head, -1, sizeof head);
edge_count = 0;
}
// 添加边 u->v,容量为capacity
void addEdge(int u, int v, int capacity) {
edges[edge_count] = {v, head[u], capacity, 0};
head[u] = edge_count++;
edges[edge_count] = {u, head[v], 0, 0};
head[v] = edge_count++;
}
// BFS 构建分层图
bool bfs() {
queue<int> q;
memset(depth, 0, sizeof(int) * (node_count + 1));
depth[source] = 1;
q.push(source);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = head[u]; i != -1; i = edges[i].next) {
int v = edges[i].to;
if (!depth[v] && edges[i].capacity > edges[i].flow) {
depth[v] = depth[u] + 1;
q.push(v);
}
}
}
return depth[sink];
}
// DFS 寻找增广路
int dfs(int u, int flow) {
if (u == sink || flow == 0) return flow;
int total_flow = 0;
for (int& i = current[u]; i != -1; i = edges[i].next) {
int v = edges[i].to;
if (depth[v] == depth[u] + 1) {
int d = dfs(v, min(flow - total_flow, edges[i].capacity - edges[i].flow));
if (d > 0) {
edges[i].flow += d;
edges[i ^ 1].flow -= d;
total_flow += d;
if (total_flow == flow) return total_flow;
}
}
}
return total_flow;
}
// Dinic 算法求最大流
void dinic() {
while (bfs()) {
memcpy(current, head, sizeof(int) * (node_count + 1));
max_flow += dfs(source, INF);
}
}
} maxFlow;
复杂度
最多循环n - 1轮,每次复杂度为O(MN),在最坏情况下是O(N2M)
MPM 算法
MPM算法和Danic类似,只是在每一轮寻找增广路不同。
所以这里注重于如何找到 找到分层图的一个堵塞流,MPM算法将这个步骤优化:
MPM算法注重于点,引入新关键词 点容量,点剩余流量,参考点。
- 点容量:这个点的所有入边容量之和,这个点的所有出边容量之和,这两者的较小者就是点容量。
- 点剩余容量:都改成剩余容量。
- 参考点:每次所有点中剩余容量最小的点。
MPM算法认为,参考点每次都可以因流的增加而使参考点的剩余流量为0。
因为参考点是容量最小的,所以必定有路径能从S到参考点以及参考点到T,把路径删去点的容量,让点的剩余流量为0,再删去点。
总结一遍步骤:
- 建立剩余流量图的分层图
- 建立所有点的点容量,找到参考点。
- S到参考点以及参考点到T的路径流量删去对应量,使参考点的剩余容量归零0.
- 删去参考点,找到堵塞流为止回到第二步
- 在剩余流量图中进行对应的减少以及建立反向边
复杂度
可以发现每次找堵塞流是基于顶点的,所以每次寻找堵塞流的步骤复杂度为O(V2),应为寻找参考点使用了V 次,而每次删去流也用了V次。这是一个O(V3)的最大流算法
from: https://cp-algorithms.com/graph/mpm.html
struct MPM{
struct FlowEdge{
int v, u;
long long cap, flow;
FlowEdge(){}
FlowEdge(int _v, int _u, long long _cap, long long _flow)
: v(_v), u(_u), cap(_cap), flow(_flow){}
FlowEdge(int _v, int _u, long long _cap)
: v(_v), u(_u), cap(_cap), flow(0ll){}
};
const long long flow_inf = 1e18;
vector<FlowEdge> edges;
vector<char> alive;
vector<long long> pin, pout;
vector<list<int> > in, out;
vector<vector<int> > adj;
vector<long long> ex;
int n, m = 0;
int s, t;
vector<int> level;
vector<int> q;
int qh, qt;
void resize(int _n){
n = _n;
ex.resize(n);
q.resize(n);
pin.resize(n);
pout.resize(n);
adj.resize(n);
level.resize(n);
in.resize(n);
out.resize(n);
}
MPM(){}
MPM(int _n, int _s, int _t){resize(_n); s = _s; t = _t;}
void add_edge(int v, int u, long long cap){
edges.push_back(FlowEdge(v, u, cap));
edges.push_back(FlowEdge(u, v, 0));
adj[v].push_back(m); // 同样添加边的编号
adj[u].push_back(m + 1);
m += 2;
}
// 创建分层图
bool bfs(){
while(qh < qt){
int v = q[qh++];
for(int id : adj[v]){
if(edges[id].cap - edges[id].flow < 1)continue;
if(level[edges[id].u] != -1)continue;
level[edges[id].u] = level[v] + 1;
q[qt++] = edges[id].u;
}
}
return level[t] != -1;
}
// 点容量
long long pot(int v){
return min(pin[v], pout[v]);
}
// 如果已经让一个参考点剩余容量为0,那就再也不用考虑这个点和它的边了
void remove_node(int v){
for(int i : in[v]){
int u = edges[i].v;
auto it = find(out[u].begin(), out[u].end(), i);
out[u].erase(it);
pout[u] -= edges[i].cap - edges[i].flow;
}
for(int i : out[v]){
int u = edges[i].u;
auto it = find(in[u].begin(), in[u].end(), i);
in[u].erase(it);
pin[u] -= edges[i].cap - edges[i].flow;
}
}
// 上述说明的将参考点到起点或者终点删去对应流大小
void push(int from, int to, long long f, bool forw){
qh = qt = 0;
ex.assign(n, 0);
ex[from] = f;
q[qt++] = from;
while(qh < qt){
int v = q[qh++];
if(v == to)
break;
long long must = ex[v];
auto it = forw ? out[v].begin() : in[v].begin();
while(true){
int u = forw ? edges[*it].u : edges[*it].v;
long long pushed = min(must, edges[*it].cap - edges[*it].flow);
if(pushed == 0)break;
if(forw){
pout[v] -= pushed;
pin[u] -= pushed;
}
else{
pin[v] -= pushed;
pout[u] -= pushed;
}
if(ex[u] == 0)
q[qt++] = u;
ex[u] += pushed;
edges[*it].flow += pushed;
edges[(*it)^1].flow -= pushed;
must -= pushed;
if(edges[*it].cap - edges[*it].flow == 0){
auto jt = it;
++jt;
if(forw){
in[u].erase(find(in[u].begin(), in[u].end(), *it));
out[v].erase(it);
}
else{
out[u].erase(find(out[u].begin(), out[u].end(), *it));
in[v].erase(it);
}
it = jt;
}
else break;
if(!must)break;
}
}
}
// 计算最大流量
long long flow(){
long long ans = 0;
while(true){
pin.assign(n, 0);
pout.assign(n, 0);
level.assign(n, -1);
alive.assign(n, true);
level[s] = 0;
qh = 0; qt = 1;
q[0] = s;
if(!bfs())
break;
for(int i = 0; i < n; i++){
out[i].clear();
in[i].clear();
}
for(int i = 0; i < m; i++){
if(edges[i].cap - edges[i].flow == 0)
continue;
int v = edges[i].v, u = edges[i].u;
if(level[v] + 1 == level[u] && (level[u] < level[t] || u == t)){
in[u].push_back(i);
out[v].push_back(i);
pin[u] += edges[i].cap - edges[i].flow;
pout[v] += edges[i].cap - edges[i].flow;
}
}
pin[s] = pout[t] = flow_inf;
while(true){
int v = -1;
for(int i = 0; i < n; i++){
if(!alive[i])continue;
if(v == -1 || pot(i) < pot(v))
v = i;
}
if(v == -1)
break;
if(pot(v) == 0){
alive[v] = false;
remove_node(v);
continue;
}
long long f = pot(v);
ans += f;
push(v, s, f, false); // 删去参考点到S,true和false表示方向
push(v, t, f, true); // 删去参考点到T
alive[v] = false;
remove_node(v); // 删去节点以及对应的边
}
}
return ans;
}
};
ISAP 算法
通过改进层次图的构建和流量的增广来加速算法的运行。ISAP的关键优化在于使用了一种叫做分层推进(Relabel-to-Front)的技术,以及通过维护每个节点的高度信息来减少不必要的搜索和迭代。
对于原来的ISAP算法,每次删去堵塞流,都需要重新创建分层图,这一步是效率极低的,因为整个遍历了一遍图,实际层数变化的只有其中的几个点。
而变化的原因也是出于找到的流废掉了几条边,使得层数变化,那能否在DFS的过程中就解决这个问题。
显然是在dfs回溯的过程中才能修改层数,所以是从终点往起点进行回溯。那么层数的更新就成为了一个问题,如果从后往前,则最前面的层数只能越来越减少,甚至变成负数,因此我们改变保存层数的方式,保存点到终点的距离,也就是反向的层数。
因此第一步应当是建立初始的层数,也就是对于反图从终点进行BFS标号。
有以下步骤:
- 对于反图从终点进行BFS标号
- 当前顶点 V为终点时增广
- 当前顶点有满足 dist[V] = dist[j] + 1 的出弧时前进
- 当前顶点无满足条件的出弧时重标号并回退一步。对V点的重标号操作为 dist[i] = 1 + min。min为对于V相邻的dis最小(离汇点最近)的点。
- 整个循环当源点 s 的距离标号 dist[s] >= n 时结束。
GAP优化
记录每个层的点的数量,gap[i]数组表示距离标号为i的点有多少个。如果重标号使得gap数组中原标号数目变为0,即修改则无法到达,则算法结束。
const int maxn = 1000; // 最大节点数,根据需要调整
const int INF = 1e9; // 无穷大
struct Edge {
int from, to, cap, flow;
Edge(int u, int v, int c, int f) : from(u), to(v), cap(c), flow(f) {}
};
bool operator<(const Edge& a, const Edge& b) {
return a.from < b.from || (a.from == b.from && a.to < b.to);
}
struct ISAP {
int n, m, s, t;
vector<Edge> edges;
vector<int> G[maxn];
bool vis[maxn];
int d[maxn];
int cur[maxn];
int p[maxn];
int num[maxn];
// 添加边
void AddEdge(int from, int to, int cap) {
edges.emplace_back(from, to, cap, 0);
edges.emplace_back(to, from, 0, 0);
m = edges.size();
G[from].push_back(m - 2);
G[to].push_back(m - 1);
}
// 广度优先搜索建立层次图
bool BFS() {
memset(vis, 0, sizeof(vis));
queue<int> Q;
Q.push(t);
vis[t] = 1;
d[t] = 0;
while (!Q.empty()) {
int x = Q.front();
Q.pop();
for (int i : G[x]) {
Edge& e = edges[i ^ 1];
if (!vis[e.from] && e.cap > e.flow) {
vis[e.from] = 1;
d[e.from] = d[x] + 1;
Q.push(e.from);
}
}
}
return vis[s];
}
// 初始化
void init(int n) {
this->n = n;
for (int i = 0; i < n; i++) G[i].clear();
edges.clear();
}
// 增广路径
int Augment() {
int x = t, a = INF;
while (x != s) {
Edge& e = edges[p[x]];
a = min(a, e.cap - e.flow);
x = edges[p[x]].from;
}
x = t;
while (x != s) {
edges[p[x]].flow += a;
edges[p[x] ^ 1].flow -= a;
x = edges[p[x]].from;
}
return a;
}
// 求最大流
int Maxflow(int s, int t) {
this->s = s;
this->t = t;
int flow = 0;
BFS();
memset(num, 0, sizeof(num));
for (int i = 0; i < n; i++) num[d[i]]++;
int x = s;
memset(cur, 0, sizeof(cur));
while (d[s] < n) {
if (x == t) {
flow += Augment();
x = s;
}
bool ok = false;
for (int& i = cur[x]; i < G[x].size(); i++) {
Edge& e = edges[G[x][i]];
if (e.cap > e.flow && d[x] == d[e.to] + 1) {
ok = true;
p[e.to] = G[x][i];
cur[x] = i;
x = e.to;
break;
}
}
if (!ok) {
int m = n - 1;
for (int i : G[x]) {
Edge& e = edges[i];
if (e.cap > e.flow) m = min(m, d[e.to]);
}
if (--num[d[x]] == 0) break;
num[d[x] = m + 1]++;
cur[x] = 0;
if (x != s) x = edges[p[x]].from;
}
}
return flow;
}
};