From 9de5a024ec3a826f54814486ee19d0bca68fea19 Mon Sep 17 00:00:00 2001 From: TimotheeChauvin Date: Fri, 18 Jan 2019 20:27:44 +0100 Subject: [PATCH] add the Gale Shapley algorithm (with a test) --- .../matchings/stableMatching/GaleShapley.java | 103 +++++++++++++++ .../stableMatching/GaleShapleyTest.java | 125 ++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 src/main/java/com/matchings/stableMatching/GaleShapley.java create mode 100644 src/test/java/com/matchings/stableMatching/GaleShapleyTest.java diff --git a/src/main/java/com/matchings/stableMatching/GaleShapley.java b/src/main/java/com/matchings/stableMatching/GaleShapley.java new file mode 100644 index 00000000..3c60e836 --- /dev/null +++ b/src/main/java/com/matchings/stableMatching/GaleShapley.java @@ -0,0 +1,103 @@ +package src.main.java.com.matchings.stableMatching; + +public class GaleShapley { + + /** + * Return a stable matching between men and women according to their preferences, + * following the Gale-Shapley algorithm. + * + * @param menPrefs for each man, for each preference rank, the corresponding woman + * @param womenPrefs for each woman, for each preference rank, the corresponding man + * @return for each man, the associated woman (1-dimensional array) + */ + + public int[] GaleShapleyStableMarriage(int[][] menPrefs, int[][] womenPrefs) { + assert menPrefs.length == womenPrefs.length; // there are n individuals in each group + + int n = menPrefs.length; + if (n == 0) + return new int[0]; // handle empty initial conditions right away + + // Some implementation details: men, women and preference ranks are all labeled + // from 0 to n-1. + + // menMatching: for each man, the woman to whom he is engaged (or -1 if not engaged): + int[] menMatching = new int[n]; + // the same for women: + int[] womenMatching = new int[n]; + + // asked: for each man, highest rank asked (between 0 and n-1; + // -1 if hasn't asked anyone yet) + int[] asked = new int[n]; + + // Initialize all values of menMatching and womenMatching to -1, + // otherwise woman 0 will be considered engaged to all men and idem for man 0. + // Do the same for asked, otherwise each man will be considered as having + // already asked his first choice. + for (int i = 0; i < n; i++) { + menMatching[i] = -1; + womenMatching[i] = -1; + asked[i] = -1; + } + + // to quickly retrieve the rank of men for a given woman, we create womenRanks. + // For each woman, the array is: + // index: man; value: rank + // whereas in womenPrefs it was index: rank; value: man + // Retrieving a rank will be done be simply looking up womenRanks[woman][man] + int[][] womenRanks = new int[n][n]; + int man; + for (int w = 0; w < n; w++) { + for (int rank = 0; rank < n; rank++) { + man = womenPrefs[w][rank]; + womenRanks[w][man] = rank; + } + } + + int unengaged = 0; // at first all men are unengaged, we take the first one + int notAsked; // first rank not asked by unengaged + int prefWoman; // for the considered man, preferred woman among not asked ones + int currentManPartner; // for the considered woman, current partner (-1 if none) + while (unengaged != -1) { // while there is an unengaged man + // unengaged is our proposing man. + notAsked = asked[unengaged] + 1; + prefWoman = menPrefs[unengaged][notAsked]; + currentManPartner = womenMatching[prefWoman]; + // now unengaged asks prefWoman for engagement. + asked[unengaged] += 1; + if (currentManPartner == -1) { // prefWoman is not engaged: the two engage + menMatching[unengaged] = prefWoman; + womenMatching[prefWoman] = unengaged; + unengaged = getUnengaged(menMatching); // we need a new unengaged + } else { // prefWoman is engaged to currentManPartner (therefore >= 0) + if (womenRanks[prefWoman][unengaged] < womenRanks[prefWoman][currentManPartner]) { + // prefWoman prefers unengaged: split prefWoman and currentManPartner + menMatching[currentManPartner] = -1; + // and engage prefWoman and unengaged + menMatching[unengaged] = prefWoman; + womenMatching[prefWoman] = unengaged; + unengaged = getUnengaged(menMatching); // we need a new unengaged + } + // If prefWoman prefers currentManPartner over unengaged, nothing happens + // (except that asked[unengaged] has been incremented so unengaged won't ask + // prefWoman for engagement anymore). + } + } + return menMatching; + } + + /** + * Get a currently unengaged man, if there is one + * + * @param menMatching the current men matching array (being constructed) + * @return the first man that is not engaged, or -1 if all men are engaged + */ + + public int getUnengaged(int[] menMatching) { + for (int m = 0; m < menMatching.length; m++) { + if (menMatching[m] == -1) + return m; + } + return -1; + } +} \ No newline at end of file diff --git a/src/test/java/com/matchings/stableMatching/GaleShapleyTest.java b/src/test/java/com/matchings/stableMatching/GaleShapleyTest.java new file mode 100644 index 00000000..2e7d204f --- /dev/null +++ b/src/test/java/com/matchings/stableMatching/GaleShapleyTest.java @@ -0,0 +1,125 @@ +package src.test.java.com.matchings.stableMatching; + +import src.main.java.com.matchings.stableMatching.GaleShapley; +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; + +import java.util.Random; +import java.util.Collections; // for shuffling +import java.util.ArrayList; // for shuffling +import java.util.List; // for shuffling + +public class GaleShapleyTest { + + /** + * Test a number of GaleShapley executions on pseudo-random instances of the + * stable marriage problem. + */ + + @Test + public void testGaleShapley() { + GaleShapley galeShapley = new GaleShapley(); + int N = 10; + int[][] menPrefs; + int[][] womenPrefs; + int[] GaleShapleyMenMatching; // the solution returned by GaleShapley.java + // for each n from 0 to N-1, create and test an instance of the problem. + for (int n = 0; n < N; n++) { + System.out.println("testing n = " + n); + menPrefs = new int[n][n]; + womenPrefs = new int[n][n]; + // set all other sex individuals in each individual's preference list, + // then shuffle + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + menPrefs[i][j] = j; + womenPrefs[i][j] = j; + } + shuffleArray(menPrefs[i], i); + shuffleArray(womenPrefs[i], n+i); + } + // Now we have pseudo-random preferences for each man and each woman. + GaleShapleyMenMatching = galeShapley.GaleShapleyStableMarriage(menPrefs, womenPrefs); + assertEquals("Unstable matching", true, isStable(GaleShapleyMenMatching, menPrefs, womenPrefs)); + } + } + + /** + * Determine if the proposed menMatching is stable, i.e. if there is no + * potential couple in which both members would strictly prefer being with each + * other than being with their current partner. + * + * @param menMatching + * @param menPrefs + * @param womenPrefs + * @return whether menMatching is stable according to menPrefs and womenPrefs + */ + + public boolean isStable(int[] menMatching, int[][] menPrefs, int[][] womenPrefs) { + int n = menMatching.length; + // reconstruct womenMatching (for each woman, the associated man): + int[] womenMatching = new int[n]; + int man; + int woman; + for (man = 0; man < n; man++) { + woman = menMatching[man]; + womenMatching[woman] = man; + } + + // construct menRanks and womenRanks to quickly compare preferences: + int[][] menRanks = new int[n][n]; + int[][] womenRanks = new int[n][n]; + int individualAtThisRank; + for (int i = 0; i < n; i++) { + for (int rank = 0; rank < n; rank++) { + // womenRanks + individualAtThisRank = womenPrefs[i][rank]; + womenRanks[i][individualAtThisRank] = rank; + + // menRanks + individualAtThisRank = menPrefs[i][rank]; + menRanks[i][individualAtThisRank] = rank; + } + } + + // Do the actual test by considering all potential n*n couples and verifying + // that at least one of them is happier now than they + // would be in the potential couple + + int currentEngagedMan; // man currently engaged to considered woman + int currentEngagedWoman; // woman currently engaged to considered man + for (man = 0; man < n; man++) { + for (woman = 0; woman < n; woman++) { + currentEngagedMan = womenMatching[woman]; + currentEngagedWoman = menMatching[man]; + if (womenRanks[woman][man] < womenRanks[woman][currentEngagedMan] + && menRanks[man][woman] < menRanks[man][currentEngagedWoman]) { + // man prefers woman over currentEngagedWoman, and + // woman prefers man over currentEngagedMan. + // The marriage therefore isn't stable. + return false; + } + } + } + return true; + } + + /** + * Shuffle an array using Collections.shuffle + * + * @param array array to be shuffled + * @param seed fixed seed, for reproducibility + */ + + public void shuffleArray(int[] array, long seed) { + List list = new ArrayList<>(); + for (int i : array) { + list.add(i); + } + Collections.shuffle(list, new Random(seed)); + for (int i = 0; i < list.size(); i++) { + array[i] = list.get(i); + } + } +} \ No newline at end of file