算法的故事 / 第一季第二季目录0 / 30 章已读

第二季 · 第 38 章

谁应该做哪份工作?

《算法的故事》第二季 · 第二篇:寻找世界的最优解——匈牙利算法与一场跨越百年的数学接力1953年秋天,美国宾夕法尼亚州。一位年轻的数学家正在翻阅一本关于图论的著作。突然,他注意到书中的一条脚注。脚注指向1931年一位匈牙利数学家发表的研究。论文讨论矩阵的组合性质,似乎恰好与他正在研究的人员分配问题有关。然而,他遇到了一个意想不到的困难。这篇论文是用匈牙利语写的。那是一个没有互联网、没有在线翻译,也没有大语言模型的年代。他不能输入论文标题,等待几秒钟就得到完整译文。于是,他去附近的大学图书馆借来匈牙利语语法书和一本厚厚的匈牙利语词典。接下来,他开始学习一种自己原本并不熟悉的语言。两周后,这位数学家终于能够阅读那篇论文。而他发现的东西,让自己兴奋不已。如果把这篇1931年的研究,与另一位匈牙利数学家更早的图论结果结合起来,就能构造一种非常实用的新算法。它能够回答这样的问题:有三名工人,要完成三项不同任务。每名工人完成各项任务所需的时间不同。每项任务必须交给一个人,每个人也只能负责一项任务。怎样才能让所有任务的总耗时最短?如果有一百个人、一百项任务,又该怎么办?这位年轻的数学家叫作哈罗德·威廉·库恩(Harold W. Kuhn)。1955年,他发表了一篇重要论文,将这种方法称为:匈牙利方法(The Hungarian Method)。这个名字不是因为库恩来自匈牙利。而是为了向两位匈牙利数学家——德内什·柯尼希(Dénes Kőnig)和耶诺·埃盖尔瓦里(Jenő Egerváry)——致敬。但故事还没有结束。半个世纪之后,库恩又收到一封令人惊讶的邮件。另一位研究者发现,早在十九世纪,一位德国数学家就已经独立写出了一套本质上极其相似的方法。这意味着,算法背后的探索,竟然跨越了一百多年。而贯穿这段历史的,是一个简单却深刻的道理:对每个人来说最好的选择,未必是对整个团队最好的安排。

第一幕:一个所有人都想得到的岗位

20世纪中期工业生产与人员安排的历史环境。以下工厂案例为教学构造,并非库恩1955年论文使用的原始人员数据。想象一家小型工厂。今天有三项任务需要完成。A任务是机器调试。B任务是零件装配。C任务是质量检测。工厂有三名工人:甲、乙、丙。每个人都能够完成这三项任务。但由于经验和技能不同,他们需要的时间并不相同。假设工厂已经测量出一张时间表。
工人A:机器调试B:零件装配C:质量检测
甲4小时1小时3小时
乙2小时0小时5小时
丙3小时2小时2小时
为便于算术演示,乙完成B任务的成本设为0;可以理解为已完成准备工作后不再产生额外计量成本,并非真正的工厂工时数据。三项任务的总耗时按各自成本相加,本例优化的是总工作量而非并行完成时刻。工厂负责人希望找到一种安排,使总耗时最小。并且必须满足两个条件:每个人只负责一项任务。每项任务也只能交给一个人。

最自然的办法:先选最便宜的

经理低头看了一眼表格。乙完成B任务只需要0小时的额外成本。这是整张表里最小的数字。于是,他决定:乙负责B。现在剩下甲、丙两个人,以及A、C两项任务。丙完成C只需要2小时,而甲完成C需要3小时。所以,他继续安排:丙负责C。最后,甲只能负责A。总成本是:0+2+4=60+2+4=6
图38-1:看起来合理的贪心分配甲乙丙ABC402
贪心分配总成本

6小时

这样的安排有错吗?每个人确实得到了一项工作。所有任务也都完成了。甚至,每一步的选择看起来都有道理。但我们还能找到更好的安排。

只交换两个人的任务

保留丙负责C。让甲负责B。让乙负责A。现在总成本变成:1+2+2=51+2+2=5
图38-2:整体最优分配甲乙丙ABC122
最优分配总成本

5小时

为什么第二种更好?因为乙虽然非常擅长B,但甲同样可以用较低成本完成B。相反,乙完成A只需要2,而甲完成A需要4。如果将B任务优先交给乙,就会失去让乙承担A任务所带来的更大收益。问题不是某个人表现得不够好。而是各个人员之间存在不同的相对优势。某个人在某项任务上最好,并不意味着将这项任务交给他,就是整体最优安排。

三个人很容易,三十个人呢?

对于三个人,只有:3!=63!=6种完整分配方式。我们可以逐一检查。但如果有十个人:10!=3 628 80010!=3\,628\,800种安排。如果有二十个人:20!≈2.43×101820!\approx2.43\times10^{18}种安排。
图38-3:人员数量增长带来的组合爆炸
人数与任务数完整分配方案数量
36
5120
840,320
103,628,800
15约1.31万亿
20约2.43×10¹⁸
到了这个规模,我们就不能依靠逐一枚举。但又不能简单地让每个人抢先选择自己最擅长的任务。这就是经典的指派问题(Assignment Problem)。它与上一章的最大流问题有密切联系。我们希望在人员和任务之间建立一组匹配关系。但这一次,匹配不仅要合法,还要让总成本达到最小。而要找到解决方法,我们必须把时间从1953年拨回更早的匈牙利。

第二幕:布达佩斯——两位数学家发现匹配背后的秩序

20世纪前半叶布达佩斯的大学与学术环境。匈牙利数学家在图论、矩阵理论和组合数学方面取得了一系列重要成果。二十世纪初的匈牙利,拥有一个非常活跃的数学研究传统。布达佩斯的大学、学校与学术刊物,为许多数学家提供了成长与交流的环境。在那个年代,图论还没有获得今天这样的地位。许多研究者认为,点与线组成的简单图形更接近数学游戏,而不是值得深入研究的严肃学科。但一位数学家坚持认为,这些看似简单的结构中,隐藏着深刻的定理。他叫 德内什·柯尼希。德内什·柯尼希(1884—1944)。他的重要研究推动了图论成为独立数学学科,1936年出版了经典图论著作。

柯尼希的图论世界

柯尼希1884年出生于布达佩斯。他的父亲也是数学家。他先在布达佩斯学习,后来赴德国哥廷根,接触当时的数学研究。1907年,他获得博士学位,之后长期在布达佩斯从事教学和研究。他逐渐对图论产生了浓厚兴趣。在他看来,城市之间的道路、不同对象之间的关系,以及矩阵中的非零元素,都可能用图来表示。这种抽象能力,使原本完全不同的问题获得了共同的数学语言。1916年,柯尼希发表了与二分图和行列式有关的重要论文。1931年,他又发表了对二分图匹配与顶点覆盖关系具有决定性意义的研究。1936年,他出版了《有限与无限图的理论》,通常被认为是图论最早的系统性专著之一。[MacTutor History of Mathematics][arXiv]

一个改变匹配理论的定理

所谓二分图,就是将所有节点分成两组,并且每条边都连接两组之间的节点。例如:左边是工人。右边是任务。一条边表示某名工人可以承担某项任务。现在,我们希望选出尽可能多的边,使任何两条被选中的边都没有共同端点。这称为匹配(Matching)。如果每个工人都与恰好一项任务配对,每项任务也恰好分配给一名工人,那么我们得到一个完美匹配(Perfect Matching)。柯尼希研究了一个看似不同的问题。如果我们想覆盖图中的全部边,至少需要选中多少个节点?只要一条边的两个端点中有一个被选中,就认为这条边被覆盖。这叫作顶点覆盖(Vertex Cover)。对于二分图,柯尼希证明了一项令人惊讶的关系:最大匹配的边数=最小顶点覆盖的节点数\boxed{\text{最大匹配的边数}=\text{最小顶点覆盖的节点数}}这就是著名的柯尼希定理。
图38-4:匹配与覆盖是同一问题的两面
甲A乙B丙C绿色表示一组匹配,三条边互不冲突
查看 SVG 配图原件
示意图原件 38-007-74026e8ce60a.svg
38-007-74026e8ce60a.svg · 点击查看大图
教学示意:二分图匹配要求每名工人和每项任务至多参与一条选中边。顶点覆盖与匹配的数目相等定理,仅针对二分图具有这一普遍形式。
这个定理为什么重要?因为它把两个表面不同的任务联系起来了。寻找最大数量的不冲突分配。寻找覆盖全部可能关系所需的最少节点。一个问题在尝试增加。另一个问题在尝试减少。但在二分图里,它们的最优数值竟然相同。你应该已经对这种结构有所熟悉。上一章,我们学习过最大流最小割定理。最大流与最小割,表面上也分别是最大化和最小化问题。但它们在最优时会得到相同的数值。数学家将这种现象称为对偶性的一种体现。而柯尼希的定理,将成为匈牙利方法的重要支柱。

另一位数学家:埃盖尔瓦里

耶诺·埃盖尔瓦里(Jenő Egerváry,1891—1958),匈牙利数学家。他将图与矩阵中的组合关系推广到带有权重的情况。埃盖尔瓦里1891年出生于德布勒森。他在布达佩斯接受数学教育,1914年在利波特·费耶尔(Lipót Fejér)指导下完成学位。此后,他在多个具有应用研究背景的机构工作。他的兴趣涉及矩阵理论、应用数学和工程问题。1931年,他发表了一篇研究:Matrixok kombinatorius tulajdonságairól意思是《矩阵的组合性质》。它研究的,不再只是每项关系存在或不存在的问题。而是进一步考虑:如果矩阵中的每个位置都拥有不同数值,怎样研究其中的最佳组合?[IDEAS/RePEc][egres.elte.hu]这与我们的小工厂问题直接相关。如果所有工人完成任务的成本都一样,那么只需要找到任意一组合法匹配。但现实并非如此。甲完成A的成本是4。乙完成A的成本是2。丙完成A的成本是3。每一条匹配边都拥有自己的权重。因此,我们不仅需要完美匹配。还需要最小权完美匹配。埃盖尔瓦里的研究,为这种带权匹配问题提供了重要的数学结构。而多年之后,库恩将会把这些思想连接起来。

历史中无法被省略的黑暗

柯尼希与埃盖尔瓦里的研究,发生在政治动荡不断加剧的欧洲。1944年,纳粹德国控制下的匈牙利发生严重的反犹迫害。柯尼希曾帮助受到迫害的数学家,但最终在1944年10月19日去世,其死亡与当时的政治恐怖密切相关。他没能看到1955年以匈牙利数学家命名的算法发表。埃盖尔瓦里则继续在匈牙利从事研究,但晚年也经历了1950年代政治和学术制度带来的压力,于1958年去世。这些人生经历提醒我们:科学思想可以越过国界传播,但科学家的命运,仍深深受到所处时代的影响。[MacTutor History of Mathematics][多伦多大学数学系]库恩后来在回顾匈牙利方法的历史时,专门用较大篇幅介绍了这几位数学家生活的时代。他认为,理解算法的发现过程,也必须理解那些研究者所身处的真实世界。接下来,我们终于可以返回1953年。回到那本匈牙利语词典。以及那位正在试图读懂1931年论文的美国数学家。

第三幕:1953年,库恩为了一个算法学起匈牙利语

20世纪中期美国大学的数学研究环境。库恩在普林斯顿接受数学训练,1953年则任教于宾夕法尼亚州的布林茅尔学院。图片不是借阅匈牙利语词典的现场。哈罗德·库恩1925年出生于美国。第二次世界大战期间,他曾在军队服役。战后,他进入普林斯顿大学学习,并在数学家 Ralph Fox 指导下获得博士学位。库恩的研究兴趣逐渐从拓扑学延伸到博弈论、线性规划和优化。这并不是偶然的转变。上一章我们认识的丹齐格,正是在1947年提出了单纯形法。此后不久,数学家们开始进一步研究线性规划的对偶理论。库恩与 David Gale、Albert W. Tucker 等研究者参与了其中的重要工作。1950年前后,库恩和塔克还发表了与非线性规划有关的研究,后来著名的 Karush–Kuhn–Tucker 条件便与这些发展密切相关。因此,到了1953年,库恩已经具备了研究指派问题所需的数学背景。他知道怎样将资源配置转化为线性规划。也知道某些优化问题具有特殊的结构,可能值得设计专门算法。

1953年夏天:在洛杉矶寻找更好的计算办法

根据库恩2012年发表的亲自回顾,他在1953年夏天前往加利福尼亚大学洛杉矶分校的数值分析研究机构。那段时间,他同时研究旅行商问题和指派问题。他的目标非常实际:找到一种比通用方法更加适合求解人员分配的算法。为什么?因为丹齐格的单纯形法虽然已经出现,但电子计算机仍然处于发展早期。如果一种特定问题拥有简单得多的算法,那么它就能节省大量计算工作。这时,库恩读到了柯尼希的图论研究。他意识到,关于最大匹配与最小覆盖的数学关系,可能为指派问题提供一个重要突破口。但只知道怎样求出最多数量的不冲突匹配,还不够。他需要处理每个匹配拥有不同成本或评分的问题。接着,柯尼希著作中的一条脚注吸引了他的注意。脚注提到了埃盖尔瓦里1931年的研究。

一本词典,打开了另一种数学语言

1953年秋天,库恩回到布林茅尔学院(Bryn Mawr College)。他前往附近的哈弗福德学院(Haverford College),借来一本匈牙利语语法书和一本大型匈牙利语词典。他后来回忆,自己花了约两周学习足够的匈牙利语,随后翻译了埃盖尔瓦里的论文。匈牙利语语法书与大型词典的场景重构。库恩借书并自学翻译的经历来自他本人的回忆;画面不是当时借阅书籍的实物照片。想象这个过程。一本并不熟悉的数学论文摊在桌上。上面包含匈牙利语的说明文字。还有矩阵、公式与数学符号。数学公式本身或许能够提供一些线索。但要完整理解作者的思路,就必须读懂公式之间的文字。在今天,我们可以借助各种翻译工具迅速完成初稿,再进行人工核对。但对1953年的库恩来说,他需要自己处理这道语言障碍。翻译完成后,他发现埃盖尔瓦里的论文包含一种重要的数学技巧。它能够把带权重的指派问题,逐步转化为只需要判断某些矩阵位置能否同时选中的组合问题。而后一个问题,正好可以利用柯尼希的理论处理。两个研究成果,终于连接起来。

一条完整的研究链

图38-5:匈牙利方法的知识来源
1916、1931
Dénes Kőnig二分图、匹配与顶点覆盖

1931
Jenő Egerváry带权矩阵的组合结构

1953
Harold W. Kuhn翻译论文、连接两种数学思想

1955
匈牙利方法发表给出可实际执行的指派问题算法
库恩并没有停留在数学理论层面。他需要确认算法真正可以计算。于是,他使用随机生成的三位数评分,构造了12人、12项任务的分配问题。然后亲手解题。按照他后来的回忆,这种规模的测试问题,使用纸笔通常不超过两个小时就能完成。在当时,这让他相信这种方法具备实际价值。1955年3月,他在《Naval Research Logistics Quarterly》发表了论文:The Hungarian Method for the Assignment Problem《指派问题的匈牙利方法》。论文发表于第2卷,第83—97页。[Wiley Online Library][多伦多大学数学系]为什么命名为匈牙利方法?库恩在多年后的文章中解释得非常明确。他希望以此纪念两位匈牙利数学家的贡献。这不是一种模糊的传说。是算法发表者本人留下的清楚说明。[IDEAS/RePEc][egres.elte.hu]到这里,历史故事已经有了完整的主线。但最关键的数学问题仍然没有解答。库恩究竟发现了什么?怎样把一张由各种成本组成的复杂矩阵,逐渐转换成一种容易求解的匹配问题?下面,我们就用刚才的小工厂亲自重现这项思想。

第四幕:把成本矩阵里的数字,一行行减掉

20世纪中期数学研究中的矩阵计算场景。下面的三阶矩阵是现代教学案例,不是库恩1955年论文里的原始矩阵。让我们重新看一眼那张成本表。C=[413205322]C= \begin{bmatrix} 4&1&3\\ 2&0&5\\ 3&2&2 \end{bmatrix}每一行对应一名工人。每一列对应一项任务。我们的目标是每行选一个数字,每列也恰好选一个,尽量让三个数字的和最小。

第一项惊人的发现:每行都减去同一个数,不会改变最优分配

考虑第一行:4,1,34,\quad1,\quad3如果我们从三个数字中都减去1,得到:3,0,23,\quad0,\quad2任意完整分配都必须从第一行选择一个数字。因此,无论最后选择A、B还是C,新的总成本都会比原来减少1。所有完整分配都减少相同的数量。所以,不同分配之间的优劣关系不会改变。这意味着:从某一行的所有数字里减去同一个常数,不会改变哪个完整分配是最优的。同理,如果我们对每一列都减去相同的常数,也不会改变最优分配。因为每个完整分配都必须从每一列恰好选择一个数字。这个结论是匈牙利方法的重要基础。

第一步:行归约

我们找到每一行最小的数字。第一行:min⁡(4,1,3)=1\min(4,1,3)=1第二行:min⁡(2,0,5)=0\min(2,0,5)=0第三行:min⁡(3,2,2)=2\min(3,2,2)=2然后,分别从该行所有元素中减去行最小值。得到:C1=[302205100]C_1= \begin{bmatrix} 3&0&2\\ 2&0&5\\ 1&0&0 \end{bmatrix}
图38-6:第一次变换——每行减去最小值
原始矩阵[413205322]\begin{bmatrix}4&1&3\\2&0&5\\3&2&2\end{bmatrix}
行归约之后[302205100]\begin{bmatrix}3&0&2\\2&0&5\\1&0&0\end{bmatrix}
依次减去每行最小值1、0、2。所有完整分配的总成本都恰好减少3。
你可能会问:为什么一定要减去最小值?因为这样能够保证两个性质。第一,变换之后的所有数字仍然非负。第二,每一行至少出现一个零。零很重要。它意味着,某个位置已经达到当前行归约所允许的最低额外成本。接下来,我们要继续制造更多零。

第二步:列归约

现在,观察矩阵:C1=[302205100]C_1= \begin{bmatrix} 3&0&2\\ 2&0&5\\ 1&0&0 \end{bmatrix}第一列的最小值是1。第二列的最小值是0。第三列的最小值也是0。所以只需要从第一列减去1。得到:C2=[202105000]\boxed{ C_2= \begin{bmatrix} 2&0&2\\ 1&0&5\\ 0&0&0 \end{bmatrix} }这样,我们又让所有完整分配的成本额外减少了1。总共减少:1+0+2+1=41+0+2+1=4因此,任意一个完整分配,在原矩阵中的总成本,都比变换后矩阵中对应位置的成本之和大4。用公式表示:Z原=Z变换后+4\boxed{Z_{\text{原}}=Z_{\text{变换后}}+4}这条关系非常重要。我们已经将原来的指派问题,转换成一个拥有更多零的矩阵问题。而且,并没有改变最优分配究竟是什么。

为什么要寻找零?

想象现在我们能够从矩阵中选出三个零。并且满足:每一行恰好选中一个。每一列也恰好选中一个。那么,变换后矩阵的选中成本总和就是:0+0+0=00+0+0=0由于所有数字都非负,没有任何完整分配能够得到低于0的变换后成本。因此,这个方案必定是最优的。再加回之前减掉的常数4,就得到了原问题的最优值。从这个角度看,匈牙利方法有一项非常美丽的目标:通过不改变最优分配的矩阵变换,把复杂的带权选择问题,逐渐变成寻找互不冲突的零。但我们现在遇到了新的麻烦。尽管矩阵里已经有许多零,却未必能够选出三个互不冲突的零。这恰好是柯尼希定理开始发挥作用的地方。

第五幕:所有的零都挤在一起,怎么办?

匈牙利方法的关键不只是制造零,而是找到能够同时选中的零。零的位置之间存在冲突,这正是二分图匹配要解决的问题。现在,我们拥有:C2=[202105000]C_2= \begin{bmatrix} 2&0&2\\ 1&0&5\\ 0&0&0 \end{bmatrix}看起来已经很有希望了。第一行有一个零。第二行也有一个零。第三行甚至有三个零。但是,仔细观察。第一行唯一的零位于B列。第二行唯一的零也位于B列。两名工人都想得到同一项任务。如果我们让甲选择B:乙就不能再选择B。但乙所在的行里已经没有其他零了。所以,即使整个矩阵有五个零,我们仍然无法选出三个彼此不冲突的零。

画线,覆盖全部零

现在尝试一个看似奇怪的操作。用最少数量的水平线与竖直线,把矩阵里全部的零覆盖起来。对于当前矩阵,只需要两条线。一条竖线,覆盖B列。一条横线,覆盖丙所在的第三行。
图38-7:只用两条线,就能够覆盖全部零ABC甲乙丙202105000蓝色:B列;橙色:丙行两条线已经覆盖全部零,因此不存在能够选取三个互不同行、不同列的零的方案。下一步必须改变矩阵结构。
这项操作与柯尼希定理有着直接联系。每一行可以看成一个工人节点。每一列可以看成一个任务节点。只有矩阵元素为零时,才在对应的工人与任务之间画一条边。于是,寻找互不冲突的零,就变成寻找二分图匹配。而使用最少数量的行和列覆盖所有零,则对应寻找最小顶点覆盖。根据柯尼希定理:最大零匹配数量=最少覆盖线数\boxed{\text{最大零匹配数量}=\text{最少覆盖线数}}当前最少只需要两条线。所以最多只能选出两个互不冲突的零。但我们需要三个。这说明,此时还不能结束算法。

埃盖尔瓦里的下一步:改变矩阵,但不破坏最佳方案

我们需要创造新的零。而且不能随便把某个数字修改成零。如果胡乱修改成本,最优分配就可能改变。算法必须通过一种受到数学约束的方式调整整张矩阵。先看刚才两条线没有覆盖的元素。它们是:第一行A列:2。第一行C列:2。第二行A列:1。第二行C列:5。其中最小的是:δ=1\boxed{\delta=1}接下来执行三项操作:
  • 所有没有被线覆盖的元素,减去1。
  • 同时被横线和竖线覆盖的元素,加上1。
  • 只被一条线覆盖的元素,保持不变。
这样,矩阵就从:[202105000]\begin{bmatrix} 2&0&2\\ 1&0&5\\ 0&0&0 \end{bmatrix}变成:C3=[101004010]\boxed{ C_3= \begin{bmatrix} 1&0&1\\ 0&0&4\\ 0&1&0 \end{bmatrix} }看到了吗?原来第二行A列的1,变成了0。我们成功创造了一个新的零。同时,其他数字也发生了有规则的变化。

为什么这样的调整是合法的?

我们可以把这个操作拆成两个更容易理解的步骤。先从所有没有被横线覆盖的行,也就是甲和乙两行,分别减去1。然后,再给所有被竖线覆盖的列,也就是B列,统一加上1。对于任何完整分配:它必定从甲行选出一个数字。也必定从乙行选出一个数字。所以,前一步让所有完整分配的总成本减少2。而任何完整分配,也必定从B列选出一个数字。后一步让所有完整分配的总成本增加1。因此,整张矩阵调整之后,每一种完整分配的总成本都减少:2−1=12-1=1所有方案都发生了同样的变化。它们之间的优劣关系仍然不变。这就是这一步最精妙的地方。算法并不是随意创造零。而是通过一种不改变完整分配相对优劣的变换,改变零在矩阵中的分布。现在,再检查新矩阵:C3=[101004010]C_3= \begin{bmatrix} 1&0&1\\ 0&0&4\\ 0&1&0 \end{bmatrix}甲可以选择B。乙可以选择A。丙可以选择C。三个位置恰好都是零。每个人选中不同任务。每项任务也只被选择一次。我们终于找到了完整的零匹配。
图38-8:三个互不冲突的零
ABC甲乙丙101004010绿色方格构成一个完整的零匹配
查看 SVG 配图原件
示意图原件 38-013-46defd0a689a.svg
38-013-46defd0a689a.svg · 点击查看大图
把选中的位置对应回原始成本表。甲负责B,成本1。乙负责A,成本2。丙负责C,成本2。所以:Z=1+2+2=5\boxed{Z=1+2+2=5}原来所有分配方案的成本,经过前面的变换总共减少:4+1=54+1=5我们在最终矩阵中找到了三个零,所以原问题的最优成本正好是5。这便是匈牙利方法在三阶问题上的一次完整演示。但我们还可以进一步追问:为什么这是数学上真正的最优解,而不只是碰巧找到了一个不错的方案?这个问题,将把我们带到上一章刚刚接触过的思想:对偶性。

第六幕:一份数学证书,证明没有人能做得更好

线性规划对偶理论为匈牙利方法提供了严格的最优性解释。矩阵中的零,对应着原问题与对偶问题同时达到等式的关键位置。在上一章,我们研究过最大流最小割定理。最大流提供一份可以实现的运输方案。最小割提供一个任何方案都不能突破的上界。当两者数值相等,最优性就得到了证明。匈牙利方法拥有相似的结构。只不过,这一次我们寻找的是最小成本,所以需要构造一个下界。

给每个人、每项任务安排一个数学价格

我们为每名工人设置一个数:u1,u2,u3u_1,u_2,u_3再为每项任务设置一个数:v1,v2,v3v_1,v_2,v_3要求它们满足:ui+vj≤cij\boxed{u_i+v_j\le c_{ij}}其中 cijc_{ij} 表示第 ii 名工人执行第 jj 项任务的成本。这些变量并不是工人实际工资,也不是任务真实市场报价。它们是为了证明最优性而引入的数学变量。现在,考虑任意一个合法的完整分配。因为每个工人恰好出现一次,每项任务也恰好出现一次,所以该分配的总成本一定满足:Z≥∑iui+∑jvjZ\ge\sum_i u_i+\sum_j v_j因此:∑iui+∑jvj\boxed{\sum_i u_i+\sum_j v_j}就是所有合法分配成本的一个共同下界。只要我们能够找到一组满足约束的 ui,vju_i,v_j,就能立即得到一个对全部分配方案成立的成本下界。

给出一份具体的下界证书

对于我们的小工厂,可以取:u=(2,1,2)u=(2,1,2)以及:v=(1,−1,0)v=(1,-1,0)现在检查每个组合。例如,甲负责A:u1+v1=2+1=3≤4u_1+v_1=2+1=3\le4甲负责B:u1+v2=2−1=1≤1u_1+v_2=2-1=1\le1乙负责A:u2+v1=1+1=2≤2u_2+v_1=1+1=2\le2丙负责C:u3+v3=2+0=2≤2u_3+v_3=2+0=2\le2其他组合也满足对应的不等式。因此,这是一组合法的对偶变量。它提供的总成本下界是:L=(2+1+2)+(1−1+0)=5\begin{aligned} L&=(2+1+2)+(1-1+0)\\ &=5 \end{aligned}也就是说:Z≥5\boxed{Z\ge5}任何合法分配,都不可能花费少于5。而我们已经找到:甲→B\text{甲}\rightarrow B乙→A\text{乙}\rightarrow A丙→C\text{丙}\rightarrow C其成本恰好为:Z=5Z=5于是:Zmin⁡=5\boxed{Z_{\min}=5}证明完成。
图38-9:一份最优分配与一份对偶证书
实际找到的可行分配

5

成本上界
由对偶变量证明

5

成本下界
5≤Zmin⁡≤55\le Z_{\min}\le5

最优成本 = 5

零与对偶变量到底是什么关系?

回顾最后的矩阵:C3=[101004010]C_3= \begin{bmatrix} 1&0&1\\ 0&0&4\\ 0&1&0 \end{bmatrix}如果我们用原始成本减去对应行和列的对偶变量:cij−ui−vjc_{ij}-u_i-v_j你会发现,得到的恰好就是这个非负矩阵。也就是说:C3[i,j]=cij−ui−vj\boxed{C_3[i,j]=c_{ij}-u_i-v_j}所以,当某个位置为零时:cij=ui+vjc_{ij}=u_i+v_j它表示对应的对偶约束恰好达到等式。这样的边,被称为紧边(Tight Edge)。因此,匈牙利方法不断创造零,实际上也可以理解为:不断调整对偶变量,让更多能够参与最优分配的紧边出现。然后,在这些紧边中寻找完整匹配。当我们既找到一个完整的零匹配,又拥有合法的对偶变量时,最优性就可以得到严格证明。这正是线性规划中的**原始—对偶(Primal–Dual)**思想。它和前两章的联系非常深刻。丹齐格的单纯形法,在可行解之间寻找更好的目标值。福特与富尔克森的最大流算法,用一份流和一道割共同证明最优。库恩的匈牙利方法,则通过带权匹配与对偶变量,一边寻找可行分配,一边建立最优性保证。它们并不是三种毫无关联的计算技巧。而是现代组合优化逐渐形成的一套共同思想:不仅要找到答案,还要给出证明没有更好答案的数学证书。

第七幕:互动实验室——亲手运行匈牙利方法

匈牙利方法在早期可以依靠纸笔完成。库恩曾用手算验证12×12的分配问题,今天我们可以在计算机里逐步重现这套矩阵变换。现在,轮到你亲自执行这项算法。你不需要先背诵任何复杂程序。只要理解四件事:第一,每行每列都减去适当的常数,不会改变最优分配。第二,零代表在当前对偶调整下达到等式的候选匹配位置。第三,如果无法从零中选出完整匹配,就需要通过最小覆盖找到调整方向。第四,当找到完整的零匹配时,就能得到最优分配及其数学证明。

实验一:让矩阵一步步出现新的零

图38-10:匈牙利方法逐步演示步骤 0 / 4
原始成本矩阵
任务 A任务 B任务 C
甲
4
1
3
乙
2
0
5
丙
3
2
2
每一行是一名工人,每一列是一项任务。目标是每行、每列恰好选一个数字,让总和最小。
相对原始成本的统一减少量

0

实验二:你来当工厂经理

前一个实验展示了算法怎样找到最优方案。现在,不妨暂时忘记答案。由你亲自安排三名工人。每个人选择一项任务。系统会判断有没有任务冲突,并计算总成本。
图38-11:三名工人的任务分配挑战在下面指定各自任务。每项任务不能重复使用。
工人当前任务成本
甲A4
乙B0
丙C2
你选择的总成本

6

最优成本

5

这份安排合法,但还可以减少 1 个成本单位。试着交换两个人的任务。

实验三:不用专业优化库,自己实现匈牙利算法

现在,我们使用 Python 编写一个实际能够处理 n×mn\times m 成本矩阵的匈牙利方法实现。为了便于处理潜在变量和匹配关系,这份程序采用一种常见的原始—对偶写法。它与刚才“行归约、列归约、覆盖零”的教学演示共享同一数学思想,但在实现上通过位势和增广路径更新,不必真的把所有覆盖线画出来。
python

def hungarian(cost):
    """
    求最小成本指派。
    要求:至少有一行,矩阵非空且为矩形,行数 <= 列数。
    返回:(每行匹配的列索引列表, 最小总成本)
    """
    n = len(cost)
    if n == 0 or not cost[0]:
        raise ValueError("成本矩阵不能是空的")

    m = len(cost[0])

    if any(len(row) != m for row in cost):
        raise ValueError("成本矩阵必须是矩形")

    if n > m:
        raise ValueError("此实现要求行数不大于列数")

    u = [0] * (n + 1)
    v = [0] * (m + 1)
    p = [0] * (m + 1)
    way = [0] * (m + 1)

    for i in range(1, n + 1):
        p[0] = i
        j0 = 0

        minv = [float("inf")] * (m + 1)
        used = [False] * (m + 1)

        while True:
            used[j0] = True
            i0 = p[j0]
            delta = float("inf")
            j1 = 0

            for j in range(1, m + 1):
                if not used[j]:
                    cur = cost[i0-1][j-1] - u[i0] - v[j]

                    if cur < minv[j]:
                        minv[j] = cur
                        way[j] = j0

                    if minv[j] < delta:
                        delta = minv[j]
                        j1 = j

            for j in range(m + 1):
                if used[j]:
                    u[p[j]] += delta
                    v[j] -= delta
                else:
                    minv[j] -= delta

            j0 = j1

            if p[j0] == 0:
                break

        while True:
            j1 = way[j0]
            p[j0] = p[j1]
            j0 = j1

            if j0 == 0:
                break

    assignment = [0] * n

    for j in range(1, m + 1):
        if p[j] != 0:
            assignment[p[j] - 1] = j - 1

    total = sum(
        cost[i][assignment[i]]
        for i in range(n)
    )

    return assignment, total


cost = [
    [4, 1, 3],
    [2, 0, 5],
    [3, 2, 2]
]

assignment, total = hungarian(cost)

workers = ["甲", "乙", "丙"]
jobs = ["A", "B", "C"]

for i, j in enumerate(assignment):
    print(
        workers[i],
        "负责任务",
        jobs[j],
        "成本",
        cost[i][j]
    )

print("最小总成本:", total)
程序将输出:
text
甲 负责任务 B 成本 1
乙 负责任务 A 成本 2
丙 负责任务 C 成本 2
最小总成本: 5
如果输入的是一个普通的 n×nn\times n 稠密成本矩阵,上述实现的时间复杂度为:O(n3)\boxed{O(n^3)}这比枚举全部 n!n! 种分配更适合较大的问题。不过,我们应该准确区分历史与现代实现。库恩1955年的原始算法,与后来经过改进的匈牙利方法,不应简单视为完全相同的程序版本。1957年,James Munkres 研究了指派和运输问题的算法,推动了该方法的复杂度分析。此后,Edmonds、Karp、Tomizawa 等研究者又发展了相关更高效的实现,使 O(n3)O(n^3) 的稠密指派求解成为经典结果之一。[Zenodo][IDEAS/RePEc]如果只是希望在真实 Python 项目中求解分配问题,通常不需要自己重写上述算法。可以直接使用 SciPy:
python

import numpy as np
from scipy.optimize import linear_sum_assignment

cost = np.array([
    [4, 1, 3],
    [2, 0, 5],
    [3, 2, 2]
])

rows, cols = linear_sum_assignment(cost)

print("工人索引:", rows)
print("任务索引:", cols)
print("最小成本:", cost[rows, cols].sum())
这里也有一个值得说明的技术细节。当前 SciPy 的 linear_sum_assignment 使用的是经过修改的 Jonker–Volgenant 算法,而不是直接照搬1955年库恩论文的原始实现。它同样解决线性指派问题,支持矩形成本矩阵,并可以通过 maximize=True 处理最大收益分配。[SciPy v1.18.0 Manual]历史上形成的算法思想,被后来的工程实现不断改进。这正是《算法的故事》里不断重复出现的主题。一项算法并不是发表之后就凝固不动。它会随着数学研究、计算机硬件和实际需求的变化,持续演化。

第八幕:2005年,一份十九世纪的手稿让库恩大吃一惊

卡尔·古斯塔夫·雅可比(Carl Gustav Jacob Jacobi,1804—1851),十九世纪德国数学家。他的遗稿中包含与现代匈牙利方法在数学结构上基本等价的算法。现在,时间来到2005年。距离库恩发表匈牙利方法,已经过去了整整五十年。算法早已成为组合优化中的经典。数学教材介绍它。计算机科学学生学习它。各种优化软件实现它。人们也已经充分认识了柯尼希和埃盖尔瓦里的历史贡献。但就在这一年,法国数学家 François Ollivier 在研究十九世纪的数学文献时,有了一项令人惊讶的发现。他发现,德国数学家 卡尔·古斯塔夫·雅可比 的遗稿中,竟然描述过一种与匈牙利方法极为相似的计算过程。雅可比1851年去世。而匈牙利方法直到1955年才正式发表。两者之间相隔一百多年。

雅可比为什么会研究人员分配?

他并不是在为一家工厂安排工人。也不是在研究现代意义上的运输调度。他的研究对象,是常微分方程组。在分析微分方程的阶数和相关数学界限时,他遇到了一种矩阵式的组合选择问题。这类问题需要从不同位置选取元素,并使某个数学量达到最优。从现代角度看,其结构与指派问题密切相关。雅可比由此发展出一套能够高效求解该问题的方法。这段研究生前并未以现代读者能够轻易获知的方式传播。相关成果保存在遗稿中,并于1890年以身后出版物的形式发表。

一封跨越世纪的邮件

根据库恩2012年发表的回忆,他在2006年初收到 Ollivier 发来的邮件。邮件讨论了雅可比的两篇拉丁文研究,以及其中涉及的指派问题算法。库恩随即认真比较了雅可比的方法与自己1955年提出的匈牙利方法。他的结论非常明确。虽然两种算法使用不同的语言与符号,也采用不同的数学表达,但核心计算过程在适当解释下本质上相同。库恩甚至在2012年的论文中,使用同一个矩阵,逐步展示了这两套方法怎样产生对应的计算步骤。[IDEAS/RePEc][多伦多大学数学系]
图38-12:跨越三个时代的算法历史

19世纪

雅可比的独立研究研究微分方程时,发展出与后来的指派方法本质相近的算法。成果在其去世后发表。

1916—1955

匈牙利研究与库恩的重新构造柯尼希、埃盖尔瓦里的研究,为库恩1955年发表的方法提供了数学基础。

2005—2012

历史上的重新发现Ollivier 发现雅可比遗稿的联系,库恩随后发表对比研究,重新解释算法的历史源流。

那么,匈牙利算法到底是谁发明的?

这是一个必须认真回答的问题。如果只看已知的数学方法出现时间,雅可比的研究明显更早。如果看二十世纪图论与带权匹配理论的发展,柯尼希和埃盖尔瓦里有不可替代的贡献。如果看1950年代算法被系统整理、命名、公开发表,并获得广泛传播的过程,库恩的工作同样十分重要。这几项历史事实并不需要彼此否定。同一种数学结构,完全可能被不同年代的人,从不同的问题中独立发现。早期的发现可能长期没有得到广泛传播。后来的研究者在不知道前人工作的情况下,重新找到类似的方法。而一项理论真正成为普遍使用的算法,往往还需要形式化、证明、实现和推广。因此,最准确的说法是:库恩于1955年发表并命名了现代匈牙利方法;它建立在柯尼希与埃盖尔瓦里的成果上,而后来重新发现的雅可比遗稿表明,一种本质相近的算法在十九世纪就已存在。这比简单宣称“某一个人独立发明了一切”更加接近真实科学史。对于我们这套《算法的故事》而言,这也是一段尤其值得记住的经历。数学知识并不总沿着最短道路传播。有时,一篇论文会因为语言障碍而长期不被更广泛的研究者理解。有时,一份手稿会在档案里沉睡一百多年。有时,一个已经成为经典的算法,还会在半个世纪之后,突然发现自己拥有更古老的祖先。算法的历史,本身也是一个不断寻找、连接与重新发现知识的过程。

第九幕:从人员分配,到机器人和人工智能

现代工业系统中的任务调度经常涉及人员、机器和任务之间的匹配。匈牙利方法适用于其中满足线性指派结构的一类问题。进入二十一世纪,指派问题已经远远超出了工厂分工。我们可以把它抽象成两组对象。一边是需要分配的资源。另一边是需要完成的任务。每一种资源与每一项任务之间,都有一个成本、时间、距离或收益。目标是找到一组互不冲突的匹配,并让整体成本最小或整体收益最大。这种结构,在现代计算机系统中非常常见。

机器人如何分配任务?

假设一个仓库有五台机器人。仓库里有五项搬运任务。每台机器人到达不同货架所需的移动时间并不一样。如果每台机器人只承担一项任务,那么我们可以计算一个成本矩阵:cij=机器人 i 完成任务 j 的预计成本c_{ij}=\text{机器人 }i\text{ 完成任务 }j\text{ 的预计成本}然后求解最小成本匹配。当然,真实仓库还可能涉及电量、路线冲突、任务先后关系和充电安排。这些因素并不一定都能直接用简单指派模型表达。但指派算法可以成为大型调度系统中的一个重要组成部分。

计算机视觉如何追踪多个对象?

多目标追踪示意:每一帧中检测到的目标,需要与上一时刻的已有轨迹建立关联。线性指派是常见的数据关联模型之一。再考虑一段监控或运动分析视频。上一帧里有三个人。这一帧里也检测到三个人。计算机怎样判断:这一帧的第一个人,究竟对应上一帧的哪一条轨迹?如果只根据距离选择最近的目标,可能出现错误。因为不同的人会移动。他们可能彼此靠近、交叉或发生短暂遮挡。一种常见做法是,为每条已有轨迹与每个新检测目标计算关联成本。例如:位置距离。外观特征差异。运动预测误差。然后通过指派算法,寻找整体成本最小的对应关系。但需要注意,实际多目标追踪还要处理新目标出现、旧目标消失、漏检、误检和遮挡等情况。因此,匈牙利方法通常只是追踪系统的数据关联环节,而不是完整的视觉追踪算法。

当代优化软件中的匈牙利思想

在现代 SciPy 中,线性指派求解器可以处理矩形成本矩阵。也就是说,人员数量与任务数量不必完全相同。如果任务数量多于人员数量,可能只为部分任务分配人员。如果存在任务禁止分配、不同资源容量或其他特殊约束,还需要根据具体模型处理。这一点提醒我们:同一个数学算法,并不是简单地把任何现实对象放进表格就能自动获得合理的决策。算法会忠实地优化我们提供的目标,但它并不知道我们是否遗漏了现实中真正重要的条件。这也是为什么好的运筹学研究,不只是计算出一个答案。还需要不断检查模型。目标函数是否合理?成本数据是否可靠?哪些约束不能违反?某些结果是否会产生不公平的分配?在没有建模这些问题时,匈牙利算法本身并不会替人类作出价值判断。这为下一章埋下了一个重要伏笔。

尾声:为什么每个人都满意,整体却未必最好?

让我们再次回到1953年。库恩坐在书桌前。他试图将人员分配问题变成一个可以高效求解的数学问题。柯尼希的图论研究已经告诉他,匹配与覆盖之间存在深刻关系。埃盖尔瓦里的论文又提供了处理带权矩阵的数学技巧。他借来匈牙利语词典,自己翻译那篇论文。然后将两项研究结合起来。终于,一套能够在一般成本矩阵中寻找最佳分配的方法逐渐形成。两年之后,它正式发表。此后,数学家和程序员不断改进算法的计算过程。它被用于工业生产、物流规划、数据关联和许多其他领域。而在几十年之后,人们又从雅可比十九世纪的遗稿中,发现了它更加古老的数学踪迹。从十九世纪德国的微分方程研究,到二十世纪匈牙利的图论,再到1950年代美国的运筹学,最后进入现代计算机软件。四位研究者,三个不同时代。他们面对的数学问题各有不同。但最终连接到同一种深刻的结构。怎样在大量彼此冲突的选择中,找到整体最优的一组匹配?这就是匈牙利方法的核心问题。它告诉我们,局部看起来最便宜的选择,不一定能够组成最便宜的整体方案。某个人也许非常适合某项工作。但如果把这项工作交给他,另一名工人可能不得不承担一项成本高得多的任务。因此,真正的优化需要考虑所有人和所有任务之间的关系。这是一种超越个人最优的系统性思维。然而,当我们把这套方法用于人类社会时,还会遇到一个新的问题。假设不是工厂经理安排工人。而是学生选择学校。医院选择住院医师。或者两组人按照各自的偏好进行配对。这时,成本最低是否就意味着每个人都会接受这样的安排?未必。一个数学上总成本最小的匹配,可能让某些参与者强烈希望改变自己的配对。如果两名参与者都希望彼此重新配对,那么原本的安排可能变得不稳定。这不是简单的成本问题。而是一个关于偏好、激励与稳定性的问题。当分配对象不再只是被动接受安排的机器和任务,而是拥有自己意愿的人,什么样的匹配才能真正维持下去?这将成为下一章的核心问题。

历史档案:从原始论文到发现者的亲自回忆

本章特别重视匈牙利算法历史中的“发现、重新发现和传播”三个不同层次。主要资料包括:1. Harold W. Kuhn:《The Hungarian Method for the Assignment Problem》(1955)。原始论文发表于《Naval Research Logistics Quarterly》第2卷,第83—97页,摘要明确指出方法结合了两位匈牙利数学家的研究。2. Harold W. Kuhn:《A Tale of Three Eras》(2012)。库恩亲自撰写的历史研究,详细说明1953年借匈牙利语词典、翻译埃盖尔瓦里论文、1955年算法发表,以及后来发现雅可比早期算法的过程。3. MacTutor 数学史档案:Dénes Kőnig。用于核查柯尼希的研究经历、图论贡献及所处历史时代。4. András Frank:《On Kuhn's Hungarian Method — A Tribute from Hungary》。从匈牙利数学界的视角,说明柯尼希与埃盖尔瓦里成果和匈牙利方法之间的数学关系。5. SciPy 官方文档:linear_sum_assignment。本章三阶成本矩阵采用这一经典教学示例;文档也说明当前具体实现与历史上的原始匈牙利算法有所不同。本章开篇对1953年研究环境的描写,以及工厂经理的分配决策,属于根据真实数学问题组织的教学叙事。库恩学习匈牙利语并翻译论文是有本人回忆支持的史实,但具体动作和心理活动没有被当成逐时记录的历史事实。矩阵归约、最小零覆盖、对偶证明与交互实验均为现代教学表达,其结果可独立验证。
下一章预告 · 第39章

《为什么最满意的分配反而不稳定?》

1962年,两位数学家开始研究一个与人员分配密切相关,却拥有完全不同目标的问题。假设有一组学生和一组学校。每个学生都拥有自己的志愿顺序。每所学校也有自己偏好的录取对象。我们能够找到一种安排,让任何学生和学校都没有理由私下改变配对吗?如果这样的安排存在,又该怎样计算?下一章,我们将认识 David Gale 与 Lloyd Shapley,进入稳定匹配与延迟接受算法的世界。从匈牙利算法的“成本最优”,走向 Gale–Shapley 算法的“匹配稳定”,探讨当算法面对拥有独立意愿的人时,为什么优化的目标必须发生变化。
——《算法的故事》第二季 · 第38章 完

本章配图