题解:洛谷 P3376 网络最大流
【题目来源】
【题目描述】
如题,给出一个网络图,以及其源点和汇点,求出其网络最大流。
【输入】
第一行包含四个正整数 \(n,m,s,t\),分别表示点的个数、有向边的个数、源点序号、汇点序号。
接下来 \(m\) 行每行包含三个正整数 \(u_i,v_i,w_i\),表示第 \(i\) 条有向边从 \(u_i\) 出发,到达 \(v_i\),边权为 \(w_i\)(即该边最大流量为 \(w_i\))。
【输出】
一行,包含一个正整数,即为该网络的最大流。
【输入样例】
4 5 4 3
4 2 30
4 3 20
2 3 20
2 1 30
1 3 30
【输出样例】
50
【核心思想】
-
问题分析:给定一个 \(n\) 个点、\(m\) 条边的有向带权图,每条边有容量上限 \(w_i\),源点为 \(s\),汇点为 \(t\)。需要求出从 \(s\) 到 \(t\) 的最大流量,即在不超过每条边容量的条件下,从 \(s\) 出发能够输送到 \(t\) 的最大单位流量。这是网络最大流的经典问题,核心在于利用“增广路径”不断寻找可行的额外流量,直到无法再增加。
-
算法选择:
- Edmonds‐Karp (EK) 算法:基于 BFS 搜索从 \(s\) 到 \(t\) 的最短增广路(按边数最少),每次找到一条增广路后,更新残量网络,累加流量,直到不存在增广路。时间复杂度 \(O(V E^2)\),适合中等规模图。
- Dinic 算法:在 EK 基础上优化,通过 BFS 建立层次图,再用 DFS 一次性找出当前层次图中的所有增广路(多路增广),配合当前弧优化,效率更高,实际复杂度近似 \(O(V^2 E)\),通常作为首选模板算法。
- 代码分别实现了 EK 和 Dinic,推荐使用 Dinic。
-
关键步骤(以 Dinic 为例):
- 建图:对每条有向边 \((u, v, w)\),添加正向边容量为 \(w\),并同时添加反向边容量为 \(0\)(用于残量网络,支持流量回退)。
- BFS 分层:从源点 \(s\) 出发,沿剩余容量 \(>0\) 的边遍历,给每个节点标上层次 \(dep\),构建层次图(保证流只从低层流向高层)。
- DFS 多路增广:在层次图上从 \(s\) 开始,利用当前弧优化,递归地寻找所有能到达 \(t\) 的路径,每次找出当前路径上的最小剩余容量(瓶颈),更新正向边和反向边的容量,累加流量。
- 循环:重复 BFS 和 DFS,直到 BFS 无法到达 \(t\),此时得到最大流。
- 输出:总流量 \(flow\)。
-
时间/空间复杂度:
- EK 算法:\(O(V E^2)\)。
- Dinic 算法:\(O(V^2 E)\)(实际运行通常接近 \(O(E \sqrt{V})\))。
- 空间复杂度:\(O(V + E)\),存储邻接表和容量。
-
增广路与残量网络的核心思想:
- 增广路定理:最大流等于所有从源点到汇点的增广路径所能增加的流量之和,当不存在增广路时,当前流即为最大流。
- 残量网络:每条边保留“剩余容量”,同时添加反向边,表示可以撤销或调整之前分配的流量,使得算法能够修正非最优选择。
- BFS 找最短增广路(EK) 保证每次增广都是最短路,减少增广次数;Dinic 的分层 + DFS 多路增广 则进一步提高效率,一次 BFS 可找出多条增广路。
- 适用场景:网络流是图论中的核心算法,广泛应用于流量分配、任务调度、二分图匹配等实际问题。
【算法标签】
提高+ #网络流
【代码详解】
// EK算法
#include <bits/stdc++.h>
using namespace std;
#define int long long // 将 int 定义为 long long,防止流量溢出
const int N = 205, M = 5005 * 2; // N: 最大点数, M: 最大边数(双向边,乘2)
int n, m, s, t; // n: 点数, m: 边数, s: 源点, t: 汇点
int h[N], e[M], w[M], ne[M], idx; // 链式前向星:h表头, e终点, w容量, ne下一条, idx当前边数
int mf[N], pre[N]; // mf[u]: 从源点到u的当前路径上的最小剩余容量(瓶颈流量)
// pre[u]: 记录到达u的边的编号,用于回溯增广路径
// 链式前向星:添加有向边,同时添加反向边(残量网络)
void add(int a, int b, int c)
{
// 正向边:容量为c
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// BFS寻找从源点s到汇点t的增广路径( Edmonds-Karp算法,用BFS找最短增广路)
bool bfs()
{
// 初始化:mf数组为0表示未访问
memset(mf, 0, sizeof(mf));
queue<int> q;
q.push(s); // 从源点出发
mf[s] = 1e9; // 源点的瓶颈流量设为无穷大
while (!q.empty())
{
int u = q.front(); q.pop();
// 遍历u的所有出边
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i]; // 边的终点
// v未访问且该边还有剩余容量(w[i] > 0)
if (mf[v] == 0 && w[i])
{
// 更新v的瓶颈流量:从源点到u的瓶颈 与 当前边容量 的较小值
mf[v] = min(mf[u], w[i]);
pre[v] = i; // 记录到达v的边编号,用于后续回溯路径
q.push(v); // v入队继续BFS
if (v == t) // 找到汇点,增广路径存在
return true;
}
}
}
return false; // 无法到达汇点,不存在增广路径
}
// Edmonds-Karp算法:不断BFS找增广路,直到不存在增广路为止
int EK()
{
int flow = 0; // 总流量
while (bfs()) // 当还存在增广路径时
{
int v = t;
// 从汇点回溯到源点,更新残量网络
while (v != s)
{
int i = pre[v]; // 到达v的边编号
// 正向边容量减少(已使用mf[t]的流量)
w[i] -= mf[t];
// 反向边容量增加(i^1是i的反向边,因为边是成对添加的)
w[i ^ 1] += mf[t];
// 通过反向边的终点信息,回溯到上一个节点
v = e[i ^ 1];
}
flow += mf[t]; // 累加本次增广的流量
}
return flow; // 返回最大流
}
signed main() // 使用 signed 替代 int,因为 #define int long long
{
cin >> n >> m >> s >> t; // 读入点数、边数、源点、汇点
// 初始化链式前向星
memset(h, -1, sizeof(h));
// 读入m条有向边
while (m--)
{
int u, v, w;
cin >> u >> v >> w;
add(u, v, w); // 添加正向边,容量为w
add(v, u, 0); // 添加反向边,容量为0(初始时无反向流量)
}
cout << EK() << endl; // 输出最大流
return 0;
}
// Dicnic算法
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 205, M = 5005 * 2;
int n, m, s, t; // 点数,边数,源点,汇点
// 使用数组模拟邻接表
int h[N], e[M], w[M], ne[M], idx = 2; // idx从2开始,方便找反向边
int d[N], cur[N]; // d: 层次深度,cur: 当前弧优化数组
// 添加有向边和反向边
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// BFS构建层次图
bool bfs()
{
memset(d, 0, sizeof(d));
queue<int> q;
q.push(s);
d[s] = 1;
while (q.size())
{
int u = q.front();
q.pop();
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i];
if (d[v] == 0 && w[i]) // 如果v未访问且边有容量
{
d[v] = d[u] + 1;
q.push(v);
if (v == t)
{
return true; // 找到汇点
}
}
}
}
return false; // 无法到达汇点
}
// DFS进行多路增广
int dfs(int u, int mf)
{
if (u == t)
{
return mf; // 到达汇点
}
int sum = 0;
for (int i = cur[u]; i != -1; i = ne[i])
{
cur[u] = i; // 当前弧优化
int v = e[i];
if (d[v] == d[u] + 1 && w[i]) // 满足层次关系和容量限制
{
int f = dfs(v, min(mf, w[i]));
w[i] -= f; // 正向边减少容量
w[i ^ 1] += f; // 反向边增加容量
sum += f;
mf -= f;
if (mf == 0)
{
break; // 流量已用完
}
}
}
if (sum == 0)
{
d[u] = 0; // 残量网络中u不可达
}
return sum;
}
// Dinic算法主函数
int dinic()
{
int flow = 0;
while (bfs()) // 每次BFS构建新的层次图
{
memcpy(cur, h, sizeof(h)); // 重置当前弧
flow += dfs(s, 1e9); // 从源点开始增广
}
return flow;
}
signed main()
{
cin >> n >> m >> s >> t;
memset(h, -1, sizeof(h));
for (int i = 1; i <= m; i++)
{
int u, v, w;
cin >> u >> v >> w;
add(u, v, w), add(v, u, 0); // 添加正向边和反向边
}
cout << dinic() << endl;
return 0;
}
【运行结果】
4 5 4 3
4 2 30
4 3 20
2 3 20
2 1 30
1 3 30
50
浙公网安备 33010602011771号