From d36d75d2ef7d9af3fc1f32bfb3bb3b4a5028b482 Mon Sep 17 00:00:00 2001 From: Tom Sha-bar Date: Mon, 9 Feb 2026 18:40:41 -0500 Subject: [PATCH 1/7] create file --- greedy_methods/gale_shapley_stable_matching.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 greedy_methods/gale_shapley_stable_matching.py diff --git a/greedy_methods/gale_shapley_stable_matching.py b/greedy_methods/gale_shapley_stable_matching.py new file mode 100644 index 000000000000..e69de29bb2d1 From 4cec5815077afab33f42e26e52c7af78577d6dd2 Mon Sep 17 00:00:00 2001 From: Tom Sha-bar Date: Mon, 9 Feb 2026 18:45:48 -0500 Subject: [PATCH 2/7] initial algorithm --- .../gale_shapley_stable_matching.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/greedy_methods/gale_shapley_stable_matching.py b/greedy_methods/gale_shapley_stable_matching.py index e69de29bb2d1..cea262327c07 100644 --- a/greedy_methods/gale_shapley_stable_matching.py +++ b/greedy_methods/gale_shapley_stable_matching.py @@ -0,0 +1,60 @@ +class GaleShapley: + """Implementation of the Gale-Shapley algorithem + + takes it 2 preference list as a 2D array of ints. First one is the + proposing side. + """ + + def find_matches( + self, + proposers_preferences: dict[int, list[int]], + receivers_preferences: dict[int, list[int]], + ) -> dict[int, int]: + """ + >>> gs = GaleShapley() + >>> gs.find_matches({1: [1, 2, 3], 2: [2, 1, 3], 3: [2, 3, 1]}, {1: [1, 2, 3], 2: [2, 1, 3], 3: [2, 3, 1]}) + {1: 1, 2: 2, 3: 3} + >>> gs.find_matches({}, {}) + {} + >>> gs.find_matches({1: [1,]}, {1: [1,]}) + {1: 1} + """ + + matches = {key: -1 for key in proposers_preferences.keys()} + + # [NOTE] I would've used sets, but want replicability for easy debugging. + free_proposers = list(proposers_preferences.keys()) + tested_matches = {key: 0 for key in proposers_preferences.keys()} + + while free_proposers: + proposer = free_proposers[0] + + if tested_matches[proposer] == len(proposers_preferences[proposer]): + free_proposers.remove(proposer) + continue + + receiver = proposers_preferences[proposer][tested_matches[proposer]] + tested_matches[proposer] += 1 + + if receiver not in matches.values(): + matches[proposer] = receiver + free_proposers.remove(proposer) + continue + cur_proposer = next( + prop for prop, rec in matches.items() if rec == receiver + ) + if receivers_preferences[receiver].index(proposer) < receivers_preferences[ + receiver + ].index(cur_proposer): + matches[cur_proposer] = -1 + matches[proposer] = receiver + free_proposers.remove(proposer) + free_proposers.append(cur_proposer) + + return matches + + +if __name__ == "__main__": + import doctest + + doctest.testmod() From 6696fa782ea2dfa416fda26405331b1b3b9de56a Mon Sep 17 00:00:00 2001 From: Tom Sha-bar Date: Mon, 9 Feb 2026 19:21:14 -0500 Subject: [PATCH 3/7] explanation and docs --- .../gale_shapley_stable_matching.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/greedy_methods/gale_shapley_stable_matching.py b/greedy_methods/gale_shapley_stable_matching.py index cea262327c07..cedd08aa797a 100644 --- a/greedy_methods/gale_shapley_stable_matching.py +++ b/greedy_methods/gale_shapley_stable_matching.py @@ -1,3 +1,35 @@ +""" +Gale-Shapley Stable Matching (Hospital-Proposing Version) + +This function implements the Gale-Shapley algorithm to produce a stable +matching between two groups: hospitals and students. Each hospital ranks +students in order of preference, and each student ranks hospitals. + +A matching is considered stable if there is no hospital-student pair who +would both prefer to be matched with each other over their current assignment + +Algorithm overview: +1. Start with all hospitals and students unmatched. +2. While there exists an unmatched hospital that still has students left + to propose to: + a. The hospital proposes to the highest-ranked student on its preference + list that it has not yet proposed to. + b. If the student is unmatched, they tentatively accept the proposal. + c. If the student is already matched, they compare their current match + with the new hospital and keep the one they prefer more, rejecting + the other. +3. Rejected hospitals continue proposing down their lists. +4. The process ends when all hospitals are matched or have exhausted their + preference lists. + +Properties: +- The algorithm always terminates with a stable matching. +- The result is optimal for the proposing side (hospitals): each hospital + receives the best student it could obtain in any stable matching. +- If students propose instead, the result becomes student-optimal. +""" + + class GaleShapley: """Implementation of the Gale-Shapley algorithem @@ -11,6 +43,7 @@ def find_matches( receivers_preferences: dict[int, list[int]], ) -> dict[int, int]: """ + # add some tests >>> gs = GaleShapley() >>> gs.find_matches({1: [1, 2, 3], 2: [2, 1, 3], 3: [2, 3, 1]}, {1: [1, 2, 3], 2: [2, 1, 3], 3: [2, 3, 1]}) {1: 1, 2: 2, 3: 3} From d1a036785576e58e0363e0d209a52360c5aa195a Mon Sep 17 00:00:00 2001 From: Tom Sha-bar Date: Tue, 10 Feb 2026 18:18:07 -0500 Subject: [PATCH 4/7] add some doctests --- greedy_methods/gale_shapley_stable_matching.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/greedy_methods/gale_shapley_stable_matching.py b/greedy_methods/gale_shapley_stable_matching.py index cedd08aa797a..bc556518e5c8 100644 --- a/greedy_methods/gale_shapley_stable_matching.py +++ b/greedy_methods/gale_shapley_stable_matching.py @@ -43,19 +43,29 @@ def find_matches( receivers_preferences: dict[int, list[int]], ) -> dict[int, int]: """ - # add some tests >>> gs = GaleShapley() - >>> gs.find_matches({1: [1, 2, 3], 2: [2, 1, 3], 3: [2, 3, 1]}, {1: [1, 2, 3], 2: [2, 1, 3], 3: [2, 3, 1]}) + >>> gs.find_matches( + ... {1: [1, 2, 3], 2: [2, 1, 3], 3: [2, 3, 1]}, + ... {1: [1, 2, 3], 2: [2, 1, 3], 3: [2, 3, 1]}) {1: 1, 2: 2, 3: 3} >>> gs.find_matches({}, {}) {} - >>> gs.find_matches({1: [1,]}, {1: [1,]}) + >>> gs.find_matches( + ... {1: [1,]}, + ... {1: [1,]}) {1: 1} + >>> gs.find_matches( + ... {1: [1, 2, 3, 4], 2: [1, 2, 3, 4], 3: [1, 2, 3, 4], 4: [1, 2, 3, 4]}, + ... {1: [4, 3, 2, 1], 2: [1, 2, 3, 4], 3: [2, 3, 4, 1], 4: [3, 4, 1, 2]}) + {1: 2, 2: 3, 3: 4, 4: 1} + >>> gs.find_matches( + ... {1: [2, 3, 4, 5, 6, 1], 2: [2, 4, 5, 6, 1, 3], 3: [4, 5, 6, 1, 2, 3], 4: [5, 6, 1, 2, 3, 4], 5: [2, 1, 6, 3, 4, 5], 6: [1, 2, 3, 4, 5, 6]}, + ... {1: [6, 1, 2, 3, 4, 5], 2: [1, 2, 3, 4, 5, 6], 3: [2, 3, 4, 5, 6, 1], 4: [3, 4, 5, 6, 1, 2], 5: [4, 5, 6, 1, 2, 3], 6: [5, 6, 1, 2, 3, 4]}) + {1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 1} """ matches = {key: -1 for key in proposers_preferences.keys()} - # [NOTE] I would've used sets, but want replicability for easy debugging. free_proposers = list(proposers_preferences.keys()) tested_matches = {key: 0 for key in proposers_preferences.keys()} From ea67a47186c25dbeb8c5721b03a45a4e9bb5b013 Mon Sep 17 00:00:00 2001 From: Tom Sha-bar Date: Tue, 10 Feb 2026 18:34:24 -0500 Subject: [PATCH 5/7] fix ruff and add documentation --- .../gale_shapley_stable_matching.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/greedy_methods/gale_shapley_stable_matching.py b/greedy_methods/gale_shapley_stable_matching.py index bc556518e5c8..dcffce429a62 100644 --- a/greedy_methods/gale_shapley_stable_matching.py +++ b/greedy_methods/gale_shapley_stable_matching.py @@ -59,19 +59,21 @@ def find_matches( ... {1: [4, 3, 2, 1], 2: [1, 2, 3, 4], 3: [2, 3, 4, 1], 4: [3, 4, 1, 2]}) {1: 2, 2: 3, 3: 4, 4: 1} >>> gs.find_matches( - ... {1: [2, 3, 4, 5, 6, 1], 2: [2, 4, 5, 6, 1, 3], 3: [4, 5, 6, 1, 2, 3], 4: [5, 6, 1, 2, 3, 4], 5: [2, 1, 6, 3, 4, 5], 6: [1, 2, 3, 4, 5, 6]}, - ... {1: [6, 1, 2, 3, 4, 5], 2: [1, 2, 3, 4, 5, 6], 3: [2, 3, 4, 5, 6, 1], 4: [3, 4, 5, 6, 1, 2], 5: [4, 5, 6, 1, 2, 3], 6: [5, 6, 1, 2, 3, 4]}) + ... {1: [2, 3, 4, 5, 6, 1], 2: [2, 4, 5, 6, 1, 3], 3: [4, 5, 6, 1, 2, 3], + ... 4: [5, 6, 1, 2, 3, 4], 5: [2, 1, 6, 3, 4, 5], 6: [1, 2, 3, 4, 5, 6]}, + ... {1: [6, 1, 2, 3, 4, 5], 2: [1, 2, 3, 4, 5, 6], 3: [2, 3, 4, 5, 6, 1], + ... 4: [3, 4, 5, 6, 1, 2], 5: [4, 5, 6, 1, 2, 3], 6: [5, 6, 1, 2, 3, 4]}) {1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 1} """ - matches = {key: -1 for key in proposers_preferences.keys()} - + matches = dict.fromkeys(proposers_preferences.keys(), -1) + tested_matches = dict.fromkeys(proposers_preferences.keys(), 0) free_proposers = list(proposers_preferences.keys()) - tested_matches = {key: 0 for key in proposers_preferences.keys()} while free_proposers: proposer = free_proposers[0] + # continue if all options for proposer have been exhausted if tested_matches[proposer] == len(proposers_preferences[proposer]): free_proposers.remove(proposer) continue @@ -79,6 +81,7 @@ def find_matches( receiver = proposers_preferences[proposer][tested_matches[proposer]] tested_matches[proposer] += 1 + # set receiver as match if not previously matched if receiver not in matches.values(): matches[proposer] = receiver free_proposers.remove(proposer) @@ -86,13 +89,15 @@ def find_matches( cur_proposer = next( prop for prop, rec in matches.items() if rec == receiver ) + + # give receiver new proposer match only if it preferes new over old if receivers_preferences[receiver].index(proposer) < receivers_preferences[ receiver ].index(cur_proposer): - matches[cur_proposer] = -1 - matches[proposer] = receiver free_proposers.remove(proposer) free_proposers.append(cur_proposer) + matches[cur_proposer] = -1 + matches[proposer] = receiver return matches From dba7712080d98b963b7c8dfd0eb1aa4418fb2c1b Mon Sep 17 00:00:00 2001 From: Tom Sha-bar Date: Tue, 10 Feb 2026 18:47:57 -0500 Subject: [PATCH 6/7] add wikipedia link --- greedy_methods/gale_shapley_stable_matching.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/greedy_methods/gale_shapley_stable_matching.py b/greedy_methods/gale_shapley_stable_matching.py index dcffce429a62..b8b448533a03 100644 --- a/greedy_methods/gale_shapley_stable_matching.py +++ b/greedy_methods/gale_shapley_stable_matching.py @@ -1,6 +1,8 @@ """ Gale-Shapley Stable Matching (Hospital-Proposing Version) +wikipedia: https://en.wikipedia.org/wiki/Gale%E2%80%93Shapley_algorithm + This function implements the Gale-Shapley algorithm to produce a stable matching between two groups: hospitals and students. Each hospital ranks students in order of preference, and each student ranks hospitals. From 71aa71891b622a9863f97e65c085ae3b404b5978 Mon Sep 17 00:00:00 2001 From: Tom Sha-bar Date: Tue, 10 Feb 2026 19:15:57 -0500 Subject: [PATCH 7/7] fix typo failures --- greedy_methods/gale_shapley_stable_matching.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/greedy_methods/gale_shapley_stable_matching.py b/greedy_methods/gale_shapley_stable_matching.py index b8b448533a03..dc8becbcdbe2 100644 --- a/greedy_methods/gale_shapley_stable_matching.py +++ b/greedy_methods/gale_shapley_stable_matching.py @@ -33,7 +33,7 @@ class GaleShapley: - """Implementation of the Gale-Shapley algorithem + """Implementation of the Gale-Shapley algorithm takes it 2 preference list as a 2D array of ints. First one is the proposing side. @@ -91,8 +91,7 @@ def find_matches( cur_proposer = next( prop for prop, rec in matches.items() if rec == receiver ) - - # give receiver new proposer match only if it preferes new over old + # give receiver new proposer match only if it prefers new over old if receivers_preferences[receiver].index(proposer) < receivers_preferences[ receiver ].index(cur_proposer):