Gale-Shapley算法与Kuhn-Munkras算法

516 阅读7分钟

背景

Gale-Shapley算法,又称延迟接受算法,是由经济学家David Gale和数学家Lloyd Shapley于1962年提出的一种用于解决稳定匹配问题的算法。该算法广泛应用于婚姻匹配、学生与学校分配、医生与医院匹配等领域,其核心目标是找到一种稳定且高效的匹配方案。

匈牙利算法,又称Kuhn-Munkres算法或KM算法,是一种用于解决分配问题的高效算法,最初由E. Munkres在20世纪50年代提出。 这种算法主要应用于匹配问题,例如在作业分配、网络优化、任务调度等领域。

人车匹配问题

image.png

如上图所示派单匹配场景:

  • 乘客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;  
    }
  
}