题解:洛谷 P3376 网络最大流

【题目来源】

洛谷: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

【核心思想】

  1. 问题分析:给定一个 \(n\) 个点、\(m\) 条边的有向带权图,每条边有容量上限 \(w_i\),源点为 \(s\),汇点为 \(t\)。需要求出从 \(s\)\(t\) 的最大流量,即在不超过每条边容量的条件下,从 \(s\) 出发能够输送到 \(t\) 的最大单位流量。这是网络最大流的经典问题,核心在于利用“增广路径”不断寻找可行的额外流量,直到无法再增加。

  2. 算法选择

    • Edmonds‐Karp (EK) 算法:基于 BFS 搜索从 \(s\)\(t\) 的最短增广路(按边数最少),每次找到一条增广路后,更新残量网络,累加流量,直到不存在增广路。时间复杂度 \(O(V E^2)\),适合中等规模图。
    • Dinic 算法:在 EK 基础上优化,通过 BFS 建立层次图,再用 DFS 一次性找出当前层次图中的所有增广路(多路增广),配合当前弧优化,效率更高,实际复杂度近似 \(O(V^2 E)\),通常作为首选模板算法。
    • 代码分别实现了 EK 和 Dinic,推荐使用 Dinic。
  3. 关键步骤(以 Dinic 为例):

    • 建图:对每条有向边 \((u, v, w)\),添加正向边容量为 \(w\),并同时添加反向边容量为 \(0\)(用于残量网络,支持流量回退)。
    • BFS 分层:从源点 \(s\) 出发,沿剩余容量 \(>0\) 的边遍历,给每个节点标上层次 \(dep\),构建层次图(保证流只从低层流向高层)。
    • DFS 多路增广:在层次图上从 \(s\) 开始,利用当前弧优化,递归地寻找所有能到达 \(t\) 的路径,每次找出当前路径上的最小剩余容量(瓶颈),更新正向边和反向边的容量,累加流量。
    • 循环:重复 BFS 和 DFS,直到 BFS 无法到达 \(t\),此时得到最大流。
    • 输出:总流量 \(flow\)
  4. 时间/空间复杂度

    • EK 算法:\(O(V E^2)\)
    • Dinic 算法:\(O(V^2 E)\)(实际运行通常接近 \(O(E \sqrt{V})\))。
    • 空间复杂度:\(O(V + E)\),存储邻接表和容量。
  5. 增广路与残量网络的核心思想

    • 增广路定理:最大流等于所有从源点到汇点的增广路径所能增加的流量之和,当不存在增广路时,当前流即为最大流。
    • 残量网络:每条边保留“剩余容量”,同时添加反向边,表示可以撤销或调整之前分配的流量,使得算法能够修正非最优选择。
    • 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
posted @ 2026-09-01 22:40  团爸讲算法  阅读(19)  评论(0)    收藏  举报