【图论】网络流算法入门

(决定狠狠加训图论了,从一直想学但没启动的网络流算法开始。)

网络流问题

问题定义 :在带权有向图 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)

最大流-最小割定理

在任何网络中,最大流的值等于最小割的容量
直观理解

• 流量受限于网络中"最窄的瓶颈",即最小割的容量。

• 最大流无法超过任何割的容量,而存在至少一个割的容量等于最大流。

证明思路

  1. 弱对偶性:最大流 ≤ 最小割(显然成立)。
  2. 强对偶性 :通过构造残留网络,证明存在一个割的容量等于最大流。
    当算法终止时,残留网络中 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算法

原理

  1. 分层图:通过BFS构建层次图(将节点按到源点的最短距离分层)。
  2. 阻塞流:在分层图上用DFS找增广路径,并通过"当前弧优化"跳过无效边。
  3. 多路增广 :允许一次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)

原理

  1. 动态分层:初始BFS分层后,逆向维护层次值(从汇点开始更新)。
  2. 重贴标签:当某层节点耗尽时,调整节点层次(类似最大流中的GAP优化)。
  3. 单次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)

原理

  1. 预流推进:允许节点暂时超额流量,通过"推进"操作将流量推向汇点。
  2. 最高标号优先:优先处理层次高的活跃节点,用优先队列维护。
  3. 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

原理

  1. SPFA找最短路:每次用SPFA(队列优化的Bellman-Ford)找费用最小的增广路。
  2. 沿最短路增广 :在最短路径上调整流量并更新残余网络。
    特点
    • 支持负权边 ,但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

原理

  1. 势函数去负权 :通过势函数 h ( u ) h(u) h(u)调整边权,使得所有边权非负。
  2. Dijkstra找最短路:用Dijkstra代替SPFA,时间复杂度更稳定。
  3. 势函数更新 :每次增广后更新势函数,保持边权非负。
    时间复杂度 : 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算法模板)


总结

最大流问题 :优先选择DinicISAP (代码简单),超大规模数据用HLPP

费用流问题

• 含负权边 → MCMF-SPFA

• 无负权边 → MCMF-Dijkstra (更高效)。

关键区别

• Dinic/ISAP/HLPP解决最大流,MCMF类解决费用流。

• SPFA允许负权但慢,Dijkstra需正权但快;HLPP通过预流推进优化最大流。

相关推荐
Ace'34 分钟前
每日一题之既约分数
c++
理智的灰太狼42 分钟前
1015 Reversible Primes
数据结构·c++·算法
Dream it possible!1 小时前
LeetCode 热题 100_杨辉三角(82_118_简单_C++)(动态规划)
c++·算法·leetcode·动态规划
狐凄1 小时前
练习题:110
开发语言·python·算法
JNU freshman1 小时前
蓝桥杯 之 LCA算法
算法·蓝桥杯
xcx66571 小时前
Freertos3(事件标志组 任务通知 软件定时器 )
算法
江湖人称菠萝包1 小时前
侯捷 C++ 课程学习笔记:C++内存管理机制
c++·笔记
烟锁池塘柳01 小时前
【数学建模】动态规划算法(Dynamic Programming,简称DP)详解与应用
算法·数学建模·动态规划
执笔论英雄1 小时前
【DeepSeek学C++】移动构造函数
c++
siy23332 小时前
[c语言日寄]柔性数组
c语言·开发语言·笔记·学习·算法·柔性数组