Assume we have two disjoint finite sets. Let us call them L and R. We want to pair each element in L with a unique element in R. However, we also have some constraints: we also assume that each element in L declared a list of preferences of vertices they want to be matched, and vice versa. A one-to-one matching M between L and R is called stable if any subset of M is altered to obtain another matching M’ then there is at least one matching \((x,y)\) in M’ where both \(x\) and \(y\) are matched with their lesser choices than their original match M.
The problem of finding such stable matchings is called the Stable Matching Problem, and you can find a detailed description of the problem here.
Gale and Shaply came up with a deferred acceptence algorithm that produces a stable match, however, it is not symmetric in L and R. The algorithm favors the proposing party rather than the proposees. Here is the pseudo-code
Algorithm Gale-Shaply:
Input: Two disjoint finite sets L and R
A list of permutations PR of L indexed by R
A list of permutations PL of R indexed by L
Output: A one-to-one stable matching between L and R
Begin
For each y in R set Prop(y) to the empty set
For each x in L
Let m be the top choice of x in PL(x)
Add (x,m) to Prop(m)
End
If Prop is a one-to-one matching
Return Prop
Else
For each y in R
Let n be the top choice of y in L
such that (n,y) is in Prop(y)
For each x in Prop(y) \ { (n,y) }
Remove (x,y) from PL(x)
End
End
End
Call Gale-Shaply with L, R, PR and the new PL
End
Here is a more intuitive natural language description:
Every element in L proposes to its top choice on the list of preferences. Every element in R conditionally accepts the proposal from its top choice among those who proposed, and rejects the other proposals. We continue the process: Every element in L send in a new proposal with its top choice out of those who have not yet rejected it. Every element in R conditionally accepts the proposal from its top choice among those who proposed, and rejects the other proposals. This process continues until we achieve a one-to-one match.
Before implementing the algorithm above, I am going to need a utility
function called assoc-merge
. I need this to merge
association lists in a particular way.
defun merge-assoc (xs)
(let (res)
(dolist (x xs res)
(let* ((k (car x))
(cdr (assoc k res :test #'equal))))
(r (if r
(rplacd (assoc k res :test #'equal) (push (cadr x) r))
(push x res)))))) (
MERGE-ASSOC
Let me show you how this works:
1) (:b 2) (:c 3) (:a 10) (:b 20))) (merge-assoc '((:a
3) (B 20 2) (A 10 1)) ((C
And here is my implementation of Gale-Shaply:
defun gale-shaply (left right &optional (n 0))
(labels ((maximal (unordered ordered)
(car (remove-if-not (lambda (x) (member x unordered)) ordered))))
(let* (;; this round of proposals
(mapcar (lambda (xs) (list (car xs) (cadr xs))) left))
(pro (;; now count on the RHS how many offers each candidate received
remove-duplicates (mapcar #'cdr pro) :test #'equal)))
(img (if (= (length left) (length img))
(;; if each candidate on the RHS received one proposal STOP
list :numsteps n :solution pro)
(let* (;; otherwise, first calculate what are the top choices of the candidates
(mapcar (lambda (x)
(acc (list (maximal (cdr x) (cdr (assoc (car x) right :test #'equal)))
(car x)))
(mapcar #'reverse pro))))
(merge-assoc (;; next reject the rest
remove-if (lambda (x) (member x acc :test #'equal)) pro)))
(rej (;; do another round with the updated list of preferences
mapcar (lambda (x)
(gale-shaply (let ((v (cadr (assoc (car x) rej :test #'equal))))
(cons (car x) (remove v (cdr x) :test #'equal))))
(
left)
right1+ n))))))) (
GALE-SHAPLY
In order to test this, I am going to need random permutations:
defun random-permutation (xs)
(let* ((n (length xs))
(mapcar #'cons (copy-list xs) (loop for i from 1 to n collect (random n)))))
(ys (mapcar #'car (sort ys #'< :key #'cdr)))) (
RANDOM-PERMUTATION
Let us test this:
(random-permutation '(:a :b :c :d))
(C D A B)
And finally our test function for the algorithm:
let* ((N 8)
(loop for i from 0 below N collect (format nil "y~d" i)))
(A (loop for i from 0 below N collect (format nil "x~d" i)))
(B (mapcar #'cons A (loop repeat N collect (random-permutation B))))
(left (mapcar #'cons B (loop repeat N collect (random-permutation A)))))
(right (append (list :left left :right right) (gale-shaply left right))) (
(LEFT
((y0 x4 x3 x6 x7 x5 x1 x0 x2) (y1 x0 x4 x1 x3 x2 x5 x7 x6)
(y2 x1 x4 x7 x5 x3 x0 x6 x2) (y3 x2 x6 x5 x0 x3 x7 x1 x4)
(y4 x3 x5 x7 x0 x2 x6 x1 x4) (y5 x4 x5 x3 x1 x0 x2 x6 x7)
(y6 x6 x0 x4 x7 x2 x3 x1 x5) (y7 x4 x5 x3 x1 x7 x0 x2 x6))
RIGHT
((x0 y7 y5 y0 y1 y4 y6 y2 y3) (x1 y2 y4 y7 y6 y1 y0 y3 y5)
(x2 y2 y6 y4 y5 y7 y1 y3 y0) (x3 y1 y2 y6 y0 y4 y3 y5 y7)
(x4 y5 y2 y6 y1 y3 y0 y4 y7) (x5 y0 y4 y5 y6 y1 y3 y7 y2)
(x6 y2 y3 y1 y5 y7 y0 y4 y6) (x7 y1 y0 y3 y7 y2 y4 y5 y6))5 SOLUTION
NUMSTEPS ((y0 x3) (y1 x0) (y2 x1) (y3 x2) (y4 x5) (y5 x4) (y6 x6) (y7 x7)))