问题描述
有n个男人和n个女人,其中每个男人心里对每个女人有一个优先排序,同样每个女人心里对每个男人也有一个优先排序,这个排序不能出现并列的情况。那么能不能提供这样的配对,对于每个男人M,和不是男人M配对的女人W,至少满足下面两种情况之一:
- 男人M对于他当前配对的女人W′,比女人W更满意
- 女人W对于她当前配对的男人M′,比男人M更满意
问题形式化
上面的问题,我们可以通过数学的表达方式来使问题的本质更加清晰:
考虑n个男人的集合 M={m₁, m₂, ···, mn} 和n个女人的集合 W= {w₁, w₂, ···, wn} .令 M X W 表示所有可能的形如 (m, w) 的有序对的集合,其中 m ∈ M, w ∈ W. 一个匹配S 是来自M X W 的有序对的集合,并且具有下述属性:每个 M 的成员和每个 W 的成员至多出现在 S 的一个有序对中. 一个完美匹配 S′ 是具有下述性质的匹配: M 的每个成员和 W 的每个成员恰好出现在 S′ 的一个对里。
由匹配和完美匹配的定义我们可以得出如下结论:
- 匹配表示一个男人(女人)可以单身,也可以和一个女人(男人)结婚,但是不能和多个女人(男人)结婚
- 完美匹配表示一个男人(女人)只能和一个女人(男人)结婚,不能是单身,也不能和多个女人(男人)结婚
我们在这个背景下增加优先的概念。每个男人 m ∈ M 对所有的女人排名,如果 m 给 w 的排名高于 w′,我们就说 m 偏爱 w 超过 w′ 。我们将把 m 的按顺序的排名作为他的优先表,但是不允许排名中出现并列。类似的,每个女人也对所有的男人排名。
稳定匹配
给定一个完美匹配S,它可能出错吗?假如在 S 中存在两个对 (m, w) 和 (m′, w′),它们具有 m 更偏爱 w′ 而不爱 w ,且w′ 更偏爱 m 而不爱 m′ 的性质。在这种情况下,没有什么能阻止 m 和 w′ 放弃他们当前的伴侣并且转过来走在一起,这个婚姻的集合不再是自强化的。
我们说这样的对 (m, w′) 是一个相对于 S 的不稳定因素:(m, w′) 不属于S,但是 m 和 w′ 双方都偏爱另一方而不爱他们在 S 中的伴侣。
那么我们的目标就是一个不含有不稳定因素的婚姻集合。一个稳定匹配S 要满足下面的两个条件
- 匹配 S 是完美匹配
- 不存在相对于 S 的不稳定因素
我们举两个例子来说明,首先举一个最简单的例子
假如我们有两个男人的集合 {m, m′} 和两个女人的集合 {w, w′},优先表如下:
- m 更偏爱 w 而不爱 w′
- m′ 更偏爱 w 而不爱 w′
- w 更偏爱 m 而不爱 m′
- w′ 更偏爱 m 而不爱 m′
从直观上考虑这组优先表,它描述了完全的一致性:男人在女人的顺序上一致,且女人在男人的顺序上也一致。存在一个由 (m ,w) 和 (m′, w′)对组成的唯一的稳定匹配。另一个由 (m, w′) 和 (m′, w) 组成的完美匹配不是稳定匹配,因为 (m, w) 对构成了相对于这个匹配的不稳定因素( m 和 w 双方都想离开他们各自的伴侣而组成一对)。
第二个例子稍微复杂一些,假设优先表是:
- m 更偏爱 w 而不爱 w′
- m′ 更偏爱 w′ 而不爱 w
- w 更偏爱 m′ 而不爱 m
- w′ 更偏爱 m 而不爱 m′
这种情况下,两个男人的优先表完全互相协调,他们把不同的女人排位第一。两个女人的优先表同样完全互相协调。但是男人的优先表与女人的优先表完全冲突。此时,存在两个不同的稳定匹配。由 (m ,w) 和 (m′, w′) 组成的是稳定匹配,因为两个男人已是最满意了,因此没有人想离开他们匹配的伴侣。而由 (m′, w) 和 (m, w′) 组成的匹配也是稳定匹配,因为两个女人也是最满意的。
因此,对于同样的优先表可能有多于一个的稳定匹配
Gale-Shapley 算法
伪代码如下:
初始化所有的 m ∈ M 和 w ∈ W 都是自由的;
while (存在男人m是自由的且还没对每个女人都求过婚) {
选择这样一个男人 m;
令 w 是 m 的优先表中 m 还没有求过婚的最高排名的女人;
if (w 是自由的) {
(m, w)变成约会状态;
} else {
令 m′ 是 w 的当前约会对象;
if (w 更偏爱 m′ 而不爱 m ) {
m 保持自由;
} else{
(m, w)变成约会状态;
m′ 变成自由;
}
}
}
输出已约会对的集合S
算法的一些命题和定理,证明方法就不多说了
命题 1.1 w从接受对她的第一次求婚开始保持约会状态,且她正在约会的一系列伴侣变得越来越好
命题 1.2 m求过婚的一系列女人变得越来越差
定理 1.3 G-S算法在至多n²次While循环的迭代之后终止
命题 1.4 如果m在算法执行的某点是自由的,那么存在一个他还没有向她求过婚的女人
命题 1.5 算法终止时返回的集合S是一个完美匹配
定理 1.6 考虑G-S算法的一次执行,它返回的一个对的集合S,集合S是一个稳定匹配
定理 1.7 G-S算法的每次执行都得到集合 S*
Java代码对于G-S算法的实现
首先列出算法实现所需要的类以及介绍
- IGSMatchElement接口:需要匹配的元素,相当于男人或女人
- IGSPriorityTable接口:匹配元素的优先表
- IGSMatcher接口:配对器,G-S算法的配对过程
- GSMatchElement类:实现了IGSMatchElement接口
- GSPriorityTable类:实现了IGSPriorityTable接口
- GSMatcher类:实现了IGSMatcher接口
- GSException类:G-S算法中抛出的异常
- GSPair类:配对成功的输出类
首先看一下配对元素的接口和类:
配对元素类呢,主要维护四个东西:
- 唯一标识符,用于判断是否是同一个对象。比如男人或女人的名字
- 优先级表
- 当前已配对的元素,对男人来说就是配对的女人,对女人来说就是配对的男人
- 已经求过婚的元素集合,这个是针对求婚方的男人来说的,这个集合存放这个男人当前已经求过婚的女人
IGSMatchElement接口
/**
* 配对元素
*/
public interface IGSMatchElement {
/**
* 唯一标识符
*/
String identifier();
/**
* 得到优先级表
*/
IGSPriorityTable getPriorityTable();
/**
* 当前已配对的另一方元素
* @return
*/
IGSMatchElement getAnotherMatchedElement();
/**
* 设置已配对的另一方元素
* @param anotherMatchedElement
*/
void setAnotherMatchedElement(IGSMatchElement anotherMatchedElement);
/**
* 尝试过配对的另一方元素
* @return
*/
List hasTryMatchAnotherElementList();
}
GSMatchElement类
public class GSMatchElement implements IGSMatchElement {
private String identifier;
private IGSPriorityTable priorityTable = new GSPriorityTable();
private IGSMatchElement anotherMatchedElement;
private List hasTryMatchAnotherElementList = new ArrayList<>();
public GSMatchElement(String identifier) {
this.identifier = identifier;
}
@Override
public String identifier() {
return identifier;
}
@Override
public IGSPriorityTable getPriorityTable() {
return priorityTable;
}
@Override
public IGSMatchElement getAnotherMatchedElement() {
return anotherMatchedElement;
}
@Override
public void setAnotherMatchedElement(IGSMatchElement anotherMatchedElement) {
this.anotherMatchedElement = anotherMatchedElement;
}
@Override
public List hasTryMatchAnotherElementList() {
return hasTryMatchAnotherElementList;
}
/**
* 必须要重写equal方法,去重
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof GSMatchElement)) {
return false;
}
GSMatchElement other = (GSMatchElement) obj;
if (identifier == null || other.identifier == null) {
return false;
}
return identifier.equals(other.identifier);
}
}
然后来看一下优先级表的接口和类:
优先级表的功能比较简单,就是维护了一个对方配对元素的集合。
IGSPriorityTable接口
/**
* 优先表
*/
public interface IGSPriorityTable {
/**
* 得到优先表所有元素的集合
* @return
*/
List getPriorityElementList();
/**
* 添加元素,注意去重
* @param element
*/
void add(IGSMatchElement element);
/**
* 批量添加,注意去重
* @param elements
*/
void addAll(List elements);
}
GSPriorityTable类
public class GSPriorityTable implements IGSPriorityTable {
private List elements = new ArrayList<>();
@Override
public List getPriorityElementList() {
return elements;
}
@Override
public void add(IGSMatchElement element) {
//这里要进行除重
if (elements.contains(element)) {
return;
}
elements.add(element);
}
@Override
public void addAll(List elements) {
for (IGSMatchElement element : elements) {
add(element);
}
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("[");
for (IGSMatchElement element : elements) {
sb.append(element.identifier()).append(" > ");
}
sb.setLength(sb.length() - 1);
sb.append("]");
return sb.toString();
}
}
然后就是配对过程中可能抛出的异常,这个就是对Exception的一个简单包装
GSException类
public class GSException extends Exception {
public GSException() {
}
public GSException(String message) {
super(message);
}
}
然后就是配对结果的包装类,也很简单,就是包装了一个男人元素和一个女人元素
GSPair类
/**
* 已配对成功
*/
public class GSPair {
private IGSMatchElement element1;
private IGSMatchElement element2;
public GSPair() {
}
public GSPair(IGSMatchElement element1, IGSMatchElement element2) {
this.element1 = element1;
this.element2 = element2;
}
public IGSMatchElement getElement1() {
return element1;
}
public void setElement1(IGSMatchElement element1) {
this.element1 = element1;
}
public IGSMatchElement getElement2() {
return element2;
}
public void setElement2(IGSMatchElement element2) {
this.element2 = element2;
}
@Override
public String toString() {
return "{" + element1.identifier() + ", " + element2.identifier() + "}";
}
}
最后就是我们最重要的配对器相关的代码了
IGSMatcher接口,很简单,就只有一个配对功能
/**
* 配对器
*/
public interface IGSMatcher {
/**
* 开始配对
* @return
*/
List match() throws GSException;
}
然后是GSMatcher类。这里的配对过程完全是按照G-S算法的伪代码实现的,唯一不同的地方就是多了入参数量的校验:
- 强制要求男人和女人的数量一致
- 强制要求男人或女人的优先表的容量和男人或女人的数量一致
public class GSMatcher implements IGSMatcher {
private List leftElementList;
private List rightElementList;
public GSMatcher(List leftElementList,
List rightElementList) throws GSException {
this.leftElementList = leftElementList;
this.rightElementList = rightElementList;
check();
}
private void check() throws GSException {
// 检查配对元素
checkGSMatchElement(leftElementList);
checkGSMatchElement(rightElementList);
// 检查优先表
checkGsPriorityTable(leftElementList, rightElementList);
checkGsPriorityTable(rightElementList, leftElementList);
}
private void checkGSMatchElement(List elements) throws GSException {
if (elements == null || elements.isEmpty()) {
throw new GSException("leftElementList is null or empty");
}
for (IGSMatchElement element : elements) {
if (element.hasTryMatchAnotherElementList() == null) {
throw new GSException("tryMatchAnotherElementList cannot be null");
}
}
}
private void checkGsPriorityTable(List one, List two) throws GSException {
for (IGSMatchElement element : one) {
IGSPriorityTable priorityTable = element.getPriorityTable();
if (priorityTable == null) {
throw new GSException("exist null GSPriorityTable in some GSMatchElement");
}
if (priorityTable.getPriorityElementList() == null || priorityTable.getPriorityElementList().isEmpty()) {
throw new GSException("exist empty GSMatchElement in some GSPriorityTable");
}
if (priorityTable.getPriorityElementList().size() != two.size()) {
throw new GSException("GSPriorityTable's size must be equal to another GSMatchElement's size");
}
}
}
@Override
public List match() {
init();
while (canContinue()) {
IGSMatchElement left = findLeft();
IGSMatchElement right = findRight(left);
if (right == null) {
continue;
}
// 添加到尝试配对的集合
left.hasTryMatchAnotherElementList().add(right);
// 右边未配对
if (right.getAnotherMatchedElement() == null) {
//配对
left.setAnotherMatchedElement(right);
right.setAnotherMatchedElement(left);
} else {
//判断是否重新配对
if (canRetryMatch(left, right)) {
// 重新配对
IGSMatchElement left2 = right.getAnotherMatchedElement();
left2.setAnotherMatchedElement(null);
left.setAnotherMatchedElement(right);
right.setAnotherMatchedElement(left);
}
}
}
List gsPairs = new ArrayList<>();
for (IGSMatchElement left : leftElementList) {
if (left.getAnotherMatchedElement() != null) {
gsPairs.add(new GSPair(left, left.getAnotherMatchedElement()));
}
}
return gsPairs;
}
private void init() {
for (IGSMatchElement element : leftElementList) {
element.setAnotherMatchedElement(null);
element.hasTryMatchAnotherElementList().clear();
}
for (IGSMatchElement element : rightElementList) {
element.setAnotherMatchedElement(null);
element.hasTryMatchAnotherElementList().clear();
}
}
private boolean canContinue() {
return findLeft() != null;
}
private IGSMatchElement findLeft() {
if (leftElementList.isEmpty()) {
return null;
}
for (IGSMatchElement element : leftElementList) {
// 未配对
if (element.getAnotherMatchedElement() == null) {
List hasTryMatchElements = element.hasTryMatchAnotherElementList();
List priorityElementList = element.getPriorityTable().getPriorityElementList();
// 没有将优先表的对方全部尝试过
for (IGSMatchElement priorityElement : priorityElementList) {
if (!hasTryMatchElements.contains(priorityElement)) {
return element;
}
}
}
}
return null;
}
private IGSMatchElement findRight(IGSMatchElement left) {
List priorityElementList = left.getPriorityTable().getPriorityElementList();
List hasTryMatchElements = left.hasTryMatchAnotherElementList();
for (IGSMatchElement priorityElement : priorityElementList) {
if (hasTryMatchElements.contains(priorityElement)) {
continue;
}
return priorityElement;
}
return null;
}
private boolean canRetryMatch(IGSMatchElement left, IGSMatchElement right) {
IGSMatchElement left2 = right.getAnotherMatchedElement();
List priorityElementList = right.getPriorityTable().getPriorityElementList();
return priorityElementList.indexOf(left) < priorityElementList.indexOf(left2);
}
}
测试
测试相关的代码,这里用随机数的方法把另一方配对元素打乱顺序放入每个人的优先表中
private void testGSMatcher() {
List leftElements = new ArrayList<>();
List rightElements = new ArrayList<>();
// 初始化男人
IGSMatchElement left1 = new GSMatchElement("男人1");
IGSMatchElement left2 = new GSMatchElement("男人2");
IGSMatchElement left3 = new GSMatchElement("男人3");
IGSMatchElement left4 = new GSMatchElement("男人4");
IGSMatchElement left5 = new GSMatchElement("男人5");
leftElements.add(left1);
leftElements.add(left2);
leftElements.add(left3);
leftElements.add(left4);
leftElements.add(left5);
// 初始化女人
IGSMatchElement right1 = new GSMatchElement("女人1");
IGSMatchElement right2 = new GSMatchElement("女人2");
IGSMatchElement right3 = new GSMatchElement("女人3");
IGSMatchElement right4 = new GSMatchElement("女人4");
IGSMatchElement right5 = new GSMatchElement("女人5");
rightElements.add(right1);
rightElements.add(right2);
rightElements.add(right3);
rightElements.add(right4);
rightElements.add(right5);
initPriorityTable(left1, rightElements);
initPriorityTable(left2, rightElements);
initPriorityTable(left3, rightElements);
initPriorityTable(left4, rightElements);
initPriorityTable(left5, rightElements);
initPriorityTable(right1, leftElements);
initPriorityTable(right2, leftElements);
initPriorityTable(right3, leftElements);
initPriorityTable(right4, leftElements);
initPriorityTable(right5, leftElements);
StringBuffer sb = new StringBuffer();
for (IGSMatchElement element : leftElements) {
sb.append(element.identifier()).append(" 的优先表:").append(element.getPriorityTable().toString()).append("\n");
}
sb.append("\n");
for (IGSMatchElement element : rightElements) {
sb.append(element.identifier()).append(" 的优先表:").append(element.getPriorityTable().toString()).append("\n");
}
sb.append("\n");
try {
IGSMatcher matcher = new GSMatcher(leftElements, rightElements);
List gsPairs = matcher.match();
for (GSPair pair : gsPairs) {
sb.append(pair.toString()).append("\n");
}
setText(sb.toString());
} catch (GSException e) {
e.printStackTrace();
Toast.makeText(this, "数据格式有误", Toast.LENGTH_SHORT).show();
}
}
private void initPriorityTable(IGSMatchElement element, List anotherElementList) {
List remainElementList = new ArrayList<>(anotherElementList);
// 因为这里添加可能会被去重,可以要循环添加
while (!remainElementList.isEmpty()) {
IGSMatchElement elementDest = remainElementList.get(new Random().nextInt(remainElementList.size()));
element.getPriorityTable().add(elementDest);
remainElementList.remove(elementDest);
}
}
因为用了随机数来设置优先级表,因此每次运行的结果都不会一样,某一次的测试数据为:
男人1 的优先表:[女人1 > 女人4 > 女人5 > 女人3 > 女人2 >]
男人2 的优先表:[女人5 > 女人3 > 女人1 > 女人4 > 女人2 >]
男人3 的优先表:[女人4 > 女人3 > 女人2 > 女人5 > 女人1 >]
男人4 的优先表:[女人3 > 女人4 > 女人2 > 女人5 > 女人1 >]
男人5 的优先表:[女人1 > 女人3 > 女人4 > 女人2 > 女人5 >]
女人1 的优先表:[男人5 > 男人2 > 男人1 > 男人3 > 男人4 >]
女人2 的优先表:[男人4 > 男人5 > 男人2 > 男人3 > 男人1 >]
女人3 的优先表:[男人5 > 男人4 > 男人3 > 男人2 > 男人1 >]
女人4 的优先表:[男人2 > 男人4 > 男人5 > 男人3 > 男人1 >]
女人5 的优先表:[男人3 > 男人5 > 男人2 > 男人4 > 男人1 >]
{男人1, 女人2}
{男人2, 女人5}
{男人3, 女人4}
{男人4, 女人3}
{男人5, 女人1}
按照G-S算法走一遍,发现是正确的
G-S算法添加黑名单
假如设定某些对(m, w)不能匹配,比如某个男人因为一些原因不能和某个女的配对。
注意此时的稳定匹配就不一定是完美匹配了。
此时只需要修改G-S算法的一处地方
while (存在男人m是自由的且还没对每个女人都求过婚)
修改为
while (存在男人m是自由的且还没对每个女人w都求过婚,其中(m, w) 不属于黑名单F中的对)
修改后的伪代码是
初始化所有的 m ∈ M 和 w ∈ W 都是自由的;
while (存在男人m是自由的且还没对每个女人w都求过婚,其中(m, w) 不属于黑名单F中的对) {
选择这样一个男人 m;
令 w 是 m 的优先表中 m 还没有求过婚的最高排名的女人;
if (w 是自由的) {
(m, w)变成约会状态;
} else {
令 m′ 是 w 的当前约会对象;
if (w 更偏爱 m′ 而不爱 m ) {
m 保持自由;
} else{
(m, w)变成约会状态;
m′ 变成自由;
}
}
}
输出已约会对的集合S
用java代码实现,首先添加一个黑名单的类
/**
* 匹配黑名单
*/
public class GSBlackList {
private List blackList = new ArrayList<>();
public void addBlackList(IGSMatchElement left, IGSMatchElement right) {
if (left == null || right == null) {
return;
}
blackList.add(new GSPair(left, right));
}
public boolean contain(IGSMatchElement left, IGSMatchElement right) {
for (GSPair pair : blackList) {
if (pair.getElement1().equals(left) && pair.getElement2().equals(right)) {
return true;
}
}
return false;
}
}
IGSMatcher接口添加黑名单的添加操作
/**
* 配对器
*/
public interface IGSMatcher {
/**
* 添加匹配黑名单
*/
void addBlackList(IGSMatchElement left, IGSMatchElement right);
/**
* 开始配对
* @return
*/
List match() throws GSException;
}
GSMatcher类添加黑名单的变量,在构造函数中实例化
// 匹配黑名单
private GSBlackList blackList;
public GSMatcher(List leftElementList,
List rightElementList) throws GSException {
this.leftElementList = leftElementList;
this.rightElementList = rightElementList;
// 实例化黑名单
blackList = new GSBlackList();
check();
}
实现IGSMatcher接口的新增的方法
@Override
public void addBlackList(IGSMatchElement left, IGSMatchElement right) {
blackList.addBlackList(left, right);
}
findLeft方法添加对黑名单的判断
private IGSMatchElement findLeft() {
if (leftElementList.isEmpty()) {
return null;
}
for (IGSMatchElement element : leftElementList) {
// 未配对
if (element.getAnotherMatchedElement() == null) {
List hasTryMatchElements = element.hasTryMatchAnotherElementList();
List priorityElementList = element.getPriorityTable().getPriorityElementList();
// 没有将优先表的对方全部尝试过
for (IGSMatchElement priorityElement : priorityElementList) {
// 添加黑名单机制
if (!hasTryMatchElements.contains(priorityElement) && !blackList.contain(element, priorityElement)) {
return element;
}
}
}
}
return null;
}
最后在测试代码最后,实例化GSMatcher后,添加黑名单配置
try {
IGSMatcher matcher = new GSMatcher(leftElements, rightElements);
//添加黑名单
matcher.addBlackList(left1, right3);
matcher.addBlackList(left3, right2);
List gsPairs = matcher.match();
for (GSPair pair : gsPairs) {
sb.append(pair.toString()).append("\n");
}
setText(sb.toString());
} catch (GSException e) {
e.printStackTrace();
Toast.makeText(this, "数据格式有误", Toast.LENGTH_SHORT).show();
}