学习笔记:网络流

本文主要由本人写作,但经过 AI 润色使其结构更加清晰、行文更加流畅,文风可能与 AI 类似,请谅解。

引入

想象一个城市的输水系统。自来水厂(水源)通过管道网络向千家万户供水,用户使用后的废水汇入处理厂,最终排入河流。

若将水源、用户、处理厂视为结点,管道视为有向边,管道的水流上限视为容量,这就构成了一个网络。研究水流如何在该网络中分配与传输,就是网络流问题。

基础概念

一个网络是一个有向图 $ G = (V, E)$ ,其中:

  • 有两个特殊结点:源点 $ s$ (如水源)和汇点 $ t$ (如废水处理厂)。
  • 每条有向边 $ (u, v)$ 有一个容量 $ c(u, v) \geq 0$ ,表示该边能承载的最大流量。

该网络上的一个 $ f$ 是为每条边分配的一个实数 $ f(u, v)$ ,满足:

  1. 容量限制:$ 0 \leq f(u, v) \leq c(u, v)$ 。
  2. 流量守恒:除源点 $ s$ 和汇点 $ t$ 外,每个结点 $ u$ 满足:

    \[\sum_{v \in V} f(u, v) = \sum_{v \in V} f(v, u) \]

    即流入等于流出。

定义结点 $ u$ 的净流出量为流出总量减去流入总量。根据守恒性,对于中间结点净流出为 0。源点 $ s$ 的净流出称为该流的流量,记作 $ |f|$ 。

常见问题

最大流问题

给定网络,寻找一个流 $ f$ ,使其流量 $ |f| $ 最大。

Dinic 算法

Dinic 算法是一种高效的最大流算法,时间复杂度为 $ O(V^2 E)$ (最坏情况),实际运行通常更快。

流程图:

算法流程如下:

  1. 构建残量网络

    • 对于原图每条边 $ (u, v)$ ,添加一条反向边 $ (v, u)$ ,初始容量为 0。
    • 反向边的作用是提供“反悔”机制:当后续发现更优的流量分配时,可以通过反向边退回部分流量,从而重新调整。
  2. BFS 构建分层图

    • 从源点 $ s$ 开始 BFS,标记每个结点的深度(距离 $ s$ 的最短边数)。
    • 只有从深度 $ d$ 的结点指向深度 $ d+1$ 的结点的边才被保留,形成分层图
  3. DFS 寻找增广路

    • 在分层图上进行 DFS,寻找一条从 $ s$ 到 $ t$ 的路径。
    • 沿路径增加流量,增加量为路径上最小的残余容量。
    • 更新残量网络:正向边容量减少,反向边容量增加。
  4. 重复执行

    • 当一次 DFS 无法再找到增广路时,返回步骤 2,重新 BFS 构建新的分层图。
    • 直到 BFS 无法到达 $ t$ 为止,算法结束。此时总流量即为最大流。

常用优化:

  • 当前弧优化:在 DFS 中,每条边一旦被访问过,在当前分层图中就不再重复访问。
  • 多路增广:一次 DFS 可找到并增广多条路径。
  • 炸点优化:若从某点出发无法推送任何流量到 $ t$ ,则将其深度标记为无效,避免后续无效访问。

洛谷 P3376 【模板】网络最大流 参考代码
#include <bits/stdc++.h>
#define int long long
const int MIN = 0xc0c0c0c0c0c0c0c0, MAX = 0x3f3f3f3f3f3f3f3f;
const int N = 210, M = 5010;
struct Edge {
  int to, cap, next;
} g[M * 2];
int head[N], idx, curr[N], n, m, s, t, dep[N];

void addEdge(int u, int v, int w) {
  g[idx] = {v, w, head[u]};
  head[u] = idx++;
}

bool bfs(int s, int t) {
  memset(dep, -1, sizeof(int) * (n + 1));
  dep[s] = 0;
  std::queue<int> q;
  q.push(s);
  while (!q.empty()) {
    int u = q.front();
    q.pop();
    for (int i = head[u]; i != -1; i = g[i].next) {
      int v = g[i].to;
      if (dep[v] == -1 && g[i].cap > 0) {
        dep[v] = dep[u] + 1;
        if (v == t) return true;
        q.push(v);
      }
    }
  }
  return false;
}

int dfs(int u, int t, int rest) { // 当前弧优化
  if (u == t) return rest;
  int flow = 0;
  for (int &i = curr[u]; i != -1; i = g[i].next) {
    int v = g[i].to;
    if (dep[v] == dep[u] + 1 && g[i].cap > 0) {
      int d = dfs(v, t, std::min(rest, g[i].cap));
      if (d > 0) {
        g[i].cap -= d; // 更新正向边
        g[i^1].cap += d; // 更新反向边
        flow += d;
        rest -= d;
        if (rest == 0) break; // 流量已用完,及时退出
      }
    }    
  }
  if (flow == 0) dep[u] = -1; // 炸点
  return flow;
}

int dinic(int s, int t) {
  int res = 0;
  while (bfs(s, t)) {
    for (int i = 1; i <= n; i++) curr[i] = head[i]; // 为本次 BFS 分层后的所有 DFS 设置当前弧
    int f = 0;
    while ((f = dfs(s, t, MAX)) > 0) res += f;
  }
  return res;
}

signed main() {
  std::ios::sync_with_stdio(false); std::cin.tie(0);
  std::cin >> n >> m >> s >> t;
  memset(head, -1, sizeof(int) * (n + 1));
  for (int i = 1; i <= m; i++) {
    int u, v, w;
    std::cin >> u >> v >> w;
    addEdge(u, v, w);
    addEdge(v, u, 0); // 构建反向边
  }
  std::cout << dinic(s, t) << '\n';
  return 0;
}

最大流最小割定理

:将结点集 $ V$ 划分为两部分 $ S$ 和 $ T = V \setminus S$ ,且 $ s \in S, t \in T$ 。割 $ (S, T)$ 的容量定义为所有从 $ S$ 指向 $ T$ 的边的容量之和:

\[c(S, T) = \sum_{u \in S, v \in T} c(u, v) \]

最小割是容量最小的割。

最大流最小割定理:在一个网络中,最大流的流量等于最小割的容量。该定理揭示了流与割的对偶关系,证明可参考 https://oi-wiki.org/graph/flow/max-flow/#最大流最小割定理。

最小费用最大流问题

在最大流问题基础上,每条边还有一个单位流量费用 $ w(u, v)$ 。目标是找到最大流中总费用最小者。

算法思路(SPFA 费用流):

  1. 建图:同时建立容量为 0、费用为反向数($ -w$ )的反向边,以实现“退货退款”。
  2. 寻找最短路:在残量网络中,以单位费用为边权,用 SPFA 寻找从 $ s$ 到 $ t$ 的最短费用增广路。因存在负权边(反向边),通常使用 SPFA 或经过势函数处理的 Dijkstra。
  3. 沿路增广
    • 增广量 $ \Delta = \min {\text{路径上各边残余容量} }$ 。
    • 更新流量和费用。
  4. 重复:直到无法找到增广路。
洛谷 P3381 【模板】最小费用最大流 参考代码
#include <bits/stdc++.h>
#define int long long
const int MIN = 0xc0c0c0c0c0c0c0c0, MAX = 0x3f3f3f3f3f3f3f3f;
const int N = 5e3+10, M = 5e4+10;
struct Edge {
  int to, next, cap, cost;
} g[M * 2];
int head[N], idx, pre[N], dis[N], incf[N]; bool vis[N];
int n, m, s, t;
int maxFlow, minCost;

void addEdge(int u, int v, int cap, int cost) {
  g[idx] = {v, head[u], cap, cost};
  head[u] = idx++;
}

bool spfa() {
  std::queue<int> q;
  memset(dis, 0x3f, sizeof(int) * (n + 1));
  memset(vis, 0, sizeof(int) * (n + 1));
  q.push(s);
  dis[s] = 0; vis[s] = true;
  incf[s] = MAX;
  while (!q.empty()) {
    int u = q.front();
    q.pop();
    vis[u] = false;
    for (int i = head[u]; i != -1; i = g[i].next) {
      int v = g[i].to, len = g[i].cost;
      if (dis[u] + len < dis[v] && g[i].cap > 0) {
        dis[v] = dis[u] + len;
        pre[v] = i;
        incf[v] = std::min(incf[u], g[i].cap);
        if (!vis[v]) {
          q.push(v);
          vis[v] = true;
        }
      }
    }
  }
  return dis[t] != MAX;
}

void MCMF() {
  maxFlow = 0, minCost = 0;
  while (spfa()) {
    if (incf[t] == 0) break;
    int flow = incf[t];
    maxFlow += flow;
    minCost += flow * dis[t];
    for (int u = t; u != s; u = g[pre[u] ^ 1].to) {
      g[pre[u]].cap -= flow;
      g[pre[u] ^ 1].cap += flow;
    }
  }
}

signed main() {
  std::ios::sync_with_stdio(false); std::cin.tie(0);
  std::cin >> n >> m >> s >> t;
  memset(head, -1, sizeof(int) * (n + 1));
  for (int i = 1; i <= m; i++) {
    int u, v, cost, cap;
    std::cin >> u >> v >> cap >> cost;
    addEdge(u, v, cap, cost);
    addEdge(v, u, 0, -cost);
  }
  MCMF();
  std::cout << maxFlow << ' ' << minCost << '\n';
  return 0;
}

建模实战

掌握算法模板后,真正的挑战在于将实际问题转化为网络流模型。下面通过例题体会建模思路。

洛谷 P3254 圆桌问题

题意:有 $ m$ 个单位,每个单位有 $ r_i$ 名代表;有 $ n$ 张圆桌,每张可容纳 $ c_j$ 人。要求同一单位的代表不在同一桌就餐。问是否存在可行方案,并输出方案。

建模

  1. 建立超级源点 $ S$ 和超级汇点 $ T$ 。
  2. $ S$ 向每个单位 $ i$ 连边,容量为 $ r_i$ (该单位代表数)。
  3. 每个圆桌 $ j$ 向 $ T$ 连边,容量为 $ c_j$ (该桌容纳量)。
  4. 每个单位 $ i$ 向每张圆桌 $ j$ 连边,容量为 \(1\)(保证每桌至多一名该单位代表)。

若最大流等于总人数,则有解。输出方案时,检查单位 $ i$ 到圆桌 $ j$ 的边流量是否为 0(即被使用)。

参考代码
#include <bits/stdc++.h>
#define int long long
const int _INF = 0xc0c0c0c0c0c0c0c0, INF = 0x3f3f3f3f3f3f3f3f;
const int N = 150 + 270 + 10, M = 2 * (150 * 270 + 10);

struct Graph {
  struct Edge {
    int to, cap, next;
  } g[M];
  int head[N], idx, curr[N], n, m, s, t, dep[N];

  Graph() {
    memset(head, -1, sizeof(head));
  }

  void addEdge(int u, int v, int w) {
    g[idx] = {v, w, head[u]};
    head[u] = idx++;
  }

  bool bfs(int s, int t) {
    memset(dep, -1, sizeof(int) * (n + 1));
    dep[s] = 0;
    std::queue<int> q;
    q.push(s);
    while (!q.empty()) {
      int u = q.front();
      q.pop();
      for (int i = head[u]; i != -1; i = g[i].next) {
        int v = g[i].to;
        if (dep[v] == -1 && g[i].cap > 0) {
          dep[v] = dep[u] + 1;
          if (v == t) return true;
          q.push(v);
        }
      }      
    }
    return false;
  }

  int dfs(int u, int t, int rest) {
    if (u == t) return rest;
    int flow = 0;
    for (int &i = curr[u]; i != -1; i = g[i].next) {
      int v = g[i].to;
      if (dep[v] == dep[u] + 1 && g[i].cap > 0) {
        int d = dfs(v, t, std::min(rest, g[i].cap));
        if (d > 0) {
          g[i].cap -= d;
          g[i^1].cap += d;
          flow += d;
          rest -= d;
          if (rest == 0) break;
        }
      }
    }
    if (flow == 0) dep[u] = -1;
    return flow;
  }

  int dinic(int s, int t) {
    int res = 0;
    while (bfs(s, t)) {
      for (int i = 0; i <= n; i++) curr[i] = head[i];
      int f = 0;
      while ((f = dfs(s, t, INF)) > 0) res += f;
    }
    return res;
  }
} g;



signed main() {
  // freopen(".in", "r", stdin);
  // freopen(".out", "w", stdout);
  std::ios::sync_with_stdio(false); std::cin.tie(0);
  // 1 为超级源点,m + n + 2 为超级汇点
  int m, n;
  std::cin >> m >> n;
  int sumR = 0;
  for (int i = 2; i <= m + 1; i++) {
    int r;
    std::cin >> r;
    sumR += r;
    g.addEdge(1, i, r);
    g.addEdge(i, 1, 0);
  }
  for (int i = m + 2; i <= m + n + 1; i++) {
    int c;
    std::cin >> c;
    g.addEdge(i, m + n + 2, c);
    g.addEdge(m + n + 2, i, 0);
  }
  for (int i = 2; i <= m + 1; i++) {
    for (int j = m + 2; j <= m + n + 1; j++) {
      g.addEdge(i, j, 1);
      g.addEdge(j, i, 0);
    }
  }
  g.n = n + m + 2; g.m = n + m + n * m;
  int res = g.dinic(1, m + n + 2);
  if (res == sumR) {
    std::cout << "1\n";
    for (int i = 2; i <= m + 1; i++) {
      std::vector<int> ans;
      for (int j = g.head[i]; j != -1; j = g.g[j].next) {
        if (g.g[j].to >= m + 2 && g.g[j].to <= m + n + 1 && g.g[j].cap == 0) {
          ans.push_back(g.g[j].to - m - 1);
        }
      }
      for (auto const &num : ans) {
        std::cout << num << ' ';
      }
      std::cout << '\n';
    }
  } else {
    std::cout << "0\n";
  }
  return 0;
}

洛谷 P2762 太空飞行计划问题

题意:有 $ m$ 个实验,每个实验有收益 $ p_i$ ;有 $ n$ 种仪器,每个仪器有花费 $ c_j$ 。进行实验需要购买所需仪器。求最大净收益(总收益−仪器花费)及方案。

建模(最大权闭合子图):

  1. 建立超级源点 $ S$ 和超级汇点 $ T$ 。
  2. $ S$ 向每个实验 $ i$ 连边,容量为 $ p_i$ 。
  3. 每个仪器 $ j$ 向 $ T$ 连边,容量为 $ c_j$ 。
  4. 每个实验向其所需仪器连边,容量为 $ +\infty$ (确保依赖关系)。

原理:该模型的最小割对应最优选择。

  • 割边 $ S \to$ 实验 表示放弃该实验(损失收益)。
  • 割边 仪器 $ \to T$ 表示购买该仪器(支付花费)。
  • 无限边迫使:若选择实验,则必须购买其所需仪器(否则割容量无穷大)。

计算最大流(最小割),净收益 = 总收益 − 最大流。方案为最后一次 BFS 中可达的结点(实验和仪器)。

参考代码
// 超级源点 1,实验 2 ~ m+1,仪器 m+2 ~ m+n+1,超级汇点 m+n+2

#include <bits/stdc++.h>
#define int long long
const int _INF = 0xc0c0c0c0c0c0c0c0, INF = 0x3f3f3f3f3f3f3f3f;
const int N = 110, M = 2 * (50 * 50 + 100) + 10;
struct Graph {
  int head[N], curr[N], n, idx, dep[N];

  Graph() {
    memset(head, -1, sizeof(head));
  }

  struct Edge {
    int to, cap, next;
  } g[M];

  void addEdge(int u, int v, int w) {
    g[idx] = {v, w, head[u]};
    head[u] = idx++;
    g[idx] = {u, 0, head[v]};
    head[v] = idx++;
  }

  bool bfs(int s, int t) {
    memset(dep, -1, sizeof(int) * (n + 1));
    dep[s] = 0;
    std::queue<int> q;
    q.push(s);
    while (!q.empty()) {
      int u = q.front();
      q.pop();
      for (int i = head[u]; i != -1; i = g[i].next) {
        int v = g[i].to;
        if (dep[v] == -1 && g[i].cap > 0) {
          dep[v] = dep[u] + 1;
          if (v == t) return true;
          q.push(v);
        }
      }
    }
    return false;
  }

  int dfs(int u, int t, int rest) {
    if (u == t) return rest;
    int flow = 0;
    for (int &i = curr[u]; i != -1; i = g[i].next) {
      int v = g[i].to;
      if (dep[v] == dep[u] + 1 && g[i].cap > 0) {
        int d = dfs(v, t, std::min(rest, g[i].cap));
        if (d > 0) {
          g[i].cap -= d;
          g[i^1].cap += d;
          flow += d;
          rest -= d;
          if (rest == 0) break;
        }
      }
    }
    if (flow == 0) dep[u] = -1;
    return flow;
  }
  
  int dinic(int s, int t) {
    int res = 0;
    while (bfs(s, t)) {
      memcpy(curr, head, sizeof(int) * (n + 1));
      int f = 0;
      while ((f = dfs(s, t, INF)) > 0) res += f;
    }
    return res;
  }
} g;

signed main() {
  // freopen(".in", "r", stdin);
  // freopen(".out", "w", stdout);
  std::ios::sync_with_stdio(false); std::cin.tie(0);
  int m, n;
  std::cin >> m >> n;
  g.n = m + n + 2;
  int tot = 0;
  for (int i = 1; i <= m; i++) {
    int val;
    std::cin >> val;
    tot += val;
    g.addEdge(1, i+1, val);
    char tools[10010];
    memset(tools, 0, sizeof(tools));
    std::cin.getline(tools, 10000);
    int ulen = 0, tool;
    while (sscanf(tools + ulen, "%d", &tool) == 1) {
      g.addEdge(i+1, m+1+tool, INF);
      if (tool == 0) {
        ulen++;
      } else {
        while (tool) {
          tool /= 10;
          ulen++;
        }
      }
      ulen++;
    }
  }
  for (int i = 1; i <= n; i++) {
    int cost;
    std::cin >> cost;
    g.addEdge(m+i+1, m+n+2, cost);
  }
  int res = g.dinic(1, m+n+2);
  for (int i = 1; i <= m; i++) {
    if (g.dep[1+i] != -1) {
      std::cout << i << ' ';
    }
  }
  std::cout << '\n';
  for (int i = 1; i <= n; i++) {
    if (g.dep[m+1+i] != -1) {
      std::cout << i << ' ';
    }
  }
  std::cout << '\n';
  std::cout << tot - res << '\n';
  return 0;
}

建模常见特征

  1. 资源分配/匹配:如单位与圆桌、任务与机器。
  2. 依赖/选择:如实验依赖仪器,用无限容量边表示强制关联。
  3. 容量限制:边的容量常表示数量上限、花费等。
  4. 最大化/最小化:通过最大流、最小割或费用流实现。

总结

网络流是图论中强大的建模工具,其核心在于将实际问题抽象为流量分配与路径寻找。熟练掌握最大流、最小割及费用流算法后,重点应培养识别问题特征并巧妙建图的能力。

推荐练习

posted @ 2025-12-21 21:24  JZ8  阅读(0)  评论(0)    收藏  举报