前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Gale-Shapley算法

Gale-Shapley算法

作者头像
烟草的香味
发布2021-09-15 11:14:15
5830
发布2021-09-15 11:14:15
举报
文章被收录于专栏:烟草的香味烟草的香味

前言

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

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

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

单一匹配

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

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

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

代码语言:javascript
复制
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来简单实现一下:

代码语言:javascript
复制
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
    • 若女生有对象, 则从女生的喜欢列表中, 找到这两个男孩. 2n
    • 若重新组队, 则找到女生的当前对象, n
    • 向女生依次表白, n

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

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

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

扩展

人数不匹配

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

喜欢列表不为全部

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

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

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

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

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

应用

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


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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-08-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 烟草的香味 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 单一匹配
  • Gale-Shapley 算法
    • 算法优化
      • 扩展
        • 应用
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档