Gale-Shapley算法

前言

最近看了一档综艺《心动的信号》(唉, 单身久了, 开始喜欢看别人谈恋爱了)

节目*有n男n女, 他们会在节目的最后进行表白, 如果我喜欢你, 恰好你也喜欢我, 那么便就会在一起, 自此传为一段佳话.

于是, 我就在想, 如何用算法来实现这个匹配的过程呢?

单一匹配

将信息抽象化, 现有两个集合 M N, 每个集合中存在a个对象.

结果集 R 中元素为 (m, n), 其中 m ∈ M, n ∈ N, m 喜欢 n, n 也喜欢 m.

OK, 设计数据结果进行实现. (以下以Go进行简单演示)

package main

import "fmt"

type people struct {
	Name       string // 名字
	LikePeople string // 喜欢的人名
}

type lovers struct {
	Boy  people
	Girl people
}

func main() {
	// 分别构造男女队列
	boyArr := []people{}
	girlArr := []people{}
	// 分别对他们进行匹配
	var result []lovers
	for _, boy := range boyArr {
		// 查看他喜欢的女孩是否喜欢他
		for _, girl := range girlArr {
			if boy.LikePeople == girl.Name {
				// 两情相悦了
				if girl.LikePeople == boy.Name {
					result = append(result, lovers{
						Boy:  boy,
						Girl: girl,
					})
				}
				break
			}
		}
	}
	fmt.Println(result)
}

当然, 这就是简单的循环查找, 但是如果将喜欢的人, 从单个换成一个列表呢? 别说, 我后面找了一下, 还真有这么一个算法.

Gale-Shapley 算法

再来.现有两个集合 M N, 每个集合中分别存在a个对象. 匹配集 R 中的元素为: (m, n), 其中 m ∈ M, n ∈ N.

到这, 基本上和我们上面的匹配一致. Gale-Shapley在此基础上, 提出了完美匹配稳定匹配的概念.

完美匹配

完美匹配是指, MN的每个成员, 都恰好出现在R的一个匹配队列中. 恰好的意思是, 不多不少就一次.

说人话就是: MN中的所有人都配对成功, 不存在落单的男孩或女孩.

稳定匹配

一个完美匹配中如果不存在不稳定因素, 就称为稳定匹配.

我们上面每个人只能喜欢一个人, 现在不是了, 每个人都有一个喜欢程度从高到低的列表.

如果说 m1n1 n2中更喜欢 n2, 那么m1的喜欢列表就是[n2, n1].

简单解释一下不稳定因素. 如果在结果集中, 存在这样的两个元素: (m1, n1), (m2, n2). 而m1相较于n1更喜欢n2, 同时n2相较于m2也更喜欢m1, 那么他俩就有私奔的可能. 这种可能就称为不稳定因素.

Gale-Shapley算法, 就是从中得出一个稳定匹配的算法. 算法的思想通俗易懂, 一句话概括: 所有男生依次尝试想所有女生表白

算法的实现步骤如下:

  • 找到一个还没有对象, 且未向所有女生表白的男生m
  • 找到一个m还没有表白过的女生n
  • 如果n还没有对象, 则进行匹配
  • 如果n有对象. 则判断n的喜欢列表. 若更喜欢当前对象, 则保持不变, 若更喜欢m则抛弃当前对象
  • 直到m找到对象或向所有女生都表白过. 则回到第一步, 直到找不到这样的男生.

通过流程上来看, 这是一个时间复杂度O(n^2)的算法. 借用Go来简单实现一下:

package main

type people struct {
	Name        string   // 名字
	LikePeople  []string // 喜好列表
	CurrentLike int      // 后面算法记录当前表白对象时使用
	Friend      string   // 当前匹配对象
}

func main() {
	// 分别构造男女队列
	boyArr := []*people{}
	girlArr := []*people{}
	for true {
		// 找到一个没有对象, 且未全部表白的男生
		var searchBoy *people
		for _, boy := range boyArr {
			if boy.Friend != "" { // 当前男孩已经有对象了
				continue
			}
			// 男孩向所有女生表白过了
			if boy.CurrentLike >= len(boy.LikePeople) {
				continue
			}
			searchBoy = boy
			break
		}
		if searchBoy == nil { // 已经全部有对象了, 结束
			break
		}
		// 男生向女生依次表白
		var i int
		for i := searchBoy.CurrentLike; i < len(searchBoy.LikePeople); i++ {
			girlName := searchBoy.LikePeople[i]
			// 找到这个女孩
			girl := searchPeople(girlArr, girlName)
			if girl == nil { // 习惯了, 判下空
				continue
			}
			if girl.Friend == "" { // 若女孩没有对象, 则直接配对
				girl.Friend = searchBoy.Name
				searchBoy.Friend = girl.Name
				break
			} else { // 若女孩有对象, 看下 girl 更喜欢谁
				searchBoyIdx := searchNameIndex(girl.LikePeople, searchBoy.Name)
				girlFriendIdx := searchNameIndex(girl.LikePeople, girl.Friend)
				if girlFriendIdx < searchBoyIdx { // 保持当前
					continue
				} else { // 重新组队
					girlFriend := searchPeople(boyArr, girl.Friend)
					if girlFriend != nil { // 分手了
						girlFriend.Friend = ""
					}
					girl.Friend = searchBoy.Name
					searchBoy.Friend = girl.Name
				}
			}
		}
		searchBoy.CurrentLike = i
	}
}

func searchPeople(peopleArr []*people, name string) *people {
	for _, people := range peopleArr {
		if people.Name == name {
			return people
		}
	}
	return nil
}

func searchNameIndex(nameArr []string, name string) int {
	for i, tmpName := range nameArr {
		if tmpName == name {
			return i
		}
	}
	return -1
}

拢共也没有几行, 但是, 它可以保证完美匹配稳定匹配么? 这简单的逻辑让我都有点不相信自己了, 不行, 得证明一下.

首先是完美匹配, 因为是进行的一对一匹配, 如果最终存在落单的女生, 那么就一定存在相同数量落单的男生. 同时, 因为在女生没有对象的时候, 会接收任意男生的表白, 可以得出落单的女生没有收到过任何男生的表白. 而在匹配的过程中, 男生会向喜欢列表中的所有女生依次进行表白, 得出落单的男生一定向所有女生进行过表白. 前后矛盾. 故不存在这样的情况. 因此结果为完美匹配.

那么结果是否是稳定匹配呢? 如果结果中存在不稳定因素, 既(m1, n1), (m2, n2), 其中m1更喜欢n2, 同时n2也更喜欢m1. 根据算法的规则, m1会先向n2表白, 此时, 如果n2单身, 则会接受表白, 如果n2已经接受了m2的表白, 则会毅然决然的选择分手, 和m1在一起. 即使后面n2再次收到m2的表白, 也不会和m1进行分手. 故不存在这样的不稳定因素. 因此结果为稳定匹配.

算法优化

我们在最开始分析的时候, 时间复杂度是O(n^2), 现在再来看一下我们实现的时间复杂度. 上方算法共有一下几层循环:

  • 找到没有对象且未全部表白的男生, n
    • 向女生依次表白, n
      • 找到表白的女生, n
      • 若女生有对象, 则从女生的喜欢列表中, 找到这两个男孩. 2n
      • 若重新组队, 则找到女生的当前对象, n

. 很显然, 大 O 时间复杂度为O(n^3). 哎? 怎么和之前分析的不一样呢? 很明显, 就差在了第三层. 只要想办法消掉就好啦.

  • 找到要表白的女孩. 直接存储女孩的索引即可直接找到.
  • 找到女生当前对象同理, 直接存储索引.
  • 从女生的喜欢列表中, 找到更喜欢谁? 将数组换成 map, 即可实现O(1)的查找. 空间换时间呗.

至此, 第三层的循环全部去掉. 时间复杂度为O(n^2). 能不能再降低呢? 我还没有想到.

扩展

人数不匹配

如果男女生人数不一致呢? 那自然是无法得出完美匹配稳定匹配了, 但是通过上面的步骤, 无论是让人数较少的一方主动选择, 还是人数较多的一方主动选择, 得出的还是一个比较满意的匹配结果. 当然, 因为人数不匹配, 最终是一定有落单的人.

喜欢列表不为全部

如果女生的喜欢列表, 只是部分男生呢? 那么对于未出现在喜欢列表的人有两种情况:

  1. 十分讨厌, 不能进行匹配
  2. 无所谓, 如果不能和我喜欢的人在一起, 那和谁都一样

对于这两种态度, 处理方式也自然不同.

如果不能进行匹配, 则匹配结果可能不是完美匹配, 因为你喜欢的已经和别人跑了, 而喜欢你的你又拒绝了.

如果是无所谓的态度, 处理流程基本上和上面一致. 比较是不存在的给出一个较大的默认值即可.

应用

那么这个算法可以应用到哪些场景呢? 想了一下, 关于这种意向匹配的场景, 基本上都可以参考此算法, 比如: 相亲、工作分配、大学招生、拍卖等等


别人看综艺, 想从中学到交友之法, 而我看到的却是算法? 唉, 不说了, 刷剧去了.

Gale-Shapley算法

上一篇:hotspot算法实现 <<深入理解Java虚拟机>>


下一篇:如何来理解Python中的字典数据类型