当前位置: 首页 > news >正文

图算法(下)——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 不在同一个连通分量把边加入 MSTUnion(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] = wparent[v] = uQ.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) = 0while 存在从 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,容量 capacityadj_[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 正确性的核心
http://www.jsqmd.com/news/1058297/

相关文章:

  • 2026专业的张家港办理公司变更业务企业推荐哪家强 - 品牌排行榜
  • Photon光影包:3步打造Minecraft电影级视觉体验的终极指南
  • 对称群表示理论及其在物理计算中的应用
  • 构建可信赖弹性CPS:可解释AI与运行时验证的工程实践
  • 2026秦皇岛防水补漏避坑指南:卫生间/厨房/阳台/屋顶/地下室漏水检测维修全攻略,正规施工+透明报价+口碑榜靠谱服务商推荐 - 安佳防水
  • 从混乱到高效:项目管理经典书籍推荐
  • 卡梅德生物科普IL5R(白细胞介素5受体)
  • 如何用Play Integrity API Checker快速检测Android设备安全
  • 咏巷炸鸡_小成本创业加盟_低投入品牌推荐 - 3158GEO
  • 计算几何 — 从零精通算法与数据结构——Google 面试系统备战 第15篇
  • 5大音乐平台加密文件破解:浏览器内本地解密工具深度解析
  • 2026年近期江西知名的业务外包服务商怎么联系?众诚人力资源专业解析 - 品牌鉴赏官2026
  • SQL注入深度解析:从攻击分类到实战防御策略
  • GEO代运营收费标准 四种模式拆解对比哪家更划算 - 3158GEO
  • 2026年当下,如何甄别真正具备未来竞争力的无人驾驶洗地机供应厂家? - 品牌鉴赏官2026
  • 2026降AIGC工具亲测:10款网站对比,学术合规技巧盘点
  • 3分钟解锁B站缓存宝藏:你的m4s视频转换秘籍
  • 嵌入式系统互连技术选型:以太网与RapidIO的深度对比与实战指南
  • “恒宇杯”第六届辽宁省大学生金相技能大赛暨“徕卡杯”第十五届全国大学生金相技能大赛复赛(辽宁赛区) - 品牌发掘
  • 武汉市江汉区房屋修缮|维小达|窗户维修、吊顶维修、壁纸壁布、墙面维修、石材修复、瓷砖美缝、瓷砖维修全屋一站式旧房翻新破损修护服务 - 维小达科技
  • 2026年“恒宇杯”第十五届全国大学生金相技能大赛广西区选拔赛暨广西分区赛 - 品牌发掘
  • 2026石家庄防水补漏避坑指南:卫生间/厨房/阳台/屋顶/地下室漏水检测维修全攻略,正规施工+透明报价+口碑榜靠谱服务商推荐 - 安佳防水
  • 3分钟搭建同花顺自动化交易系统:Python量化交易终极指南
  • 2026年近期,好的1-氯丙烷公司推荐:骋源高新材料实力解析 - 品牌鉴赏官2026
  • Windows系统文件ieframe.dll丢失找不到问题解决
  • FanControl终极配置指南:Windows风扇控制软件的完整解决方案
  • Switch破解终极指南:5分钟快速部署Atmosphere大气层系统与性能优化方案
  • 2026玉林漏水检测维修本地口碑防水商家榜单:厨卫/阳台/屋面/地下室渗漏水维修,持证施工+明码实价,防水补漏公司TOP5推荐 - 即刻修防水
  • 大模型微调中的幻觉问题:自蒸馏与参数冻结的解决方案
  • Codex 实战 Skills:自动采集一天之内的 Git 提交,一键排版成精美工作日报并发送邮件