
















霍尔定理和最大流,本质是解决「二分图匹配」的两种思路——前者是"判断能不能匹配"的"理论条件",后者是"实际算出怎么匹配、能匹配多少"的"算法工具"。读者无需记复杂公式,跟着例子走,就能看懂两者的关联和区别。
二分图:简单说就是"两个阵营"的图,比如左边是「男生」(集合A),右边是「女生」(集合B),边代表"男生和女生能配对",没有跨阵营内部的边(比如男生之间不连边)。
完美匹配:左边每个男生都能找到一个唯一的女生配对,右边每个女生也只配一个男生(相当于"全员配对成功")。
霍尔条件:对于左边集合A的任意子集S,S中节点的邻居集合N(S)的大小必须 \(\geq\) S的大小。即 \(|N(S)| \geq |S|\) 对所有 \(S \subseteq A\) 成立。
最大流:把二分图变成"水流网络",源点(水龙头)往左边男生送水,男生往能配对的女生送水,女生往汇点(水池)送水,"最大流"就是最多能送多少水(对应最多能配对多少对)。
设定:左边男生集合A = {甲, 乙, 丙}(3人),右边女生集合B = {A, B, C}(3人)
边(能配对的关系):甲→A、甲→B;乙→B、乙→C;丙→C
目标:判断能不能实现「完美匹配」,同时搞懂霍尔定理和最大流是怎么解决这个问题的。
要实现完美匹配,左边任意一个子集S,能配对的女生数量(N(S)),必须 \(\geq\) 这个子集的男生数量。
简单说:不管你从左边挑几个男生,他们能选的女生,都不能比挑的男生少——不然就有男生配不上。
我们要检查「左边所有可能的子集」,看是否都满足"女生数 \(\geq\) 男生数"(霍尔条件)。
子集规模1(挑1个男生):
子集规模2(挑2个男生):
子集规模3(挑所有男生,即S=A):
所有子集都满足霍尔条件,所以「存在完美匹配」。比如:甲→A、乙→B、丙→C(或其他组合)。
把二分图改成"水流系统",用"水流能不能流满",模拟"能不能完美匹配":
我们用"简单水流模拟",不用复杂算法,就能看出最大流是多少。
构建水流网络(对应例子):
第一次流水(找一条路径):
第二次流水(再找一条路径):
第三次流水(再找一条路径):
检查是否还能流水:
最大流=3,和左边男生数量(3)相等,所以「存在完美匹配」,而且流水路径就是具体的配对方式(甲→A、乙→B、丙→C)。
| 对比维度 | 霍尔定理 | 最大流(二分图匹配) |
|---|---|---|
| 核心作用 | 判断"能不能完美匹配"(理论条件) | 算出"最多能匹配多少对"+"具体怎么匹配"(实际算法) |
| 核心思路 | 检查左边所有子集,是否满足 \(|N(S)| \geq |S|\) | 构建流网络,模拟水流,求最大流水量 |
| 能否给出匹配方案 | 不能,只给"能/不能"的结论 | 能,流水路径就是匹配方案 |
| 复杂度 | \(O(2^n)\)(指数级),小数据可用 | \(O(V^2E)\)(多项式级),大数据首选 |
| 两者关联 | 霍尔条件满足 ↔ 最大流=左边节点数(完美匹配) | 最大流算法,本质是"间接验证霍尔条件",不用枚举子集 |
| 使用建议 | 理解原理、判断小数据,不用实际计算 | 实际做题、解决问题,直接用(比如编程、刷题) |
| 文档用语 | 标准术语 | 英文 | 说明 |
|---|---|---|---|
| 小团体 | 子集 | Subset | 图论中的标准术语 |
| 能配对的女生 | 邻居集合 | Neighborhood | N(S)表示子集S的所有邻居 |
| 水流网络 | 流网络 | Flow Network | 最大流问题的标准模型 |
| 水龙头/水池 | 源点/汇点 | Source/Sink | 流网络中的特殊节点 |
定理(Hall, 1935):设G=(X,Y,E)是一个二分图,其中 \(|X|=|Y|\)。则G存在完美匹配当且仅当对于X的任意子集S,都有 \(|N(S)| \geq |S|\)。
等价表述:最大匹配的大小 = \(|X| - \max_{S \subseteq X} (|S| - |N(S)|)\)
推论:若对于所有 \(S \subseteq X\) 都有 \(|N(S)| \geq |S|+k\),则存在大小为 \(|X|+k\) 的匹配(允许部分节点不匹配)。
残余网络(Residual Network):
增广路径(Augmenting Path):
最大流最小割定理:
| 算法 | 时间复杂度 | 核心思想 | 适用场景 |
|---|---|---|---|
| Ford-Fulkerson | \(O(E \times \text{max\_flow})\) | DFS找增广路径 | 简单实现,小规模 |
| Edmonds-Karp | \(O(VE^2)\) | BFS找最短增广路径 | 保证多项式时间 |
| Dinic | \(O(V^2E)\) | 层次图+阻塞流 | 稠密图,大规模 |
| Hopcroft-Karp | \(O(E\sqrt{V})\) | 多路增广 | 二分图匹配专用 |
| 匈牙利算法 | \(O(V^3)\) | 增广路径翻转 | 二分图匹配 |
算法步骤:
与最大流的关系:匈牙利算法本质上是最大流算法在二分图上的特例。
情况:\(|X| < |Y|\) 或 \(|X| > |Y|\)
处理:
实际应用:任务分配中,员工数可能多于或少于任务数。
定理:最大匹配数 = \(|X| - \max_{S \subseteq X} (|S| - |N(S)|)\)
例子:文档中修改后的例子(丙只能配对B),\(\max_{S \subseteq X} (|S| - |N(S)|) = 1\)(取S={乙,丙}),最大匹配数 = \(3-1 = 2\)。
问题:最大权匹配(Maximum Weight Matching)
算法:
应用:员工分配任务时,不同员工完成不同任务有不同效率。
问题:一个节点可以匹配多个节点(如一个员工可做多个任务)
处理:
例子:公司有5名员工(甲、乙、丙、丁、戊),5项工作(A、B、C、D、E),每名员工只能做1项工作,每项工作只能1人做,员工擅长的工作如下:甲→A、B;乙→B、C;丙→C、D;丁→D、E;戊→E、A。
霍尔定理的作用:判断"能不能让所有员工都分配到擅长的工作"(完美匹配)。检查所有员工子集:比如挑甲、乙、丙,他们擅长的工作是A、B、C、D(4项)\(\geq\) 3人,所有子集都满足条件,说明能全员分配。
最大流的作用:算出"具体怎么分配"(比如甲→A、乙→B、丙→C、丁→D、戊→E),如果员工数多于工作数,还能算出"最多能分配多少名员工"(最大匹配数)。
例子:学校有3间实验室(A、B、C),4个科研小组(1、2、3、4),每间实验室只能容纳1个小组,每个小组只能用1间实验室,小组适配的实验室如下:小组1→A、B;小组2→B、C;小组3→A、C;小组4→A。
霍尔定理的作用:判断"能不能让3个小组用上实验室"(完美匹配,实验室数量少,小组选3个)。挑小组1、2、3,适配的实验室是A、B、C(3间)\(\geq\) 3人,满足条件,说明能实现3个小组分配。
最大流的作用:确定"哪3个小组用哪间实验室"(比如小组1→A、小组2→B、小组3→C),同时能算出"最多能让几个小组用上实验室"(这里是3个),避免资源浪费。
例子:老师有5项作业批改任务,3名学生志愿者,每名志愿者最多批改2项作业,每项作业只能由1名志愿者批改,志愿者能批改的作业类型如下:志愿者1→作业1、2;志愿者2→作业2、3、4;志愿者3→作业4、5。
霍尔定理的作用:判断"能不能让所有作业都被批改"(这里志愿者最多能批改3×2=6项,作业5项,检查子集:比如挑作业1、2、3,能批改的志愿者是1、2(2人),可批改4项 \(\geq\) 3项,满足条件,说明能全部批改)。
最大流的作用:调整水流容量(志愿者→作业的容量=2),算出具体调度方案(比如志愿者1→作业1、2;志愿者2→作业3、4;志愿者3→作业5),确保高效完成所有任务。
场景:广告位(左边)与广告主(右边)的匹配
约束:
建模:
场景:课程(左边)与时间段(右边)的分配
约束:
建模:
不管是人员、资源还是任务,只要涉及"两个阵营的配对,且每个个体只能匹配1个(或固定数量)对象",都能用到两者:
from collections import defaultdict, deque
class BipartiteMatching:
def __init__(self, n_left, n_right):
self.n_left = n_left
self.n_right = n_right
self.graph = defaultdict(list)
def add_edge(self, u, v):
"""添加左边节点u到右边节点v的边"""
self.graph[u].append(v)
def bfs(self, match_left, match_right, dist):
"""BFS寻找增广路径"""
queue = deque()
for u in range(self.n_left):
if match_left[u] == -1:
dist[u] = 0
queue.append(u)
else:
dist[u] = float('inf')
dist[-1] = float('inf')
while queue:
u = queue.popleft()
if dist[u] < dist[-1]:
for v in self.graph[u]:
if dist[match_right[v]] == float('inf'):
dist[match_right[v]] = dist[u] + 1
queue.append(match_right[v])
return dist[-1] != float('inf')
def dfs(self, u, match_left, match_right, dist):
"""DFS寻找增广路径"""
if u != -1:
for v in self.graph[u]:
if dist[match_right[v]] == dist[u] + 1:
if self.dfs(match_right[v], match_left, match_right, dist):
match_right[v] = u
match_left[u] = v
return True
dist[u] = float('inf')
return False
return True
def max_matching(self):
"""计算最大匹配"""
match_left = [-1] * self.n_left
match_right = [-1] * self.n_right
dist = [-1] * (self.n_left + 1)
matching = 0
while self.bfs(match_left, match_right, dist):
for u in range(self.n_left):
if match_left[u] == -1:
if self.dfs(u, match_left, match_right, dist):
matching += 1
return matching
# 使用示例
bm = BipartiteMatching(3, 3)
bm.add_edge(0, 0) # 甲→A
bm.add_edge(0, 1) # 甲→B
bm.add_edge(1, 1) # 乙→B
bm.add_edge(1, 2) # 乙→C
bm.add_edge(2, 2) # 丙→C
print(f"最大匹配数: {bm.max_matching()}") # 输出: 3
from collections import deque
class Dinic:
def __init__(self, n):
self.n = n
self.graph = [[] for _ in range(n)]
self.level = [0] * n
self.ptr = [0] * n
def add_edge(self, u, v, cap):
"""添加边u->v,容量为cap"""
self.graph[u].append([v, cap, len(self.graph[v])])
self.graph[v].append([u, 0, len(self.graph[u]) - 1])
def bfs(self, s, t):
"""BFS构建层次图"""
self.level = [-1] * self.n
self.level[s] = 0
queue = deque([s])
while queue:
u = queue.popleft()
for v, cap, rev in self.graph[u]:
if cap > 0 and self.level[v] == -1:
self.level[v] = self.level[u] + 1
queue.append(v)
return self.level[t] != -1
def dfs(self, u, t, f):
"""DFS寻找阻塞流"""
if u == t:
return f
for i in range(self.ptr[u], len(self.graph[u])):
v, cap, rev = self.graph[u][i]
if cap > 0 and self.level[v] == self.level[u] + 1:
pushed = self.dfs(v, t, min(f, cap))
if pushed > 0:
self.graph[u][i][1] -= pushed
self.graph[v][rev][1] += pushed
return pushed
self.ptr[u] += 1
return 0
def max_flow(self, s, t):
"""计算最大流"""
flow = 0
while self.bfs(s, t):
self.ptr = [0] * self.n
while True:
pushed = self.dfs(s, t, float('inf'))
if pushed == 0:
break
flow += pushed
return flow
# 使用示例(二分图匹配)
# 节点编号:源点0, 左边1-3, 右边4-6, 汇点7
dinic = Dinic(8)
# 源点到左边
dinic.add_edge(0, 1, 1) # 源点→甲
dinic.add_edge(0, 2, 1) # 源点→乙
dinic.add_edge(0, 3, 1) # 源点→丙
# 左边到右边
dinic.add_edge(1, 4, 1) # 甲→A
dinic.add_edge(1, 5, 1) # 甲→B
dinic.add_edge(2, 5, 1) # 乙→B
dinic.add_edge(2, 6, 1) # 乙→C
dinic.add_edge(3, 6, 1) # 丙→C
# 右边到汇点
dinic.add_edge(4, 7, 1) # A→汇点
dinic.add_edge(5, 7, 1) # B→汇点
dinic.add_edge(6, 7, 1) # C→汇点
print(f"最大流: {dinic.max_flow(0, 7)}") # 输出: 3
from itertools import combinations
def check_hall_condition(graph, n_left, n_right):
"""
检查二分图是否满足霍尔条件
graph: 邻接表,graph[u] = [v1, v2, ...] 表示左边节点u连接的右边节点
"""
# 检查所有非空子集
for size in range(1, n_left + 1):
for subset in combinations(range(n_left), size):
# 计算邻居集合
neighbors = set()
for u in subset:
neighbors.update(graph[u])
# 检查霍尔条件
if len(neighbors) < size:
return False, subset, neighbors
return True, None, None
# 使用示例
graph = {
0: [0, 1], # 甲→A,B
1: [1, 2], # 乙→B,C
2: [2] # 丙→C
}
result, subset, neighbors = check_hall_condition(graph, 3, 3)
if result:
print("满足霍尔条件,存在完美匹配")
else:
print(f"不满足霍尔条件,子集{subset}的邻居{neighbors}不足")
初始状态:
甲 ─── A
│ ╲
│ ╲
乙 ─── B
│ ╲
│ ╲
丙 ─── C
匹配过程:
最终状态:
甲 ═══ A (匹配)
│
乙 ═══ B (匹配)
│
丙 ═══ C (匹配)
流网络构建:
源点 ──┬── 甲 ──┬── A ──┬── 汇点
│ │ │
├── 乙 ─┼── B ──┤
│ │ │
└── 丙 ─┴── C ──┘
流量变化:
初始残余网络:
源点 →甲 (容量1, 流量0)
甲 →A (容量1, 流量0)
A →汇点 (容量1, 流量0)
发送1单位流量后:
源点 →甲 (容量0, 流量1) ← 正向边饱和
甲 →源点 (容量1, 流量0) ← 反向边可撤销
甲 →A (容量0, 流量1)
A →甲 (容量1, 流量0)
A →汇点 (容量0, 流量1)
汇点 →A (容量1, 流量0)
在线可视化:
本地工具:
一句话记住:霍尔定理是"裁判",只看能不能配对;最大流是"选手",不仅能判断,还能实际完成配对,而且效率高。实际用的时候,永远用最大流,不用霍尔定理枚举。
入门建议:先搞懂"二分图+完美匹配"的概念,再看例子里的水流模拟,理解最大流的路径怎么来;霍尔定理只要记住"\(|N(S)| \geq |S|\)"这个核心条件,不用死记公式和证明,重点是理解和最大流的关联。
复杂度对比:
| 问题 | 算法 | 时间复杂度 | 适用规模 |
|---|---|---|---|
| 完美匹配存在性 | 霍尔定理枚举 | \(O(2^n)\) | n \(\leq\) 20 |
| 最大匹配 | 匈牙利算法 | \(O(V^3)\) | V \(\leq\) 1000 |
| 最大匹配 | Hopcroft-Karp | \(O(E\sqrt{V})\) | V \(\leq\) 10⁵ |
| 最大流 | Dinic | \(O(V^2E)\) | V \(\leq\) 10⁴ |
| 最大权匹配 | KM算法 | \(O(V^3)\) | V \(\leq\) 500 |
(注:文档部分内容由 AI 生成)
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。