runnableExamples:
discard """
abe π ivy, bob π cath, col π dee, dan π fay, ed π jan,
fred π bea, gav π gay, hal π eve, ian π hope, jon π abi
Current matching stability: β Stable
Swapping matches for random contenders: bob <=> gav
abe π ivy, bob π gay, col π dee, dan π fay, ed π jan,
fred π bea, gav π cath, hal π eve, ian π hope, jon π abi
Current matching stability: β Unstable
π bob prefers cath over gay
π cath prefers bob over gav
"""
import std/[random, strutils, sequtils, strformat, options]
const
MNames = ["abe", "bob", "col", "dan", "ed", "fred", "gav", "hal", "ian", "jon"]
FNames = ["abi", "bea", "cath", "dee", "eve", "fay", "gay", "hope", "ivy", "jan"]
MPreferences = [
["abi", "eve", "cath", "ivy", "jan", "dee", "fay", "bea", "hope", "gay"],
["cath", "hope", "abi", "dee", "eve", "fay", "bea", "jan", "ivy", "gay"],
["hope", "eve", "abi", "dee", "bea", "fay", "ivy", "gay", "cath", "jan"],
["ivy", "fay", "dee", "gay", "hope", "eve", "jan", "bea", "cath", "abi"],
["jan", "dee", "bea", "cath", "fay", "eve", "abi", "ivy", "hope", "gay"],
["bea", "abi", "dee", "gay", "eve", "ivy", "cath", "jan", "hope", "fay"],
["gay", "eve", "ivy", "bea", "cath", "abi", "dee", "hope", "jan", "fay"],
["abi", "eve", "hope", "fay", "ivy", "cath", "jan", "bea", "gay", "dee"],
["hope", "cath", "dee", "gay", "bea", "abi", "fay", "ivy", "jan", "eve"],
["abi", "fay", "jan", "gay", "eve", "bea", "dee", "cath", "ivy", "hope"]
]
FPreferences = [
["bob", "fred", "jon", "gav", "ian", "abe", "dan", "ed", "col", "hal"],
["bob", "abe", "col", "fred", "gav", "dan", "ian", "ed", "jon", "hal"],
["fred", "bob", "ed", "gav", "hal", "col", "ian", "abe", "dan", "jon"],
["fred", "jon", "col", "abe", "ian", "hal", "gav", "dan", "bob", "ed"],
["jon", "hal", "fred", "dan", "abe", "gav", "col", "ed", "ian", "bob"],
["bob", "abe", "ed", "ian", "jon", "dan", "fred", "gav", "col", "hal"],
["jon", "gav", "hal", "fred", "bob", "abe", "col", "ed", "dan", "ian"],
["gav", "jon", "bob", "abe", "ian", "dan", "hal", "ed", "col", "fred"],
["ian", "col", "hal", "gav", "fred", "bob", "abe", "ed", "jon", "dan"],
["ed", "hal", "gav", "abe", "bob", "jon", "col", "ian", "fred", "dan"]
]
ContenderPrefs = initContenderPrefs(MPreferences, FNames)
RecipientPrefs = initRecipientPrefs(FPreferences, MNames)
proc randomPair(max: Positive): (Natural, Natural) =
let a = rand(max)
var b = rand(max - 1)
if b == a: b = max
(a.Natural, b.Natural)
proc perturbPairs(ms: var Matches) =
randomize()
let (a, b) = randomPair(ms.contenderMatches.len - 1)
echo "Swapping matches between random contenders: ",
MNames[a], " <=> ", MNames[b]
template swap(arr: var openArray[int]; a, b: int) = swap(arr[a], arr[b])
ms.contenderMatches.swap(a, b)
ms.recipientMatches.swap(ms.contenderMatches[a], ms.contenderMatches[b])
func str(c: Clash; aNames, bNames: openArray[string]): string =
&"\u{0001F494} {aNames[c.id]} prefers {bNames[c.prefers]} over {bNames[c.match]}"
proc checkPairStability(matches: Matches; recipientPrefs, contenderPrefs:
openArray[Ranking]): bool =
let clashes = checkMatchingStability(matches, contenderPrefs, recipientPrefs)
if clashes.isSome():
let (clC, clR) = clashes.get()
echo "\u2717 Unstable\n", clC.str(MNames, FNames), '\n', clR.str(FNames, MNames)
false
else:
echo "\u2713 Stable"
true
proc render(contMatches: seq[int]; cNames, rNames: openArray[
string]): string =
for c, r in pairs(contMatches):
result.add(cNames[c] & " \u{0001F491} " & rNames[r])
if c < contMatches.high: result.add(", ")
var matches = stableMatching(ContenderPrefs, RecipientPrefs)
template checkStabilityAndLog(matches: Matches; perturb: bool = false): bool =
if perturb: perturbPairs(matches)
echo render(matches.contenderMatches, MNames, FNames)
stdout.write "Current matching stability: "
checkPairStability(matches, RecipientPrefs, ContenderPrefs)
doAssert matches.checkStabilityAndLog() == true
doAssert matches.checkStabilityAndLog(perturb = true) == false
{.push raises: [].}
import std/options
from std/sequtils import newSeqWith
type
Ranking*[T: int] = concept c
c.len is Ordinal
c[int] is T
Matches* = object
contenderMatches*: seq[int]
recipientMatches*: seq[int]
Clash* = tuple[id, match, prefers: Natural]
func initContenderPrefs*[N: static int](prefs: array[N, array[N, string]];
rNames: openArray[string]): array[N, array[N, int]] {.compileTime.} =
for c, ranking in pairs(prefs):
for rank, recipient in pairs(ranking):
assert recipient in ranking
result[c][rank] = rNames.find(recipient)
func initRecipientPrefs*[N: static int](prefs: array[N, array[N, string]];
cNames: openArray[string]): array[N, array[N, int]] {.compileTime.} =
for r, ranking in pairs(prefs):
for rank, contender in pairs(ranking):
assert contender in ranking
result[r][cNames.find(contender)] = rank
func invertPrefs*[N: static int](prefs: array[N, array[N, int]]):
array[N, array[N, int]] =
for rId, ranking in prefs.pairs():
for rank, id in ranking.pairs():
result[rId][id] = rank
func stableMatching*(contenderPrefs, recipientPrefs: openArray[
Ranking]): Matches =
assert recipientPrefs.len == recipientPrefs[0].len
assert contenderPrefs.len == contenderPrefs[0].len
assert recipientPrefs.len == contenderPrefs.len
let rosterLen = recipientPrefs.len
var
recMatches = newSeqWith(rosterLen, -1)
contMatches = newSeqWith(rosterLen, -1)
contQueue = newSeqWith(rosterLen, 0)
template match(c, r: Natural) =
contMatches[c] = r
recMatches[r] = c
while contMatches.contains(-1):
for c in 0..<rosterLen:
if contMatches[c] == -1:
let r = contenderPrefs[c][contQueue[c]]
inc(contQueue[c])
let rivalMatch = recMatches[r]
if rivalMatch == -1:
match(c, r)
elif recipientPrefs[r][c] < recipientPrefs[r][rivalMatch]:
contMatches[rivalMatch] = -1
match(c, r)
Matches(contenderMatches: contMatches, recipientMatches: recMatches)
func checkMatchingStability*(matches: Matches; contenderPrefs, recipientPrefs:
openArray[Ranking]): Option[(Clash, Clash)] =
for c, curMatch in matches.contenderMatches.pairs():
let curMatchScore = contenderPrefs[c].find(curMatch)
for preferredRec in 0..<curMatchScore:
let checkedRec = contenderPrefs[c][preferredRec]
let checkedRival = matches.recipientMatches[checkedRec]
if recipientPrefs[checkedRec][checkedRival] > recipientPrefs[checkedRec][c]:
let clashC = (id: c.Natural, match: checkedRec.Natural,
prefers: curMatch.Natural)
let clashR = (id: checkedRec.Natural, match: c.Natural,
prefers: checkedRival.Natural)
return some((clashC, clashR))
none((Clash, Clash))
when isMainModule:
import std/unittest
suite "Stable Matching":
test "RosettaCode":
const
MNames = ["abe", "bob", "col"]
FNames = ["abi", "bea", "cath"]
MPreferences = [
["abi", "cath", "bea"],
["cath", "abi", "bea"],
["abi", "bea", "cath"]]
FPreferences = [
["bob", "abe", "col"],
["bob", "abe", "col"],
["bob", "col", "abe"]]
ContenderPrefs = initContenderPrefs(MPreferences, FNames)
RecipientPrefs = initRecipientPrefs(FPreferences, MNames)
func isStable(matches: Matches;
contenderPrefs, recipientPrefs: openArray[Ranking]): bool =
let c = checkMatchingStability(matches, contenderPrefs, recipientPrefs)
c.isNone()
let matches = stableMatching(ContenderPrefs, RecipientPrefs)
check matches.contenderMatches == @[0, 2, 1]
check matches.recipientMatches == @[0, 2, 1]
check isStable(matches, ContenderPrefs, RecipientPrefs)
test "TheAlgorithms/Python":
const DonorPrefs = [[0, 1, 3, 2], [0, 2, 3, 1], [1, 0, 2, 3], [0, 3, 1, 2]]
const RecipientRrefs = invertPrefs([[3, 1, 2, 0], [3, 1, 0, 2],
[0, 3, 1, 2], [1, 0, 3, 2]])
let matches = stableMatching(DonorPrefs, RecipientRrefs)
check matches.contenderMatches == @[1, 2, 3, 0]
check matches.recipientMatches == @[3, 0, 1, 2]
test "Defect: mismatched number of participants":
const ContenderPrefs = @[@[0, 1, 2], @[0, 2, 1], @[1, 0, 2], @[0, 1, 2]]
const RecipientRrefs = @[@[1, 0], @[1, 0], @[0, 1], @[1, 0]]
expect(AssertionDefect):
discard stableMatching(ContenderPrefs, RecipientRrefs)