Skip to content

Stable marriage Python implementation is cubic #1005

Description

@pgdr

Bug Report

Description

The Python implementation in the stable marriage chapter is worst case ϴ(n³), but should be O(n²).

I have attached an alternative implementation that runs in worst-case quadratic time. I can make a PR if you want to replace the current algorithm.

Steps to Reproduce

Run the code with N = 3000.

Expected behavior

Terminate within seconds.

Screenshots

N current new
0 0:00.11 0:00.08
100 0:00.10 0:00.07
200 0:00.14 0:00.09
300 0:00.18 0:00.13
400 0:00.28 0:00.16
500 0:00.24 0:00.20
600 0:00.90 0:00.27
700 0:00.71 0:00.35
800 0:01.38 0:00.43
900 0:01.95 0:00.55
1000 0:01.37 0:00.65
1100 0:04.69 0:00.80
1200 0:02.42 0:00.95
1300 0:04.20 0:01.15
1400 0:05.49 0:01.29
1500 0:09.23 0:01.55
1600 0:15.27 0:01.76
1700 0:09.13 0:01.93
1800 0:10.11 0:02.19
1900 0:11.64 0:02.27
2000 0:18.57 0:02.49
2100 0:22.63 0:02.89
2200 0:19.52 0:03.13
2300 0:26.70 0:03.43
2400 0:54.32 0:03.72
2500 1:07.84 0:04.16

Additional context

from random import sample as shuffle


def stable_matching(hospital_preferences, student_preferences):
    students = [s for s in student_preferences]
    hospitals = [h for h in hospital_preferences]
    proposals = {h: 0 for h in hospitals}
    unmatched_hospitals = [h for h in hospitals]
    student = {h: None for h in hospitals}
    hospital = {s: None for s in students}
    inrank = {
        s: {} for s in students
    }  # maps s to each hospital's s-ranking
    for s in students:
        for idx, h in enumerate(student_preferences[s]):
            inrank[s][h] = idx

    while unmatched_hospitals:
        h = unmatched_hospitals.pop()
        nxt = proposals[h]
        s = hospital_preferences[h][nxt]
        proposals[h] += 1
        # h proposes to its best student not yet proposed to
        if not hospital[s]:
            # s is available
            hospital[s] = h
            student[h] = s
        else:
            sh = hospital[s]
            rank_sh = inrank[s][sh]
            rank_h = inrank[s][h]
            if rank_h < rank_sh:
                # s dumps sh for h
                hospital[s] = h
                student[sh] = None
                student[h] = s
                unmatched_hospitals.append(sh)
            else:
                # s is taken
                unmatched_hospitals.append(h)
    return student


def _generate_instance(N):
    HOSPITALS = [f"h{i}" for i in range(N)]
    STUDENTS = [f"s{i}" for i in range(N)]

    hospital_preferences = {h: STUDENTS[:N] for h in HOSPITALS[:N]}
    student_preferences = {s: HOSPITALS[:N] for s in STUDENTS[:N]}

    for h in HOSPITALS[:N]:
        hospital_preferences[h] = shuffle(hospital_preferences[h], N)

    for s in STUDENTS[:N]:
        student_preferences[s] = shuffle(student_preferences[s], N)

    return hospital_preferences, student_preferences


if __name__ == "__main__":
    import sys

    hospital_preferences, student_preferences = _generate_instance(int(sys.argv[1]))
    #print(hospital_preferences)
    #print(student_preferences)

    M = stable_matching(hospital_preferences, student_preferences)
    for h in M:
        print(f"Hospital {h} + Student {M[h]}")

For Algorithm Archive Developers

  • The bug can be reproduced
  • The bug can be fixed (if not, please explain why not in a comment below)
  • There is a timeline to fix the bug
  • The bug has been fixed (Please link the PR)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ProblemThis is a problem in the archive or an implementation.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions