Skip to content

Add Gale-Shapley Algorithm for Stable Matching#14376

Closed
signore662-beep wants to merge 3 commits intoTheAlgorithms:masterfrom
signore662-beep:master
Closed

Add Gale-Shapley Algorithm for Stable Matching#14376
signore662-beep wants to merge 3 commits intoTheAlgorithms:masterfrom
signore662-beep:master

Conversation

@signore662-beep
Copy link
Copy Markdown

Describe your change:

This PR implements the Gale-Shapley Algorithm, a Nobel-winning algorithm used to solve the Stable Marriage Problem. It finds a stable matching between two equally sized sets of elements given their ranked preferences.

Key Features:

  • Guaranteed Stability: The implementation ensures that no two elements prefer each other over their current partners.
  • Pure Python: No external dependencies (no numpy, no pandas).
  • Optimization: Uses a dictionary for O(1) preference lookups to achieve O(n²) time complexity.
  • Elegance: Follows PEP 8 strictly, with clear type hints and comprehensive doctests.

Why this is a good addition:
While foundational to economics and computer science (used in residency matching for doctors and content delivery networks), a clean, type-hinted implementation is currently missing or needs improvement in the collection.

  • Add maths/gale_shapley.py
  • Strictly type-hinted
  • PEP 8 and Ruff compliant
  • Detailed doctests included

"""
The Gale-Shapley algorithm is a solution to the Stable Marriage Problem.
It guarantees that for every two sets of preferences, a stable matching
always exists such that no two elements would rather be with each other
than their current partners.
"""

from typing import Dict, List, Optional

def stable_matching(
proposer_prefs: Dict[str, List[str]], responder_prefs: Dict[str, List[str]]
) -> Dict[str, str]:
"""
Implements the Gale-Shapley algorithm to find a stable matching.

Args:
    proposer_prefs: A dictionary where keys are proposers and values
                    are lists of responders in order of preference.
    responder_prefs: A dictionary where keys are responders and values
                     are lists of proposers in order of preference.

Returns:
    A dictionary mapping responders to their stable proposer partners.

Example:
    >>> p_prefs = {
    ...     "A": ["X", "Y"],
    ...     "B": ["Y", "X"]
    ... }
    >>> r_prefs = {
    ...     "X": ["A", "B"],
    ...     "Y": ["B", "A"]
    ... }
    >>> stable_matching(p_prefs, r_prefs)
    {'X': 'A', 'Y': 'B'}

    >>> p_prefs = {
    ...     "guy1": ["girl1", "girl2"],
    ...     "guy2": ["girl1", "girl2"]
    ... }
    >>> r_prefs = {
    ...     "girl1": ["guy2", "guy1"],
    ...     "girl2": ["guy1", "guy2"]
    ... }
    >>> stable_matching(p_prefs, r_prefs)
    {'girl1': 'guy2', 'girl2': 'guy1'}
"""
# Initialize all proposers as free
free_proposers = list(proposer_prefs.keys())
# Dictionary to keep track of engaged responders: {responder: proposer}
engagements: Dict[str, str] = {}

# Pre-process responder preferences for O(1) lookup of preference rank
# This is the 'elegant' part that optimizes the algorithm
responder_rank: Dict[str, Dict[str, int]] = {
    res: {prop: rank for rank, prop in enumerate(prefs)}
    for res, prefs in responder_prefs.items()
}

# Track which responder a proposer has already proposed to
proposal_index: Dict[str, int] = {proposer: 0 for proposer in proposer_prefs}

while free_proposers:
    proposer = free_proposers.pop(0)
    # Get the next best responder the proposer hasn't proposed to yet
    pref_list = proposer_prefs[proposer]
    responder = pref_list[proposal_index[proposer]]
    proposal_index[proposer] += 1

    if responder not in engagements:
        # Responder is free, they get engaged
        engagements[responder] = proposer
    else:
        # Responder is already engaged, check if they prefer the new proposer
        current_partner = engagements[responder]
        if responder_rank[responder][proposer] < responder_rank[responder][current_partner]:
            # Responder prefers the new proposer
            engagements[responder] = proposer
            # The former partner is now free
            free_proposers.append(current_partner)
        else:
            # Responder stays with their current partner, proposer remains free
            free_proposers.append(proposer)

return engagements

if name == "main":
import doctest

# Run the tests to ensure the implementation is flawless
doctest.testmod()

# Simple interactive demo
test_proposers = {
    "Stan": ["Eve", "Ivy"],
    "Josh": ["Ivy", "Eve"]
}
test_responders = {
    "Eve": ["Stan", "Josh"],
    "Ivy": ["Stan", "Josh"]
}
result = stable_matching(test_proposers, test_responders)
print(f"Stable Matching Results: {result}")

@algorithms-keeper
Copy link
Copy Markdown

Closing this pull request as invalid

@signore662-beep, this pull request is being closed as the files submitted contains an invalid extension. This repository only accepts Python algorithms. Please read the Contributing guidelines first.

Invalid files in this pull request: Add Half-Life Sort (Novel Probabilistic Algorithm), Add Token Bucket algorithm for Rate Limiting

@algorithms-keeper algorithms-keeper bot added the awaiting reviews This PR is ready to be reviewed label Mar 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting reviews This PR is ready to be reviewed invalid

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant