Gale-Shapley算法
2021/8/26 1:06:15
本文主要是介绍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
在此基础上, 提出了完美匹配和稳定匹配的概念.
完美匹配
完美匹配是指, M
和N
的每个成员, 都恰好出现在R
的一个匹配队列中. 恰好的意思是, 不多不少就一次.
说人话就是: M
和N
中的所有人都配对成功, 不存在落单的男孩或女孩.
稳定匹配
一个完美匹配中如果不存在不稳定因素, 就称为稳定匹配.
我们上面每个人只能喜欢一个人, 现在不是了, 每个人都有一个喜欢程度从高到低的列表.
如果说 m1
在 n1
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
- 向女生依次表白, n
. 很显然, 大 O 时间复杂度为O(n^3)
. 哎? 怎么和之前分析的不一样呢? 很明显, 就差在了第三层. 只要想办法消掉就好啦.
- 找到要表白的女孩. 直接存储女孩的索引即可直接找到.
- 找到女生当前对象同理, 直接存储索引.
- 从女生的喜欢列表中, 找到更喜欢谁? 将数组换成 map, 即可实现
O(1)
的查找. 空间换时间呗.
至此, 第三层的循环全部去掉. 时间复杂度为O(n^2)
. 能不能再降低呢? 我还没有想到.
扩展
人数不匹配
如果男女生人数不一致呢? 那自然是无法得出完美匹配与稳定匹配了, 但是通过上面的步骤, 无论是让人数较少的一方主动选择, 还是人数较多的一方主动选择, 得出的还是一个比较满意的匹配结果. 当然, 因为人数不匹配, 最终是一定有落单的人.
喜欢列表不为全部
如果女生的喜欢列表, 只是部分男生呢? 那么对于未出现在喜欢列表的人有两种情况:
- 十分讨厌, 不能进行匹配
- 无所谓, 如果不能和我喜欢的人在一起, 那和谁都一样
对于这两种态度, 处理方式也自然不同.
如果不能进行匹配, 则匹配结果可能不是完美匹配, 因为你喜欢的已经和别人跑了, 而喜欢你的你又拒绝了.
如果是无所谓的态度, 处理流程基本上和上面一致. 比较是不存在的给出一个较大的默认值即可.
应用
那么这个算法可以应用到哪些场景呢? 想了一下, 关于这种意向匹配的场景, 基本上都可以参考此算法, 比如: 相亲、工作分配、大学招生、拍卖等等
别人看综艺, 想从中学到交友之法, 而我看到的却是算法? 或许, 破案了? 唉, 不说了, 刷剧去了.
这篇关于Gale-Shapley算法的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南