(决定狠狠加训图论了,从一直想学但没启动的网络流算法开始。)
网络流问题
• 问题定义 :在带权有向图 G = ( V , E ) G=(V, E) G=(V,E) 中,每条边 e = ( u , v ) e=(u, v) e=(u,v) 有容量 c ( u , v ) c(u, v) c(u,v),求从源点 s s s 到汇点 t t t 的最大流量。
1. 最大流 = 最小割定理
• 最小割的定义 :
割(Cut)是将图 G G G 的顶点分为两个不相交集合 S S S 和 T T T,其中 s ∈ S s \in S s∈S, t ∈ T t \in T t∈T。
割的容量是 所有从 S S S 到 T T T 的边的容量之和 ,即: 容量 ( S , T ) = ∑ u ∈ S , v ∈ T c ( u , v ) \text{容量}(S, T) = \sum_{u \in S, v \in T} c(u, v) 容量(S,T)=u∈S,v∈T∑c(u,v)
• 最大流-最小割定理 :
在任何网络中,最大流的值等于最小割的容量 。
直观理解 :
• 流量受限于网络中"最窄的瓶颈",即最小割的容量。
• 最大流无法超过任何割的容量,而存在至少一个割的容量等于最大流。
• 证明思路:
- 弱对偶性:最大流 ≤ 最小割(显然成立)。
- 强对偶性 :通过构造残留网络,证明存在一个割的容量等于最大流。
当算法终止时,残留网络中 s s s 能到达的顶点集合 S S S,其余为 T T T,此时 S S S 到 T T T 的边流量饱和,容量等于最大流。
2. 残留网络与反向边
• 残留网络 G f G_f Gf :
• 对每条边 e = ( u , v ) e=(u, v) e=(u,v),定义其反向边 e ′ = ( v , u ) e'=(v, u) e′=(v,u),容量为当前流量 f ( u , v ) f(u, v) f(u,v)。
• 残留容量:
c f ( u , v ) = c ( u , v ) − f ( u , v ) , c f ( v , u ) = f ( u , v ) c_f(u, v) = c(u, v) - f(u, v), \quad c_f(v, u) = f(u, v) cf(u,v)=c(u,v)−f(u,v),cf(v,u)=f(u,v)
• 作用:允许"退回"流量,寻找更优路径。
• 增广路径 :
在残留网络 G f G_f Gf 中找到一条从 s s s 到 t t t 的路径,路径上的最小残留容量称为 增广量 。
通过增加该路径的流量,逐步逼近最大流。
3. 费用流问题
• 问题定义 :
在最大流问题的基础上,每条边 e = ( u , v ) e=(u, v) e=(u,v) 有一个单位流量费用 w ( u , v ) w(u, v) w(u,v),求在最大流的前提下,总费用最小的流。
• 关键思想 :
• 最小费用最大流 :优先选择费用最小的增广路径。
• 算法扩展 :
◦ Successive Shortest Path (SSP) :每次用SPFA/Bellman-Ford找最短路增广。
◦ Primal-Dual :结合Dijkstra和势函数处理负权边。
◦ Cost-Scaling:通过缩放费用值优化效率。
• 费用流的反向边处理 :
反向边的费用为 − w ( u , v ) -w(u, v) −w(u,v),表示退回流量时费用被抵消。
算法模板:
1. Dinic算法
• 原理:
- 分层图:通过BFS构建层次图(将节点按到源点的最短距离分层)。
- 阻塞流:在分层图上用DFS找增广路径,并通过"当前弧优化"跳过无效边。
- 多路增广 :允许一次DFS找到多条增广路径,减少递归次数。
• 时间复杂度 : O ( n 2 m ) O(n^2 m) O(n2m),实际表现优秀,适合大多数场景。
• 适用问题 :最大流问题,在稠密图和稀疏图中均表现良好。
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define endl '\n'
#define int long long
#define pb push_back
#define pii pair<int, int>
#define FU(i, a, b) for (int i = (a); i <= (b); ++i)
#define FD(i, a, b) for (int i = (a); i >= (b); --i)
const int MOD = 1e9 + 7;
const int inf = 1e18; // 表示无穷大的容量
const int maxn = 1e5 + 10; // 最大节点数
// 边的结构体
struct edge {
int from, to; // 边的起点和终点
int cap; // 边的容量
int flow; // 边当前的流量
edge(int u, int v, int c, int f) : from(u), to(v), cap(c), flow(f) {}
};
// Dinic算法实现类
struct Dinic {
int n, m; // 节点数、边数
int s, t; // 源点、汇点
vector<edge> edges; // 所有边的集合,边数两倍(包含反向边)
vector<int> G[maxn]; // 邻接表,G[i]保存节点i的所有边在edges中的索引
bool vis[maxn]; // BFS使用的访问标记数组
int d[maxn]; // 层次网络中各节点的层次(距离)
int cur[maxn]; // 当前弧优化数组,记录每个节点当前处理的边
void init(int n) { // 初始化,n为节点数
this->n = n;
for (int i = 0; i < n; i++)
G[i].clear();
edges.clear();
}
// 清空所有边的流量,用于多次计算不同流的情况
void clear() {
for (int i = 0; i < edges.size(); i++)
edges[i].flow = 0;
}
// 将边的容量减去已使用的流量,用于调整残余网络
void reduce() {
for (int i = 0; i < edges.size(); i++)
edges[i].cap -= edges[i].flow;
}
// 添加一条从from到to,容量为cap的有向边及其反向边
void addedge(int from, int to, int cap) {
edges.push_back(edge(from, to, cap, 0)); // 正向边,初始流量0
edges.push_back(edge(to, from, 0, 0)); // 反向边,初始容量0,流量0
m = edges.size();
G[from].push_back(m - 2); // 记录正向边在edges中的索引
G[to].push_back(m - 1); // 记录反向边在edges中的索引
}
// BFS构建层次网络,返回是否存在从s到t的路径
bool BFS() {
memset(vis, 0, sizeof(vis));
queue<int> Q;
Q.push(s); // 源点入队
vis[s] = true;
d[s] = 0;
while (!Q.empty()) {
int x = Q.front();
Q.pop();
for (int i = 0; i < G[x].size(); i++) { // 遍历x的所有邻接边
edge &e = edges[G[x][i]];
// 若边的终点未访问,且仍有剩余容量
if (!vis[e.to] && e.cap > e.flow) {
vis[e.to] = true;
d[e.to] = d[x] + 1; // 层次+1
Q.push(e.to);
}
}
}
return vis[t]; // 返回汇点是否可达
}
// DFS寻找增广路径,x为当前节点,a为当前路径上的最小剩余容量
int DFS(int x, int a) {
if (x == t || a == 0) // 若到达汇点或剩余容量为0,直接返回
return a;
int flow = 0, f;
// 使用当前弧优化,避免重复访问已经处理过的边
for (int &i = cur[x]; i < G[x].size(); i++) {
edge &e = edges[G[x][i]];
// 层次递增且存在剩余容量
if (d[x] + 1 == d[e.to] &&
(f = DFS(e.to, min(a, e.cap - e.flow))) > 0) {
e.flow += f; // 更新正向边流量
edges[G[x][i] ^ 1].flow -= f; // 反向边流量减少(允许反向增广)
flow += f; // 累加到总流量
a -= f; // 剩余容量减少
if (a == 0)
break; // 剩余容量为0,无需继续搜索
}
}
if (flow == 0)
d[x] = -2; // 炸点优化
return flow;
}
// 计算从s到t的最大流
int Maxflow(int s, int t) {
this->s = s;
this->t = t;
int flow = 0;
while (BFS()) { // 每次BFS构建层次网络
memset(cur, 0, sizeof(cur)); // 重置当前弧
flow += DFS(s, inf); // 进行多路增广,累加流量
}
return flow;
}
// 返回最小割的边集合(需在Maxflow之后调用)
vector<int> Mincut() {
vector<int> ans;
for (int i = 0; i < edges.size(); i++) {
edge &e = edges[i];
// 若边的起点在层次网络中可达,终点不可达,且为原始正向边
if (vis[e.from] && !vis[e.to] && e.cap > 0)
ans.push_back(i);
}
return ans;
}
} dinic;
signed main() {
cin.tie(0)->ios::sync_with_stdio(0);
int n, m, s, t;
cin >> n >> m >> s >> t;
dinic.init(n + 5); // 初始化Dinic结构体
while (m--) {
int s, t, u;
cin >> s >> t >> u;
dinic.addedge(s, t, u); // 添加边
}
// 计算从节点s到节点t的最大流
cout << dinic.Maxflow(s, t);
return 0;
}
2. ISAP算法(Improved Shortest Augmenting Path)
• 原理:
- 动态分层:初始BFS分层后,逆向维护层次值(从汇点开始更新)。
- 重贴标签:当某层节点耗尽时,调整节点层次(类似最大流中的GAP优化)。
- 单次BFS :仅需一次BFS初始化,后续通过回溯调整层次,减少重复分层。
• 时间复杂度 : O ( n 2 m ) O(n^2 m) O(n2m),常数优于Dinic,适合大规模稀疏图。
• 适用问题 :最大流问题,尤其适合边数较多的图。
cpp
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <iostream>
#include <queue>
#include <ctime>
using namespace std;
const int maxn = 1100;
const int maxedges = 51000;
const int inf = 1 << 30;
struct edge {
int to, flow;
edge *next, *pair;
edge() {}
edge(int to, int flow, edge *next) : to(to), flow(flow), next(next) {}
void *operator new(unsigned, void *p) { return p; }
};
struct ISAP {
int gap[maxn], h[maxn], n, s, t;
edge *cur[maxn], *first[maxn], edges[maxedges * 2], *ptr;
void init(int n) {
this->n = n;
ptr = edges;
memset(first, 0, sizeof(first));
memset(gap, 0, sizeof(gap));
memset(h, 0, sizeof(h));
gap[0] = n;
}
void add_edge(int from, int to, int cap) {
first[from] = new(ptr++)edge(to, cap, first[from]);
first[to] = new(ptr++)edge(from, 0, first[to]);
first[from]->pair = first[to];
first[to]->pair = first[from];
}
int augment(int x, int limit) {
if (x == t)
return limit;
int rest = limit;
for (edge*& e = cur[x]; e; e = e->next) if (e->flow && h[e->to] + 1 == h[x]) {
int d = augment(e->to, min(rest, e->flow));
e->flow -= d, e->pair->flow += d, rest -= d;
if (h[s] == n || !rest)
return limit - rest;
}
int minh = n;
for (edge *e = cur[x] = first[x]; e; e = e->next) if (e->flow)
minh = min(minh, h[e->to] + 1);
if (--gap[h[x]] == 0)
h[s] = n;
else
++gap[h[x] = minh];
return limit - rest;
}
int solve(int s, int t, int limit = inf) {
this->s = s; this->t = t;
memcpy(cur, first, sizeof(first)); // memcpy!
int flow = 0;
while (h[s] < n && flow < limit)
flow += augment(s, limit - flow);
return flow;
}
}isap;
int main()
{
freopen("D:\\in.txt", "r", stdin);
int n, m;
scanf("%d %d", &n, &m);
isap.init(n + 5);
while (m--)
{
int s, t, u;
scanf("%d %d %d", &s, &t, &u);
isap.add_edge(s, t, u);
}
auto start = clock();
printf("%d\n", isap.solve(1, n));
double tot = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
printf("Isap: %f\n", tot);
return 0;
}
3. HLPP算法(Highest Label Preflow Push)
• 原理:
- 预流推进:允许节点暂时超额流量,通过"推进"操作将流量推向汇点。
- 最高标号优先:优先处理层次高的活跃节点,用优先队列维护。
- GAP优化 :当层次出现断层时,直接标记断层上方节点为不可达。
• 时间复杂度 : ( O ( n 2 m ) ) (O(n^2 \sqrt{m})) (O(n2m )),理论最优,适合超大规模数据。
• 适用问题 :最大流问题,在特定题目中比Dinic/ISAP快数倍。
cpp
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <ctime>
using namespace std;
const int maxn = 2e5 + 5, maxedges = 4e6 + 5, inf = 0x3f3f3f3f;
int n, m, s, t, tot;
int v[maxedges * 2], w[maxedges * 2], first[maxn], nxt[maxedges * 2];
int h[maxn], e[maxn], gap[maxn * 2], inq[maxn];//节点高度是可以到达2n-1的
struct cmp{
inline bool operator()(int a, int b) const {
return h[a] < h[b];//因为在优先队列中的节点高度不会改变,所以可以直接比较
}
};
queue<int> Q;
priority_queue<int, vector<int>, cmp> heap;
inline void add_edge(int from, int to, int flow) {
tot += 2;
v[tot + 1] = from; v[tot] = to; w[tot] = flow; w[tot + 1] = 0;
nxt[tot] = first[from]; first[from] = tot;
nxt[tot + 1] = first[to]; first[to] = tot + 1;
return;
}
inline bool bfs() {
memset(h + 1, 0x3f, sizeof(int) * n);
h[t] = 0;
Q.push(t);
while (!Q.empty())
{
int now = Q.front(); Q.pop();
for (int go = first[now]; go; go = nxt[go])
if (w[go ^ 1] && h[v[go]] > h[now] + 1)
h[v[go]] = h[now] + 1, Q.push(v[go]);
}
return h[s] != inf;
}
inline void push(int now) {
for (int go = first[now]; go; go = nxt[go]) {
if (w[go] && h[v[go]] + 1 == h[now]) {
int d = min(e[now], w[go]);
w[go] -= d; w[go ^ 1] += d; e[now] -= d; e[v[go]] += d;
if (v[go] != s && v[go] != t && !inq[v[go]])
heap.push(v[go]), inq[v[go]] = 1;
if (!e[now])//已经推送完毕可以直接退出
break;
}
}
}
inline void relabel(int now) {
h[now] = inf;
for (int go = first[now]; go; go = nxt[go])
if (w[go] && h[v[go]] + 1 < h[now])
h[now] = h[v[go]] + 1;
return;
}
inline int hlpp() {
int now, d;
if (!bfs()) //s和t不连通
return 0;
h[s] = n;
memset(gap, 0, sizeof(int) * (n * 2));
for (int i = 1; i <= n; i++)
if (h[i] < inf)
++gap[h[i]];
for (int go = first[s]; go; go = nxt[go]) {
if (d = w[go]) {
w[go] -= d; w[go ^ 1] += d; e[s] -= d; e[v[go]] += d;
if (v[go] != s && v[go] != t && !inq[v[go]])
heap.push(v[go]), inq[v[go]] = 1;
}
}
while (!heap.empty()) {
inq[now = heap.top()] = 0; heap.pop(); push(now);
if (e[now]) {
if (!--gap[h[now]]) //gap优化,因为当前节点是最高的所以修改的节点一定不在优先队列中,不必担心修改对优先队列会造成影响
for (int i = 1; i <= n; i++)
if (i != s && i != t && h[i] > h[now] && h[i] < n + 1)
h[i] = n + 1;
relabel(now); ++gap[h[now]];
heap.push(now); inq[now] = 1;
}
}
return e[t];
}
int main()
{
freopen("D:\\in.txt", "r", stdin);
scanf("%d %d", &n, &m); s = 1; t = n;
while (m--)
{
int s, t, u;
scanf("%d %d %d", &s, &t, &u);
add_edge(s, t, u);
}
auto start = clock();
printf("%d\n", hlpp());
double tot = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
printf("HLPP: %f\n", tot);
return 0;
}
4. MCMF-SPFA
• 原理:
- SPFA找最短路:每次用SPFA(队列优化的Bellman-Ford)找费用最小的增广路。
- 沿最短路增广 :在最短路径上调整流量并更新残余网络。
• 特点 :
• 支持负权边 ,但SPFA可能被卡到 O ( n m ) O(nm) O(nm)。
• 适合费用流中存在负权但边数较少的图。
• 适用问题 :最小费用最大流问题(费用流)。
cpp
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <ctime>
using namespace std;
const int maxn = 20100;
const int inf = 1 << 30;
struct edge {
int from, to, cap, flow, cost;
edge(int u, int v, int c, int f, int w) : from(u), to(v), cap(c), flow(f), cost(w) {}
};
struct MCMF {
int n, m;
vector<edge> edges;
vector<int> G[maxn];
int inq[maxn];
int d[maxn];
int p[maxn];
int a[maxn];
void init(int n) {
this->n = n;
for (int i = 0; i < n; ++i)
G[i].clear();
edges.clear();
}
void add_edge(int from, int to, int cap, int cost) {
edges.push_back(edge(from, to, cap, 0, cost));
edges.push_back(edge(to, from, 0, 0, -cost));
m = edges.size();
G[from].push_back(m - 2);
G[to].push_back(m - 1);
}
bool BellmanFord(int s, int t, int &flow, int &cost, int limit) {
for (int i = 0; i < n; ++i)
d[i] = inf;
memset(inq, 0, sizeof(inq));
d[s] = 0; inq[s] = 1; p[s] = 0; a[s] = inf;
queue<int> Q;
Q.push(s);
while (!Q.empty()) {
int u = Q.front(); Q.pop();
inq[u] = false;
for (unsigned i = 0; i < G[u].size(); ++i) {
edge &e = edges[G[u][i]];
if (e.cap > e.flow && d[e.to] > d[u] + e.cost) {
d[e.to] = d[u] + e.cost;
p[e.to] = G[u][i];
a[e.to] = min(a[u], e.cap - e.flow);
if (!inq[e.to]) {
Q.push(e.to);
inq[e.to] = true;
}
}
}
}
if (d[t] == inf)
return false;
a[t] = min(a[t], limit - flow);
flow += a[t];
cost += d[t] * a[t];
for (int u = t; u != s; u = edges[p[u]].from) {
edges[p[u]].flow += a[t];
edges[p[u] ^ 1].flow -= a[t];
}
return true;
}
int solve(int s, int t, int limit = inf) {
int flow = 0, cost = 0;
while (flow < limit && BellmanFord(s, t, flow, cost, limit));
return cost;
}
}mcmf;
int main()
{
freopen("D:\\in.txt", "r", stdin);
int n, m, k;
scanf("%d %d %d", &n, &m, &k);
mcmf.init(n + 10);
for (int i = 1; i <= m; ++i) {
int x, y, c, w;
scanf("%d %d %d %d", &x, &y, &c, &w);
mcmf.add_edge(x, y, c, w);
}
auto start = clock();
printf("%d\n", mcmf.solve(1, n, k));
double tot = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
printf("MCMF-spfa: %f\n", tot);
return 0;
}
5. MCMF-Dijkstra
• 原理:
- 势函数去负权 :通过势函数 h ( u ) h(u) h(u)调整边权,使得所有边权非负。
- Dijkstra找最短路:用Dijkstra代替SPFA,时间复杂度更稳定。
- 势函数更新 :每次增广后更新势函数,保持边权非负。
• 时间复杂度 : O ( f m log n ) O(f m \log n) O(fmlogn), f f f为最大流量,适合正权图。
• 适用问题 :最小费用最大流问题,且图中无负权或通过势函数消除负权。
cpp
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
#include <queue>
#include <ctime>
using namespace std;
const int maxn = 21000;
const int inf = 1 << 30;
struct edge {
int to, cap, cost, rev;
edge() {}
edge(int to, int cap, int cost, int rev) : to(to), cap(cap), cost(cost), rev(rev) {}
};
struct MCMF {
int n, h[maxn], d[maxn], pre[maxn], num[maxn];
vector<edge> G[maxn];
void init(int n) {
this->n = n;
for (int i = 0; i <= n; ++i)
G[i].clear();
}
void add_edge(int from, int to, int cap, int cost) {
G[from].push_back(edge(to, cap, cost, G[to].size()));
G[to].push_back(edge(from, 0, -cost, G[from].size() - 1));
}
//flow是自己传进去的变量,就是最后的最大流,返回的是最小费用
int solve(int s, int t, int &flow, int limit = inf) {
int cost = 0;
memset(h, 0, sizeof(h));
while (limit) {
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>> > Q;
for (int i = 0; i <= n; ++i)
d[i] = inf;
d[s] = 0;
Q.emplace(0, s);
while (!Q.empty()) {
auto now = Q.top(); Q.pop();
int u = now.second;
if (d[u] < now.first) continue;
for (int i = 0; i < G[u].size(); ++i) {
edge &e = G[u][i];
if (e.cap > 0 && d[e.to] > d[u] + e.cost + h[u] - h[e.to]) {
d[e.to] = d[u] + e.cost + h[u] - h[e.to];
pre[e.to] = u;
num[e.to] = i;
Q.emplace(d[e.to], e.to);
}
}
}
if (d[t] == inf) break;
for (int i = 0; i <= n; ++i)
h[i] += d[i];
int a = limit;
for (int u = t; u != s; u = pre[u])
a = min(a, G[pre[u]][num[u]].cap);
limit -= a; flow += a; cost += a * h[t];
for (int u = t; u != s; u = pre[u]) {
edge &e = G[pre[u]][num[u]];
e.cap -= a;
G[u][e.rev].cap += a;
}
}
return cost;
}
}mcmf;
int main()
{
freopen("D:\\in.txt", "r", stdin);
int n, m, k, flow = 0;
scanf("%d %d %d", &n, &m, &k);
mcmf.init(n + 10);
for (int i = 1; i <= m; ++i) {
int x, y, c, w;
scanf("%d %d %d %d", &x, &y, &c, &w);
mcmf.add_edge(x, y, c, w);
}
auto start = clock();
printf("%d\n", mcmf.solve(1, n, flow, k));
printf("flow: %d\n", flow);
double tot = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
printf("MCMF-dijkstra: %f\n", tot);
return 0;
}
(代码来自孙明志大佬的xcpc算法模板)
总结
• 最大流问题 :优先选择Dinic 或ISAP (代码简单),超大规模数据用HLPP 。
• 费用流问题 :
• 含负权边 → MCMF-SPFA 。
• 无负权边 → MCMF-Dijkstra (更高效)。
• 关键区别 :
• Dinic/ISAP/HLPP解决最大流,MCMF类解决费用流。
• SPFA允许负权但慢,Dijkstra需正权但快;HLPP通过预流推进优化最大流。