diff options
Diffstat (limited to 'stable.py')
-rw-r--r-- | stable.py | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/stable.py b/stable.py new file mode 100644 index 0000000..f3bb84e --- /dev/null +++ b/stable.py | |||
@@ -0,0 +1,188 @@ | |||
1 | #!/usr/bin/env python | ||
2 | |||
3 | # The gale_shapley implementation is taken from | ||
4 | # https://johnlekberg.com/blog/2020-08-22-stable-matching.html | ||
5 | |||
6 | from collections import deque | ||
7 | |||
8 | # Track the number of total proposals per proposer for order O questions | ||
9 | number_of_proposals = dict() | ||
10 | |||
11 | |||
12 | def pref_to_rank(pref): | ||
13 | return {a: {b: idx for idx, b in enumerate(a_pref)} for a, a_pref in pref.items()} | ||
14 | |||
15 | |||
16 | def gale_shapley(*, A, B, A_pref, B_pref): | ||
17 | """Create a stable matching using the | ||
18 | Gale-Shapley algorithm. | ||
19 | |||
20 | A -- set[str]. | ||
21 | B -- set[str]. | ||
22 | A_pref -- dict[str, list[str]]. | ||
23 | B_pref -- dict[str, list[str]]. | ||
24 | |||
25 | Output: list of (a, b) pairs. | ||
26 | """ | ||
27 | B_rank = pref_to_rank(B_pref) | ||
28 | ask_list = {a: deque(bs) for a, bs in A_pref.items()} | ||
29 | pair = {} | ||
30 | |||
31 | remaining_A = set(A) | ||
32 | while len(remaining_A) > 0: | ||
33 | a = remaining_A.pop() | ||
34 | b = ask_list[a].popleft() | ||
35 | if b not in pair: | ||
36 | pair[b] = a | ||
37 | print(f"Matching {a} with {b} (a)") | ||
38 | number_of_proposals[a] += 1 | ||
39 | else: | ||
40 | a0 = pair[b] | ||
41 | b_prefer_a0 = B_rank[b][a0] < B_rank[b][a] | ||
42 | if b_prefer_a0: | ||
43 | remaining_A.add(a) | ||
44 | print(f"{a} is rejected by {b} because {a0} is more preffered (c)") | ||
45 | number_of_proposals[a] += 1 | ||
46 | else: | ||
47 | remaining_A.add(a0) | ||
48 | print(f"Matching {a} with {b}, {a0} is now free (b)") | ||
49 | number_of_proposals[a] += 1 | ||
50 | pair[b] = a | ||
51 | |||
52 | return [(a, b) for b, a in pair.items()] | ||
53 | |||
54 | |||
55 | if __name__ == "__main__": | ||
56 | |||
57 | women = list(range(1, 27)) | ||
58 | men = [chr(x + 65) for x in range(26)] | ||
59 | |||
60 | n = int(input("n: ")) | ||
61 | |||
62 | men = men[:n] | ||
63 | # because algorithms are 1 indexed, compromise | ||
64 | women = ["0"] + women[:n] | ||
65 | |||
66 | men_pref = dict() | ||
67 | women_pref = dict() | ||
68 | |||
69 | # For 1 to n-1, men's preference lists look like | ||
70 | # Mk = Wk Wk+1 ... Wn-1 W1 W2 ... Wn | ||
71 | |||
72 | for (cur, m) in enumerate(men, start=1): | ||
73 | |||
74 | # last man n has the same preference list as n-1 | ||
75 | if cur == n: | ||
76 | cur = cur - 1 | ||
77 | |||
78 | prefs = list() | ||
79 | k = cur | ||
80 | |||
81 | # Wk .. Wn-1 | ||
82 | while k < n: | ||
83 | prefs.append(women[k]) | ||
84 | k += 1 | ||
85 | |||
86 | # W1 .. Wn-1 | ||
87 | k = 1 | ||
88 | while k < cur: | ||
89 | prefs.append(women[k]) | ||
90 | k += 1 | ||
91 | |||
92 | # last woman n is always last at everyone's preference list | ||
93 | prefs.append(women[n]) | ||
94 | men_pref[m] = prefs | ||
95 | |||
96 | # women's preference list for 1-n-2 look like | ||
97 | # Wk = Wk+1 Wk | ||
98 | |||
99 | # n-1 looks like | ||
100 | # Wn-1 = W1 Wn | ||
101 | |||
102 | # last women's preference list doesn't matter | ||
103 | |||
104 | for (cur, w) in enumerate(women, start=-1): | ||
105 | |||
106 | prefs = list() | ||
107 | |||
108 | # again, compromise between 1 indexed algorithms and nice code | ||
109 | if cur == -1: | ||
110 | continue | ||
111 | |||
112 | # last woman | ||
113 | if cur == n - 1: | ||
114 | k = 0 | ||
115 | while k < n: | ||
116 | prefs.append(men[k]) | ||
117 | k += 1 | ||
118 | women_pref[w] = prefs | ||
119 | continue | ||
120 | |||
121 | # n-1th woman | ||
122 | if cur == n - 2: | ||
123 | prefs.append(men[0]) | ||
124 | prefs.append(men[n - 1]) | ||
125 | |||
126 | k = 0 | ||
127 | |||
128 | while k < n: | ||
129 | if k == n - 1: | ||
130 | k += 1 | ||
131 | continue | ||
132 | if k == 0: | ||
133 | k += 1 | ||
134 | continue | ||
135 | prefs.append(men[k]) | ||
136 | k += 1 | ||
137 | |||
138 | women_pref[w] = prefs | ||
139 | continue | ||
140 | |||
141 | # rest of the women obey the general rule | ||
142 | prefs.append(men[cur + 1]) | ||
143 | prefs.append(men[cur]) | ||
144 | |||
145 | # we need n - k more men to fill, they don't matter | ||
146 | k = 0 | ||
147 | |||
148 | while k < n: | ||
149 | if k == cur: | ||
150 | k += 1 | ||
151 | continue | ||
152 | if k == cur + 1: | ||
153 | k += 1 | ||
154 | continue | ||
155 | prefs.append(men[k]) | ||
156 | k += 1 | ||
157 | |||
158 | women_pref[w] = prefs | ||
159 | |||
160 | print("Men Preference Table") | ||
161 | for (k,v) in men_pref.items(): | ||
162 | print(f'{k:2}| {" ".join([str(x) for x in v])}') | ||
163 | |||
164 | print("----") | ||
165 | |||
166 | print("Women Preference Table") | ||
167 | for (k,v) in women_pref.items(): | ||
168 | print(f'{k:2}| {" ".join(v)}') | ||
169 | |||
170 | print("----") | ||
171 | |||
172 | number_of_proposals = {a: 0 for a in men} | ||
173 | |||
174 | matchings= gale_shapley(A=men, B=women, A_pref=men_pref, B_pref=women_pref) | ||
175 | |||
176 | print(f"Final pairings:") | ||
177 | |||
178 | for (m,w) in matchings: | ||
179 | print(f"{m} - {w}") | ||
180 | |||
181 | print() | ||
182 | |||
183 | total = 0 | ||
184 | for (men, times) in number_of_proposals.items(): | ||
185 | total += times | ||
186 | print(f"{men} proposed {times} times") | ||
187 | |||
188 | print(f"total of {total} times with n(n-1) + 1 = {n*(n-1) + 1}") | ||