Skip to content

Stable marriage Python implementation is cubic #1005

Open
@pgdr

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)

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