相比于EK算法,Dinic算法在稀疏图上效率相当(可视n、m相近),而在稠密图上的处理更优秀。
对网络流基本思想不清楚可参见我的另一篇博客《网络流的核心思想》。
分层思想
Dinic算法在每次增广前,先用 BFS 来将图分层。设源点的层数为 0 0 0,那么一个点的层数便是它离源点的最近距离。
通过分层,可起到如下两种效果:
- 如果不存在到汇点的增广路(即汇点的层数不存在),我们即可停止增广。
- 确保我们找到的增广路是最短的(每次找增广路的时候,都只找比当前点层数多 1 1 1的点进行增广)。
两个优化
- 多路增广:在使用EK算法时,由于我们使用BFS找增广路,当每次贪心取得一条增广路并增广后,我们从汇点沿着增广路往前走,很可能会遇见一些点实际经过的流量小于改点所有入度的容量和,或者说在残量网络中,改点的入度和初度均大于 0 0 0,显然我们可以利用该点后向弧的残量向该点前向弧增广。上面的这一过程涉及到回溯的操作,BFS处理这一操作并不方便。而Dinic算法使用DFS找增广路,这就给了我们使用一次DFS找到多条增广路并增广的机会(见代码),大大提高了算法的效率。
- 当前弧优化:显然,由于上述多路增广操作,对于任意节点,我们每增广它的一条前向弧,意味着这条弧后所有边都被我们多路增广过了,那么当我们再次处理该节点时,就可以不用考虑这条弧。或者说,如果一条边已经被增广过,那么它就没有可能被增广第二次。那么,我们下一次进行增广的时候,就可以不必再走那些已经被增广过的边。
时间复杂度
最多仅需 n − 1 n-1 n−1轮增广即可求得最大流,单轮增广的最坏复杂度是 O ( n m ) O(nm) O(nm),总的复杂度为 O ( n 2 m ) O(n^2m) O(n2m),但事实上对于随机图,Dinic的实际运行速度要远由于这一时间上界。
特别地,在求解二分图最大匹配问题时,Dinic 算法的时间复杂度是 O ( m n ) O(m\sqrt n) O(mn)。
对于时间复杂度的证明可参考OI-Wiki相关内容。
//
// Created by Visors on 2020/9/26.
//
// 题目名:【模板】网络最大流
// 题目来源:luogu
// 题目链接:https://www.luogu.com.cn/problem/P3376
// 算法:Dinic.cpp
// 用途:网络最大流
// 时间复杂度:O(m*n^2)
//
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int oo = 0x7fffffff;
struct Edge {
int from; // 起点
int to; // 终点
int capacity; // 容量
int flow; // 流量
int next; // 下条边
Edge() = default;
Edge(int from, int to, int capacity, int flow, int next) : from(from), to(to), capacity(capacity), flow(flow),
next(next) {}
};
vector<Edge> edges; // 边集
vector<int> heads; // 首边集
vector<int> levels; // 层级
vector<bool> vis; // 是否增广
vector<int> cur; // 当前弧
int n, m, s, t;
inline void addEdge(int u, int v, int c, int f) {
edges.emplace_back(v, u, 0, -f, heads[v]);
heads[v] = edges.size() - 1;
edges.emplace_back(u, v, c, f, heads[u]);
heads[u] = edges.size() - 1;
}
bool bfs() {
// 初始化分层以及访问标记
fill(levels.begin(), levels.end(), 0);
fill(vis.begin(), vis.end(), false);
queue<int> q;
q.push(s);
vis[s] = true;
// levels[s] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = heads[u]; ~i; i = edges[i].next) {
Edge &e = edges[i];
int &v = e.to;
if (!vis[v] && e.capacity > e.flow) {
vis[v] = true;
levels[v] = levels[u] + 1; // 层级+1
q.push(v);
}
}
}
return vis[t]; // 返回
}
ll dfs(int u, int imp) {
if (u == t || imp == 0) return imp;
ll ret = 0;
int f;
// 当前弧优化,注意下面的i是cur元素的引用
for (int &i = cur[u]; ~i; i = edges[i].next) {
Edge &e = edges[i];
int &v = e.to;
// 多路增广优化,利用dfs性质分路求解尽可能跑尽当前可改进量imp
if (levels[v] == levels[u] + 1 && (f = dfs(v, min(imp, e.capacity - e.flow))) > 0) {
e.flow += f;
edges[i ^ 1].flow -= f;
ret += f; // 网络总流量增加
imp -= f; // 当前可改进量减少
if (imp == 0) break;
}
}
return ret;
}
ll maxFlow() {
ll flow = 0;
// 只要BFS还能给汇点分层,就证明当前残量网络还存在从源到汇的增广路
while (bfs()) {
// 每轮BFS前重置当前弧标记
for (int i = 0; i < n; i++) cur[i] = heads[i];
flow += dfs(s, oo);
}
return flow;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
scanf("%d%d%d%d", &n, &m, &s, &t);
s--, t--;
edges.clear();
heads.resize(n, -1);
levels.resize(n);
vis.resize(n);
cur.resize(n);
for (int i = 1, u, v, c; i <= m; i++) {
scanf("%d%d%d", &u, &v, &c);
addEdge(u - 1, v - 1, c, 0);
}
cout << maxFlow() << endl;
return 0;
}