1、学习目标

算法设计的第一步不是写代码,而是把现实问题压缩成清楚的数学对象:输入是什么,输出是什么,什么叫合法,什么叫更好。本章用稳定匹配作为第一个完整案例,练习从问题建模到算法证明。

模块 要掌握的内容
问题形式化 输入、输出、约束、目标
稳定匹配 偏好列表、匹配、阻塞对、稳定性
Gale-Shapley 求婚方提出、接受方保留、迭代收敛
正确性证明 终止性、完备性、稳定性
复杂度 每对最多被提议一次,所以 O(n^2)

2、算法问题如何描述

一个算法问题通常写成:

1
2
3
输入:满足某些条件的数据对象
输出:满足某些性质的解
目标:如果有多个解,如何衡量好坏

比如排序:

1
2
3
输入:n 个可比较元素
输出:按非降序排列的序列
目标:正确排序,并尽可能高效

稳定匹配:

1
2
3
输入:两组人数相等的对象,每个人给出对另一组的偏好排序
输出:一组一对一匹配
目标:不存在会破坏当前匹配的阻塞对

3、稳定匹配问题

设有 n 个学生和 n 个学校,双方都给出对另一方的完整偏好列表。

基本概念:

概念 说明
匹配 每个学生分配给一个学校,每个学校接收一个学生
完美匹配 所有人都被匹配
阻塞对 学生和学校彼此都更喜欢对方,而不是当前匹配对象
稳定匹配 不存在任何阻塞对

阻塞对是稳定匹配的核心。它表示当前结果里有一对双方都有动力私下换掉现有搭档,因此这个结果“不稳定”。

4、Gale-Shapley 算法

以学生向学校提出申请为例:

1
2
3
4
5
6
7
while 存在未匹配且还没申请完的学生:
学生 s 向自己列表中尚未申请过的最喜欢学校 c 申请
if c 当前没有匹配:
c 暂时接受 s
else:
比较 s 和当前学生 s'
c 保留更喜欢的那个,拒绝另一个

这个算法有两个很重要的动态:

  1. 学生的选择只会越来越差,因为他按偏好从高到低申请。
  2. 学校手里的暂定对象只会越来越好,因为它每次保留更喜欢的人。

5、终止性

每个学生最多向每个学校申请一次,总申请次数最多 n^2。每次循环至少发生一次新的申请,所以算法一定会终止。

1
申请次数 <= n * n

这类证明很常见:找到一个单调变化且有上界的量。这里的量就是“已经发出的申请数量”。

6、完备性

算法结束后,不会有人没被匹配。

证明思路:

  1. 假设某个学生 s 未匹配。
  2. 若他还没申请完,算法不应停止。
  3. 若他申请过所有学校,则每个学校都至少收到过申请。
  4. 学校收到申请后会一直保留一个暂定对象,不会变空。
  5. 那么所有学校都有人,学生总数和学校总数相等,s 不可能未匹配。

所以算法结束时得到完美匹配。

7、稳定性

假设算法结果存在阻塞对 (s, c),即:

  1. s 更喜欢 c,而不是当前学校。
  2. c 更喜欢 s,而不是当前学生。

因为 s 按偏好从高到低申请,既然他更喜欢 c,他一定在当前学校之前向 c 申请过。

当时有两种情况:

  1. c 接受了 s,但后来只可能换成更喜欢的人。
  2. c 拒绝了 s,说明当时已有更喜欢的人,后面也只会更好。

因此最终 c 不可能更喜欢 s 胜过当前学生,矛盾。算法结果稳定。

8、复杂度

部分 复杂度
申请次数 O(n^2)
若偏好比较用顺序扫描 可能到 O(n^3)
若预处理排名表 O(n^2)
总复杂度 O(n^2)

实现时通常预处理:

1
rank[c][s] = 学校 c 对学生 s 的排名

这样比较两个学生谁更受学校喜欢就是 O(1)

9、算法设计启发

稳定匹配这个例子给出的训练价值:

训练点 启发
建模 先定义阻塞对,才能定义稳定性
算法 简单规则反复执行,也可能得到全局性质
证明 终止性、完备性、稳定性要分开证明
数据结构 预处理排名表能把比较降到 O(1)
结果偏向 哪一方提出申请,结果会偏向哪一方

10、易错点

易错点 修正
把稳定理解成双方都最满意 稳定只要求不存在阻塞对
忘记证明完美匹配 稳定性之前先证明所有人都匹配
学校拒绝后还可能后悔 学校手里的人只会越来越好
复杂度只看循环 偏好比较如果不预处理会多一层
认为稳定匹配唯一 稳定匹配可能不唯一

11、复盘清单

检查项 状态
能形式化描述一个算法问题 待复盘
能解释阻塞对和稳定匹配 待复盘
能手动模拟 Gale-Shapley 算法 待复盘
能证明算法终止 待复盘
能证明结果稳定 待复盘
能说明为什么预处理排名表 待复盘