背景
Gale-Shapley算法,又称延迟接受算法,是由经济学家David Gale和数学家Lloyd Shapley于1962年提出的一种用于解决稳定匹配问题的算法。该算法广泛应用于婚姻匹配、学生与学校分配、医生与医院匹配等领域,其核心目标是找到一种稳定且高效的匹配方案。
匈牙利算法,又称Kuhn-Munkres算法或KM算法,是一种用于解决分配问题的高效算法,最初由E. Munkres在20世纪50年代提出。 这种算法主要应用于匹配问题,例如在作业分配、网络优化、任务调度等领域。
人车匹配问题
如上图所示派单匹配场景:
-
乘客1:派给司机1来接3分钟,派给司机2来接8分钟
-
乘客2:派给司机1来接4分钟,派给司机2来接12分钟
-
如果按照距离最近的贪心算法来派单:则乘客1距离司机1最近3分钟,乘客2只剩下司机2最近12分钟,整体接驾时间为3+12=15分钟。
-
如果按照距离全局最优的KM算法来派单:则考虑所有司机和乘客组合后的最优派单距离,最终会得到乘客1派给司机2接驾6分钟,乘客2派给司机1接驾4分钟,整体最优接驾时间为6+4=10分钟。
对比贪心算法和KM算法,明显可以看出KM算法匹配的结果更合理。
匹配问题衍生
KM算法是全局最优匹配,这个最优只体现在当前时空下,一旦把时间和空间的范围进一步放大,这个最优也许并不是真正的最优匹配。
- 时间范围放大:如果当前时间把司机全部派给乘客,下一刻又来了需求,并且正好都在司机附近,这个时候没有司机接单了,当前的最优匹配就影响了未来的最优匹配。
- 空间范围放大:如果订单多司机少时,系统给司机派了距离最近的订单,但是这个订单的目的地在一个偏僻的郊区,导致司机后续接单困难,而其它远的订单目的地在市中心热区,司机可以更快的连续接单。
这些类似的问题,其实都不是一个简单的最优匹配算法就能够解决的,需要借助深度学习算法进一步优化。这也是为什么滴滴打车要搞一个复杂的滴滴大脑来解决这类问题。cloud.tencent.com/developer/a…
本篇不继续深入讨论这个问题,而是换一个研究的方向,讨论Gale-Shapley算法对人车匹配问题的思路。
GS算法和KM算法
应用场景
- GS算法:主要用于解决稳定匹配问题,在人车匹配时,以乘客和司机的个性化意愿进行匹配。先考虑乘客对司机选择的意愿度,再考虑司机对乘客选择的意愿度。
- KM算法:用于解决二分图的最大权匹配问题,在人车匹配时,以系统的全局(收益或者流水)指标最优进行匹配,不针对乘客和司机的个性化意愿度。
算法原理
- GS算法:通过一系列的“求婚”和“拒绝”过程,使两组对象之间形成稳定的匹配关系。男性向女性求婚,女性选择最偏好的求婚者,直到所有男性都被接受。
- KM算法:通过构建代价矩阵和调整顶点的标号,寻找最优匹配。它通过不断寻找增广路径来优化匹配,直到找到最优解。
时间复杂度
- GS算法:时间复杂度为O(n^2),其中n是参与匹配的对象数量。
- KM算法:时间复杂度为O(n^3),适用于处理更复杂的带权匹配问题。
优势和缺点
-
GS算法:
- 优势:确保匹配的稳定性,适用于需要稳定匹配的场景。
- 缺点:不适用于需要考虑权值的匹配问题。
-
KM算法:
- 优势:能够求解最大权匹配问题,适用于需要考虑权值的场景。
- 缺点:计算复杂度较高,对于大规模数据集可能存在性能瓶颈。
适用建议
- GS算法:适用于需要稳定匹配的场景,如婚姻匹配、学校分配等。
- KM算法:适用于需要考虑权值的匹配问题,如资源分配、任务分配等。
代码示例
KM算法: juejin.cn/post/707163…
GS算法
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* @author ****
*/
@Data
public class Person {
// 姓名
public String name;
// 喜好列表
public List<String> preferences;
// 订婚对象
public Person engagedTo;
// 性别,true表示男性,false表示女性
public boolean isMan;
public Person(String name, List<String> preferences, boolean isMan) {
this.name = name;
this.preferences = new ArrayList<>(preferences);
this.isMan = isMan;
this.engagedTo = null;
}
public Person(String woman1, List<String> list) {
this.name = woman1;
this.preferences = new ArrayList<>(list);
this.isMan = false;
}
}
import java.util.*;
import java.util.stream.Collectors;
/**
* @author ***
*/
public class GaleShapleyAlg {
/**
* 实现Gale-Shapley稳定匹配算法。
*
* @param men 男性列表
* @param women 女性列表
*/
public static Map<String, String> stableMatching(List<Person> men, List<Person> women) {
Map<String, Person> womanEngagedTo = createWomanEngagementMap(women);
Queue<Person> freeMen = createFreeMenQueue(men);
while (!freeMen.isEmpty()) {
Person man = freeMen.poll();
processManProposals(man, womanEngagedTo, freeMen);
}
Map<String, String> result = new HashMap<>();
for (Person one : men) {
String manName = one.getName();
String womanName = one.engagedTo.getName();
if (women.stream().anyMatch(w -> womanName.equals(w.getName())
&& manName.equals(w.engagedTo.getName()))) {
result.put(manName, womanName);
}
}
return result;
}
/**
* 创建一个映射,将女性姓名映射到女性对象。
*
* @param women 女性列表
* @return 映射女性姓名到女性对象的HashMap
*/
private static Map<String, Person> createWomanEngagementMap(List<Person> women) {
return women.stream()
.collect(Collectors.toMap(Person::getName, person -> person));
}
/**
* 创建一个包含所有男性的队列。
*
* @param men 男性列表
* @return 包含所有男性的队列
*/
private static Queue<Person> createFreeMenQueue(List<Person> men) {
return new LinkedList<>(men);
}
/**
* 处理男性向女性求婚的过程。
*
* @param man 当前求婚的男性
* @param womanEngagedTo 当前女性已订婚的映射
* @param freeMen 尚未配对的男性队列
*/
private static void processManProposals(Person man, Map<String, Person> womanEngagedTo, Queue<Person> freeMen) {
int currentPreferenceIndex = 0;
// 开始遍历当前男性的偏好列表
while (currentPreferenceIndex < man.preferences.size()) {
String womanName = man.preferences.get(currentPreferenceIndex);
// 获取当前偏好女性的姓名
Person woman = womanEngagedTo.get(womanName);
// 根据女性姓名从映射中获取对应的女性对象
if (woman == null) {
// 如果映射中不存在该女性,则继续遍历下一个偏好
currentPreferenceIndex++;
continue;
}
if (woman.engagedTo != null) {
// 如果女性已订婚
if (preferManMore(woman, man, woman.engagedTo)) {
// 如果女性更偏爱当前男性
rejectCurrentEngagement(woman.engagedTo, freeMen);
// 拒绝当前订婚的男性,并将其加入未配对队列
acceptNewProposal(woman, man, womanEngagedTo);
// 接受当前男性的求婚,并更新订婚状态
break;
// 跳出循环,因为已经成功配对
}
// 如果女性不偏爱当前男性
currentPreferenceIndex++;
// 继续遍历下一个偏好
} else {
// 如果女性未订婚
acceptNewProposal(woman, man, womanEngagedTo);
// 接受当前男性的求婚,并更新订婚状态
break;
// 跳出循环,因为已经成功配对
}
}
}
/**
* 判断女性是否更喜欢当前的求婚者。
*
* @param woman 女性
* @param proposingMan 当前求婚的男性
* @param currentEngagedMan 当前订婚的男性
* @return 如果更喜欢求婚者,则返回true;否则返回false
*/
private static boolean preferManMore(Person woman, Person proposingMan, Person currentEngagedMan) {
// 获取当前订婚男性在女性偏好列表中的索引
int womanPrefIndexForCurrentEngagedMan = woman.preferences.indexOf(currentEngagedMan.name);
// 获取当前求婚男性在女性偏好列表中的索引
int womanPrefIndexForProposingMan = woman.preferences.indexOf(proposingMan.name);
// 如果当前求婚男性的索引小于当前订婚男性的索引,则表示女性更喜欢当前求婚者
return womanPrefIndexForProposingMan < womanPrefIndexForCurrentEngagedMan;
}
/**
* 处理当前订婚被拒绝的情况。
*
* @param rejectedMan 被拒绝的男性
* @param freeMen 尚未配对的男性队列
*/
private static void rejectCurrentEngagement(Person rejectedMan, Queue<Person> freeMen) {
rejectedMan.engagedTo = null;
freeMen.add(rejectedMan);
}
/**
* 接受新的求婚并更新订婚状态。
*
* @param woman 接受求婚的女性
* @param proposingMan 求婚的男性
* @param womanEngagedTo 当前女性已订婚的映射
*/
private static void acceptNewProposal(Person woman, Person proposingMan, Map<String, Person> womanEngagedTo) {
womanEngagedTo.put(woman.name, woman);
woman.engagedTo = proposingMan;
proposingMan.engagedTo = woman;
}
}