图算法(下)——MST 与最大流 — 从零精通算法与数据结构——Google 面试系统备战 第14篇

第14章:图算法(下)——最小生成树与最大流

本章目标

读完本章你会:

  • 手写 Kruskal + Union-Find 和 Prim 算法
  • 理解 MST 的贪心正确性——割性质和环性质
  • 手写 Ford-Fulkerson 和 Edmonds-Karp 求最大流
  • 理解 max-flow/min-cut 对偶性的含义和应用
  • 用最大流解决二分图匹配

知识讲解

从一个生活例子开始

最小生成树 (MST): 你要用最少的光缆连接 n 个城市。每条可能的连接有不同成本(地理条件不同)。

  • Kruskal 的思路:从小到大试每根光缆,"这两个城市已经连上了吗?没连上就铺这根,连上了就跳过"。
  • Prim 的思路:从任意城市开始,每次从"当前已连通的集合"到"外部"的最便宜光缆。

最大流: 城市之间的管道有不同的容量限制。从水源城市到用水城市,每秒最多能输送多少水?

关键发现:最大流量 = 最小切割容量(瓶颈)。如果能找到一条从水源到目的地的"瓶颈路",那么它的容量就是全局上限。

工作原理

14.1 Kruskal 算法 + 并查集

KRUSKAL(G):
  将所有边按权重排序
  初始化并查集(每个节点自成一集合)

  for 每条边 (u, v) 按权重从小到大:
    if Find(u) ≠ Find(v):     // u 和 v 不在同一个连通分量
      把边加入 MST
      Union(u, v)              // 合并两个连通分量

并查集的核心操作:

  • Find(x):返回 x 所在集合的代表元,路径压缩使后续 Find 近乎 O(1)
  • Union(x, y):合并两个集合,按秩合并保证树不退化

复杂度: O(E log E) = O(E log V)(排序主导),加上几乎线性的并查集操作。

为什么成立——割性质: 对任何割(把图分成两组节点),跨越割的最小权重边一定属于某个 MST。Kruskal 每次选的最小边就是一个割的最小跨越边。

14.2 Prim 算法

PRIM(G, start):
  dist[v] = ∞(v 距离已建成 MST 的最短距离)
  dist[start] = 0
  优先队列 Q 包含所有节点,按 dist 排序

  while Q 不为空:
    u = Q.pop_min()         // 距离 MST 最近的节点
    for u 的每条邻边 (u, v, w):
      if v 在 Q 中 and w < dist[v]:
        dist[v] = w
        parent[v] = u
        Q.decrease_key(v, w)

复杂度: O(E log V)(二叉堆),或 O(E + V log V)(斐波那契堆——理论优势)。

Kruskal vs Prim 选择:

Kruskal Prim
适合 稀疏图 稠密图
数据结构 并查集 优先队列
实现难度 较简单 中等
边排序 必须 不需要
空间 O(E) 存排序的边 O(V) 优先队列

14.3 最大流:Ford-Fulkerson

核心概念:

  • 流网络: 有向图,每条边 (u,v) 有容量 c(u,v),流量 f(u,v) ≤ c(u,v)
  • 守恒: 除源点 s 和汇点 t 外,每个节点流入 = 流出
  • 剩余容量: c_f(u,v) = c(u,v) - f(u,v)(剩余可用容量)
  • 增广路: 从 s 到 t 的路径,路径上所有边的剩余容量 > 0
FORD-FULKERSON(G, s, t):
  for 每条边: f(u,v) = 0
  while 存在从 s 到 t 的增广路 p:
    瓶颈 = min{c_f(u,v) : (u,v) 在 p 上}
    for 每条边 (u,v) 在 p 上:
      f(u,v) += 瓶颈
      f(v,u) -= 瓶颈      // 反向边——允许"撤销"流量

Edmonds-Karp 改进: 每次选最短(边数最少)的增广路——用 BFS 找。复杂度 O(V·E²)。

直觉——为什么反向边? 想象两条路径争抢同一条管道的容量。一条路径先占满了管道,另一条路径可以通过反向边"推回"一部分流量,让两条路径都满意。反向边是实现"流量重分配"的机制。

14.4 Max-Flow/Min-Cut 对偶性

s-t 割: 将节点分成 S(含 s)和 T(含 t)的两组。割容量 = 所有从 S 跨到 T 的边的容量之和。

定理: 最大流的值 = 最小 s-t 割的容量。

应用——二分图最大匹配: 左组 L 到右组 R,每条边表示"可以配对"。建超级源 → L 所有节点(容量 1),R 所有节点 → 超级汇(容量 1)。最大流 = 最大匹配数。


代码实战

include/algo/graph_advanced.h

#ifndef ALGO_GRAPH_ADVANCED_H_
#define ALGO_GRAPH_ADVANCED_H_

#include <cstdint>
#include <vector>

namespace algo {

// ========== Union-Find(并查集)==========
class UnionFind {
 public:
  explicit UnionFind(int n);
  int Find(int x);
  void Union(int x, int y);
  bool Connected(int x, int y);
  int Count() const { return count_; }

 private:
  std::vector<int> parent_;
  std::vector<int> rank_;
  int count_;
};

// ========== 最小生成树 ==========
struct Edge {
  int u, v, weight;
  bool operator<(const Edge& other) const { return weight < other.weight; }
};

// Kruskal:返回 MST 的边集合,若图不连通则返回空
std::vector<Edge> KruskalMST(int vertices, std::vector<Edge> edges);

// Prim:返回 MST 的总权重(用优先队列实现)
int64_t PrimMST(const std::vector<std::vector<std::pair<int, int>>>& adj);

// ========== 最大流 (Edmonds-Karp) ==========
class MaxFlow {
 public:
  MaxFlow(int vertices);
  void AddEdge(int from, int to, int64_t capacity);
  int64_t Compute(int source, int sink);

 private:
  int V_;
  struct FlowEdge {
    int to, rev;        // 目标节点和反向边索引
    int64_t cap, flow;
  };
  std::vector<std::vector<FlowEdge>> adj_;
};

}  // namespace algo

#endif  // ALGO_GRAPH_ADVANCED_H_

include/algo/graph_advanced_impl.h(Kruskal + MaxFlow)

namespace algo {

// ========== UnionFind ==========
inline UnionFind::UnionFind(int n) : parent_(n), rank_(n, 0), count_(n) {
  for (int i = 0; i < n; ++i) parent_[i] = i;
}

inline int UnionFind::Find(int x) {
  if (parent_[x] != x) parent_[x] = Find(parent_[x]);  // 路径压缩
  return parent_[x];
}

inline void UnionFind::Union(int x, int y) {
  int rx = Find(x), ry = Find(y);
  if (rx == ry) return;
  // 按秩合并
  if (rank_[rx] < rank_[ry]) std::swap(rx, ry);
  parent_[ry] = rx;
  if (rank_[rx] == rank_[ry]) ++rank_[rx];
  --count_;
}

inline bool UnionFind::Connected(int x, int y) { return Find(x) == Find(y); }

// ========== Kruskal ==========
inline std::vector<Edge> KruskalMST(int vertices, std::vector<Edge> edges) {
  std::sort(edges.begin(), edges.end());
  UnionFind uf(vertices);
  std::vector<Edge> mst;

  for (auto& e : edges) {
    if (!uf.Connected(e.u, e.v)) {
      uf.Union(e.u, e.v);
      mst.push_back(e);
      if (static_cast<int>(mst.size()) == vertices - 1) break;
    }
  }
  if (static_cast<int>(mst.size()) != vertices - 1) return {};  // 不连通
  return mst;
}

// ========== Edmonds-Karp ==========
inline MaxFlow::MaxFlow(int vertices) : V_(vertices), adj_(vertices) {}

inline void MaxFlow::AddEdge(int from, int to, int64_t capacity) {
  // 正边:from → to,容量 capacity
  adj_[from].push_back({to, static_cast<int>(adj_[to].size()), capacity, 0});
  // 反边:to → from,容量 0(初始),rev 指向正边在 adj_[from] 中的位置
  // rev = adj_[from].size() - 1 因为正边刚刚被 push_back 到 adj_[from] 的末尾
  adj_[to].push_back({from, static_cast<int>(adj_[from].size()) - 1, 0, 0});
}

inline int64_t MaxFlow::Compute(int source, int sink) {
  int64_t total_flow = 0;
  while (true) {
    // BFS 找最短增广路
    std::vector<int> parent(V_, -1);
    std::vector<int> edge_idx(V_, -1);
    std::queue<int> q;
    q.push(source);

    while (!q.empty() && parent[sink] == -1) {
      int u = q.front(); q.pop();
      for (int i = 0; i < static_cast<int>(adj_[u].size()); ++i) {
        auto& e = adj_[u][i];
        if (parent[e.to] == -1 && e.cap > e.flow) {
          parent[e.to] = u;
          edge_idx[e.to] = i;
          q.push(e.to);
        }
      }
    }

    if (parent[sink] == -1) break;  // 没有增广路了

    // 找瓶颈
    int64_t bottleneck = std::numeric_limits<int64_t>::max();
    for (int v = sink; v != source; v = parent[v]) {
      int u = parent[v];
      auto& e = adj_[u][edge_idx[v]];
      bottleneck = std::min(bottleneck, e.cap - e.flow);
    }

    // 增广
    for (int v = sink; v != source; v = parent[v]) {
      int u = parent[v];
      auto& e = adj_[u][edge_idx[v]];
      e.flow += bottleneck;
      adj_[e.to][e.rev].flow -= bottleneck;
    }

    total_flow += bottleneck;
  }
  return total_flow;
}

}  // namespace algo

本章小结

  1. Kruskal 通过排序 + 并查集贪心构建 MST,O(E log E);Prim 通过优先队列,O(E log V)
  2. MST 贪心正确性由割性质环性质保证
  3. Ford-Fulkerson 通过反复找增广路求最大流;Edmonds-Karp 用 BFS 保证最坏 O(V·E²)
  4. 最大流的值 = 最小 s-t 割的容量——这一定理连接了流和割两个概念
  5. 并查集的两大优化:路径压缩(Find 近乎 O(1))和按秩合并(保持树平衡)

关键术语

术语 释义
MST 最小生成树——连接所有节点的最小总成本无环子图
并查集 维护不相交集合的数据结构,近乎 O(1) 的 Find 和 Union
增广路 剩余网络中从源到汇的可达路径——增大流量
最小割 将图分成 S(含源)和 T(含汇)的最小总容量划分
反向边 允许"撤销"已分配流量——Ford-Fulkerson 正确性的核心
posted @ 2026-06-22 01:09  Yobeeo  阅读(14)  评论(0)    收藏  举报