-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathstable-roommates-algorithm.js
More file actions
140 lines (114 loc) · 3.76 KB
/
stable-roommates-algorithm.js
File metadata and controls
140 lines (114 loc) · 3.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
function rejectSymmetrically(i, j, prefs) {
const indexJ = prefs[i].indexOf(j)
if (indexJ > -1) prefs[i].splice(indexJ, 1)
const indexI = prefs[j].indexOf(i)
if (indexI > -1) prefs[j].splice(indexI, 1)
}
function acceptProposal(i, preferred, proposals, accepts) {
proposals[i] = preferred
accepts[preferred] = i
}
function wouldBreakup(preferred, i, accepts, prefs) {
const indexCurrentProposal = prefs[preferred].indexOf(accepts[preferred])
const indexNewProposal = prefs[preferred].indexOf(i)
// TODO add -1 check?
return indexNewProposal < indexCurrentProposal
}
function breakupFor(preferred, i, proposals, accepts, prefs) {
proposals[i] = preferred
const oldAccept = accepts[preferred]
accepts[preferred] = i
rejectSymmetrically(preferred, oldAccept, prefs)
return oldAccept
}
function firstPhase(prefs) {
const unmatched = [ ...Array(prefs.length).keys() ]
const proposals = {}
const accepts = {}
while (unmatched.length > 0) {
const i = unmatched[0]
const preferred = prefs[i][0]
if (!accepts[preferred]) {
// the preferred is free
acceptProposal(i, preferred, proposals, accepts)
unmatched.shift()
} else if (wouldBreakup(preferred, i, accepts, prefs)) {
// preferred is willing to breakup
preferredOldAccept = breakupFor(preferred, i, proposals, accepts, prefs)
unmatched[0] = preferredOldAccept
} else {
rejectSymmetrically(i, preferred, prefs)
}
}
return { prefs, accepts }
}
function secondPhase(prefs, accepts) {
for (let i = 0; i < prefs.length; i++) {
const indexAccept = prefs[i].indexOf(accepts[i])
if (indexAccept > -1) {
// deep copy, just to be sure
const toRemove = JSON.parse(JSON.stringify(prefs[i].slice(indexAccept + 1)))
toRemove.forEach(r => rejectSymmetrically(i, r, prefs))
}
}
return prefs
}
function getRotation(i, prefs, secondChoices, lastChoices) {
if (lastChoices.slice(0,-1).indexOf(lastChoices[lastChoices.length - 1]) !== -1) {
// preference cycle
return { secondChoices, lastChoices }
}
const second = prefs[i][1]
// at a given point in the recursion, we find that the current i has no second
// preference, hence there are no other rotations to remove, thus no stable matching exists.
// NOTE: this is just an intuition without actual proof
if (second === undefined) {
return { secondChoices: null, lastChoices: null }
}
secondChoices.push(second)
const secondLastIndex = prefs[second].length - 1
const secondLast = prefs[second][secondLastIndex]
lastChoices.push(secondLast)
return getRotation(secondLast, prefs, secondChoices, lastChoices)
}
function removeRotation(secondChoices, lastChoices, prefs) {
let i = lastChoices.indexOf(lastChoices[lastChoices.length - 1])
for (i++; i < lastChoices.length; i++) {
rejectSymmetrically(secondChoices[i-1], lastChoices[i], prefs)
}
}
function thirdPhase(prefs) {
let i = 0
while (i < prefs.length) {
if (prefs[i].length === 1) {
i++
} else {
const { secondChoices, lastChoices } = getRotation(i, prefs, [], [i])
// no stable matching exists
if (secondChoices === null || lastChoices === null) {
return null
}
removeRotation(secondChoices, lastChoices, prefs)
// if any list becomes empty, return null (no stable matching exists)
if (prefs.some(p => p.length === 0)) {
return null
}
i = 0
}
}
return prefs
}
/**
* Returns the matching list with the given preferences, or null if no stable
* matching exists.
*
* @param {Array.<Array>} preferences
*/
function computeMatches(preferences) {
let { prefs, accepts } = firstPhase(preferences)
prefs = secondPhase(prefs, accepts)
return thirdPhase(prefs)
}
module.exports = {
computeMatches
}