Gale-Shapley算法

前言

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

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

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

单一匹配

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

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

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

package mainimport "fmt"type people struct {Name       string // 名字LikePeople string // 喜欢的人名
}type lovers struct {Boy  peopleGirl people
}func main() {// 分别构造男女队列boyArr := []people{}girlArr := []people{}// 分别对他们进行匹配var result []loversfor _, 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 maintype people struct {Name        string   // 名字LikePeople  []string // 喜好列表CurrentLike int      // 后面算法记录当前表白对象时使用Friend      string   // 当前匹配对象
}func main() {// 分别构造男女队列boyArr := []*people{}girlArr := []*people{}for true {// 找到一个没有对象, 且未全部表白的男生var searchBoy *peoplefor _, boy := range boyArr {if boy.Friend != "" { // 当前男孩已经有对象了continue}// 男孩向所有女生表白过了if boy.CurrentLike >= len(boy.LikePeople) {continue}searchBoy = boybreak}if searchBoy == nil { // 已经全部有对象了, 结束break}// 男生向女生依次表白var i intfor 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.NamesearchBoy.Friend = girl.Namebreak} 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.NamesearchBoy.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. 无所谓, 如果不能和我喜欢的人在一起, 那和谁都一样

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

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

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

应用

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


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/508373.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

阿里云定时任务并自动释放

前言 最近写了一个爬虫脚本, 脚本跑在一台北京的 ecs 上. 但奈何因某种未知力量, 需要连接代理才能访问目标网站. 本来想着自己搭代理, 但是太贵了, 就暂时搁置了. 直到我发现了这个: 阿里云香港的服务器, 一个小时才5分钱. 如果脚本直接跑在香港服务器上不就可以了咩, 按照这…

智能优化算法应用:基于金豺算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于金豺算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于金豺算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.金豺算法4.实验参数设定5.算法结果6.参考文献7.MA…

pixiv小控件

前言 最近看到一个大佬, 开源了一款博客小插件, 地址. 可以将pixiv网站的日榜放到博客侧边栏. 看上去很炫酷. 于是我也引入到了自己的博客中. 在此向大佬表示感谢. 但是在使用过程中, 经常遇到访问很慢的情况, 查看之后才发现, 大佬的服务器架设在韩国, 难怪访问比较慢, 都走…

PHP-PDO参数绑定问题

前言 今天在执行这样一段代码: $data [username > hujingnb,address > beijing, ]; $dbh new PDO("mysql:host{$host};dbname{$dbname}", $username, $password); $statement $dbh->prepare(INSERT INTO test_user (username, address) VALUES (:usern…

Python 的协程

前言 最近在看部分Python源码时, 发现了async 这个关键字. 查了一下发现了Python中的协程. 协程这玩意, 在GO中我用过啊, 简单说, 就是一个轻量级的线程嘛, 由语言自己来实现不同协程的调度. 想着Python中可能也是差不多的东西吧. 但是我Google搜了一下, 前面的说明都给出了下…

PHP脚本调用命令获取实时输出

在写脚本的时候, 经常会有需要调用其他命令. 而在调用一些耗时命令的时候, 我们是需要能够实时掌握脚本进度的. 一般来说, 脚本的进度通常是通过脚本的输出来获得. 如果是一个bash脚本, 那么直接调用命令 A就可以将执行权交出去, 然后命令 A的输出就可以实时显示出来了. 如果…

如何使用git管理crontab任务

前言 在Linux系统上执行定时任务, 使用crontab还是很方便的(有关crontab的使用可看crontab指令笔记). 只需要一行命令就完成了. 但是, 美中不足的是, crontab通过命令行管理任务, 无法通过代码库对任务进行管理. 若要更换机器, 所有任务都要重新增加一遍. 更糟的是若服务器突…

Golang 反射操作整理

前言 反射是什么? 我们平常也是经常用到, 而且这名词都用烂了, 这里就不再详细介绍了. 简单说, 就是有一个不知道是什么类型的变量, 通过反射可以获取其类型, 并可操作属性和方法. 反射的用途一般是用作生成工具方法, 比如你需要一个ToString方法, 要将变量转为字符串类型,…

Wordpress不同页面显示不同小工具

问题 想做一个在右侧显示的文章目录, 使用文章目录的插件 Easy Table of Contents, 将其添加到右侧的侧边栏中, 很轻松做到了这点. 但是, 一个新的问题出现了. 这个目录的工具, 需要在文章页面显示, 而在其他页面不显示. 那么问题来了, 如何让不同的页面显示不同的侧边栏工具…

虚拟内存分页机制的地址映射

概述 在之前的文章虚拟内存对分页机制做了简单的介绍. 还有一个疑问, 那就是如何将虚存中的逻辑地址映射为物理地址呢? 今天就来简单分析一下. 对于一个分页的地址来说, 一般包含两个元素: 页号: 第几页偏移量: 当前页的第几个字节 以下以 addr_virtual(p, o)表示一个逻辑…

虚拟内存分页机制的页面置换

前言 之前简单介绍过虚拟内存是如何与物理内存进行地址映射的: 虚拟内存分页机制的地址映射, 但是仅仅地址映射是不够的, 在地址映射说过会有缺页的情况, 此时就需要操作操作系统将缺少的页加载到内存中. 但是, 如果内存满了怎么办呢? 毕竟虚拟内存一般都要大于物理内存的, 不…

Kubernetes各个组件的概念

前言 Kubernetes中的概念太多了, 什么Pod Service Deployment 等等等等, 给刚接触的我都整蒙了. 通过几天观察下来, 说一下我对各个组件的理解. 此文章仅仅对这些概念做一个简单的介绍, 不至于后面看其他文章的时候一头雾水. Node Node很好理解. 就是服务实际运行的实例, 可…

Kubernetes中Pod生命周期

在 Kubernetes中Pod是容器管理的最小单位, 有着各种各样的Pod管理器. 那么一个Pod从启动到释放, 在这期间经历了哪些过程呢? Pod自开始创建, 到正常运行, 再到释放, 其时间跨度及经历的阶段大致如下: 说一下各个阶段的作用以及是为了解决什么问题. 容器调度和下载镜像的过程就…

Kubernetes存储卷的使用

在Kubernetes中, 有这不同方式的内容挂载, 简单记录一下他们的配置方式. ConfigMap 配置内容 内容配置 apiVersion: v1 kind: ConfigMap metadata:name: test-config data: # 添加配置的 key-value 内容test-key: test-value引入 apiVersion: v1 kind: Pod spec: containe…

响应HTTP服务的shell脚本

以下内容均为第一版, 实际使用请查看最新信息, 转至: https://hujingnb.com/archives/729 前言 兄弟萌, 我实现了一个实用的小工具, 特来分享. 事情刚开始是这样的, 我需要一个脚本来实现代码仓库web hook的任务, 首先想到的是直接调用php, 但是php-fpm是以www-data用户运行…

wait函数的作用

前言 在编写C程序的时候, 通过fork函数来创建新的进程, wait函数来等待子进程结束. 那么就有一个问题了, 什么情况下父进程需要等待子进程结束后继续执行呢? 如果需要等待子进程结束, 那直接将操作放到父进程执行不就醒了么? 反正等着也是等着. 当然, 还有有一种情况, 任务…

OAuth1.0介绍

背景 为什么需要OAuth授权呢? 最典型的应用场景就是第三方登录了, 我们开发了一个网站希望用户可以QQ登录, 但是怎么能拿到用户的 QQ 信息呢? 用户将 账号密码告诉我们当然可以, 但是这样有如下隐患: 我们拿到了用户的密码, 这样很不安全. 而且任意一个应用被黑, 所有相关…

PHP 数组的内部实现

前言 这几天在翻github的时候, 碰巧看到了php的源码, 就 down 下来随便翻了翻. 地址: https://github.com/php/php-src 那么PHP中什么玩意最引人注目嘞? 一定是数组了, PHP中的数组太强大了, 于是就想着不如进去看看数组的实现部分. 这篇文章打算全程针对代码进行解读了. 以…

go1.18新特性

前言 最近突然发现golang更新版本1.18了, 于是迫不及待的来看看这个版本加了些什么新特性. 没准就有之前困扰很久的问题, 在新版本被官方解决了呢. 先简单概述一下都有些什么变化, 后面再细说: 增加泛型的支持系统库方法增加修复 bug 另外, 像"系统内核更新"这种…

base64编码原理

引出 众所周知, ASICC编码共127个, 使用了7个bit进行编码. 而文件在存储的时候是以 字节为单位, 也就是8bit. 这就难免导致有一部分编码是没有定义在ASICC编码中的. 而在网络中传输二进制数据的时候(字符串本质上也是二进制数据嘛), 如果直接传输比特流, 倒也不是不可以, 只是…