stable_marriage 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +45 -0
- data/LICENSE +22 -0
- data/README.md +56 -0
- data/Rakefile +6 -0
- data/lib/stable_marriage/descending_insertion_sort_array.rb +17 -0
- data/lib/stable_marriage/match_maker.rb +36 -0
- data/lib/stable_marriage/match_set.rb +55 -0
- data/lib/stable_marriage/preference.rb +27 -0
- data/lib/stable_marriage/version.rb +3 -0
- data/lib/stable_marriage.rb +32 -0
- data/stable-marriage.gemspec +24 -0
- metadata +112 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 75eb013fa666c7e0bf2f44c2e566be9038cdc043
|
4
|
+
data.tar.gz: c048c9b0fea7f68473d6f7d54183c8f1effa8923
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 05e7d654750796ea8b08b762b9f284f9f0ff93410171c73590c163784a0dd51e2d7e423e5593145342af584216d209f11b0048f552f2155f973052f03a3ce9a3
|
7
|
+
data.tar.gz: 0403e2c0dfd5b57a06646661d5b3027f255b9cf9dbdae3d4cf94b45ac215974de59592814395989b83a1cdf80d1cacd48500a0ebd640ec00c54fbcd93f0afa60
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
stable_marriage (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
byebug (10.0.2)
|
10
|
+
coderay (1.1.2)
|
11
|
+
diff-lcs (1.3)
|
12
|
+
method_source (0.9.0)
|
13
|
+
pry (0.11.3)
|
14
|
+
coderay (~> 1.1.0)
|
15
|
+
method_source (~> 0.9.0)
|
16
|
+
pry-byebug (3.6.0)
|
17
|
+
byebug (~> 10.0)
|
18
|
+
pry (~> 0.10)
|
19
|
+
rake (10.5.0)
|
20
|
+
rspec (3.7.0)
|
21
|
+
rspec-core (~> 3.7.0)
|
22
|
+
rspec-expectations (~> 3.7.0)
|
23
|
+
rspec-mocks (~> 3.7.0)
|
24
|
+
rspec-core (3.7.1)
|
25
|
+
rspec-support (~> 3.7.0)
|
26
|
+
rspec-expectations (3.7.0)
|
27
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
28
|
+
rspec-support (~> 3.7.0)
|
29
|
+
rspec-mocks (3.7.0)
|
30
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
31
|
+
rspec-support (~> 3.7.0)
|
32
|
+
rspec-support (3.7.1)
|
33
|
+
|
34
|
+
PLATFORMS
|
35
|
+
ruby
|
36
|
+
|
37
|
+
DEPENDENCIES
|
38
|
+
bundler (~> 1.12)
|
39
|
+
pry-byebug (~> 3.3)
|
40
|
+
rake (~> 10.0)
|
41
|
+
rspec (~> 3.4)
|
42
|
+
stable_marriage!
|
43
|
+
|
44
|
+
BUNDLED WITH
|
45
|
+
1.16.1
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Tom Collier
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
22
|
+
|
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# Stable Marriage
|
2
|
+
|
3
|
+
[](https://www.youtube.com/watch?v=Qcv1IqHWAzg)
|
4
|
+
|
5
|
+
The [Stable Marriage Problem](https://en.wikipedia.org/wiki/Stable_marriage_problem)
|
6
|
+
is a mathematical problem that attempts to uniquely match a set of _N_ items
|
7
|
+
(classically male suitors) with another set of _N_ items (classically females the
|
8
|
+
suitors wish to marry). The final matches are referred to as proposals.
|
9
|
+
|
10
|
+
## Gale-Shapely Algorithm
|
11
|
+
|
12
|
+
This gem provides a variant of the [Gale-Shapely algorithm](https://en.wikipedia.org/wiki/Stable_marriage_problem#Solution).
|
13
|
+
Gale-Shapely guarantees a complete matching (i.e. every suitor is paired with
|
14
|
+
exactly one female and vice versa). Though this algorithm assumes every member
|
15
|
+
of either group has a complete ranking of the other group. For large populations,
|
16
|
+
this is not always practical.
|
17
|
+
|
18
|
+
## Variation
|
19
|
+
|
20
|
+
The algorithm applied here has 2 primary differences from Gale-Shapely
|
21
|
+
|
22
|
+
1. Neither suitors nor suitees need to have a complete ranking of the other set.
|
23
|
+
2. The rankings are determined by a symmetrical match score. For example, the match
|
24
|
+
score for Alice and Steve is the same as for Steve and Alice. Though scores are
|
25
|
+
relative, so Alice's match score for Steve may be her highest scoring match, but
|
26
|
+
that same score may only be the fifth highest scoring match for Steve.
|
27
|
+
|
28
|
+
Because of these differences, not every suitor or suitee is guaranteed to have
|
29
|
+
a proposal. Swapping the suitor and suitee sets can have dramatic effects on the
|
30
|
+
final set of proposals.
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
```bash
|
35
|
+
gem install stable-marriage
|
36
|
+
```
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require 'stable_marriage'
|
40
|
+
sm = StableMarriage.new
|
41
|
+
sm.add_match('Alice', 'Marcus', 0.366)
|
42
|
+
sm.add_match('Alice', 'Steve', 0.453)
|
43
|
+
sm.add_match('Alice', 'Will', 0.245)
|
44
|
+
sm.add_match('Janice', 'Phil', 0.486)
|
45
|
+
sm.add_match('Janice', 'Steve', 0.304)
|
46
|
+
sm.add_match('Lily', 'Steve', 0.299)
|
47
|
+
sm.add_match('Maria', 'Steve', 0.602)
|
48
|
+
puts sm.proposals
|
49
|
+
# {
|
50
|
+
# "Maria" => "Steve",
|
51
|
+
# "Janice" => "Phil",
|
52
|
+
# "Alice" => "Marcus"
|
53
|
+
# }
|
54
|
+
|
55
|
+
# Note: neither Lily nor Will were matched in the proposals map
|
56
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class StableMarriage
|
2
|
+
class DescendingInsertionSortArray < Array
|
3
|
+
# @param item [Object] any object that responds to `>`
|
4
|
+
def sorted_insert(item)
|
5
|
+
insertion_index = (0...size).bsearch(&search_proc(item))
|
6
|
+
insert(insertion_index || length, item)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def search_proc(item)
|
12
|
+
->(index) { item > self[index] }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private_constant :DescendingInsertionSortArray
|
17
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class StableMarriage
|
2
|
+
class MatchMaker
|
3
|
+
def initialize(match_set)
|
4
|
+
@match_set = match_set
|
5
|
+
end
|
6
|
+
|
7
|
+
def proposals
|
8
|
+
proposals = {}
|
9
|
+
(0...match_set.max_suitor_preferences).each do |round|
|
10
|
+
suitee_proposals = {}
|
11
|
+
match_set.each_suitor_prefs do |suitor, suitor_prefs|
|
12
|
+
if round < suitor_prefs.count && !proposals.key?(suitor)
|
13
|
+
suitee = match_set.suitee_preferred_at(suitor, round)
|
14
|
+
suitee_proposals[suitee] ||= []
|
15
|
+
suitee_proposals[suitee] << suitor
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
suitee_proposals.each do |suitee, suitors|
|
20
|
+
winning_suitor = match_set.most_preferred(suitee, suitors)
|
21
|
+
proposals[winning_suitor] = suitee
|
22
|
+
suitors.each do |suitor|
|
23
|
+
proposals.delete(suitor) unless suitor == winning_suitor
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
proposals
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :match_set
|
34
|
+
end
|
35
|
+
private_constant :MatchMaker
|
36
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require_relative 'descending_insertion_sort_array'
|
2
|
+
require_relative 'preference'
|
3
|
+
|
4
|
+
class StableMarriage
|
5
|
+
class MatchSet
|
6
|
+
attr_reader :max_suitor_preferences
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@suitors = {}
|
10
|
+
@suitees = {}
|
11
|
+
@max_suitor_preferences = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(suitor, suitee, score)
|
15
|
+
suitor_prefs = add_suitor_prefs(suitor)
|
16
|
+
suitee_prefs = add_suitee_prefs(suitee)
|
17
|
+
|
18
|
+
suitor_prefs.sorted_insert(Preference.new(suitee, score))
|
19
|
+
if suitor_prefs.count > max_suitor_preferences
|
20
|
+
@max_suitor_preferences = suitor_prefs.count
|
21
|
+
end
|
22
|
+
|
23
|
+
suitee_prefs.sorted_insert(Preference.new(suitor, score))
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_suitor_prefs(&block)
|
27
|
+
suitors.each(&block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def suitee_preferred_at(suitor, round)
|
31
|
+
suitors[suitor][round].object
|
32
|
+
end
|
33
|
+
|
34
|
+
def most_preferred(suitee, suitors)
|
35
|
+
suitees[suitee].detect do |preference|
|
36
|
+
suitors.include?(preference.object)
|
37
|
+
end.object
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def add_suitor_prefs(object)
|
43
|
+
suitors[object] ||= DescendingInsertionSortArray.new
|
44
|
+
suitors[object]
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_suitee_prefs(object)
|
48
|
+
suitees[object] ||= DescendingInsertionSortArray.new
|
49
|
+
suitees[object]
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_reader :suitors, :suitees
|
53
|
+
end
|
54
|
+
private_constant :MatchSet
|
55
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class StableMarriage
|
2
|
+
class Preference
|
3
|
+
attr_reader :object, :score
|
4
|
+
|
5
|
+
def initialize(object, score)
|
6
|
+
@object = object
|
7
|
+
@score = score
|
8
|
+
end
|
9
|
+
|
10
|
+
def hash
|
11
|
+
object.hash
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
return false unless other.is_a?(self.class)
|
16
|
+
hash == other.hash
|
17
|
+
end
|
18
|
+
alias_method :eql?, :==
|
19
|
+
|
20
|
+
# Items used in DescendingInsertionSortArray must implement `>`
|
21
|
+
def >(other)
|
22
|
+
score > other.score
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private_constant :Preference
|
27
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'stable_marriage/match_maker'
|
2
|
+
require_relative 'stable_marriage/match_set'
|
3
|
+
|
4
|
+
class StableMarriage
|
5
|
+
def initialize
|
6
|
+
@match_set = MatchSet.new
|
7
|
+
end
|
8
|
+
|
9
|
+
# The the matching between the suitor and suitee with the match score.
|
10
|
+
#
|
11
|
+
# @param suitor [Object] the proposal maker in the Gale-Shapely algorithm.
|
12
|
+
# This party tends to fare better than the suitee in the algorithm.
|
13
|
+
# @param suitee [Object] the proposal acceptor/rejector in the Gale-Shapely
|
14
|
+
# algorithm.
|
15
|
+
# @param score [Numeric] a numeric representation of the strength of the
|
16
|
+
# match. A higher score means a better match.
|
17
|
+
def add_match(suitor, suitee, score)
|
18
|
+
match_set.add(suitor, suitee, score)
|
19
|
+
end
|
20
|
+
|
21
|
+
# A map with suitors as keys and suitees as objects
|
22
|
+
#
|
23
|
+
# @return [Hash<Object, Object>] the final matching between suitors and
|
24
|
+
# suitees.
|
25
|
+
def proposals
|
26
|
+
MatchMaker.new(match_set).proposals
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :match_set
|
32
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'stable_marriage/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'stable_marriage'
|
8
|
+
spec.version = StableMarriage::VERSION
|
9
|
+
spec.authors = ['Tom Collier']
|
10
|
+
spec.email = ['tcollier@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = 'Ruby implementation of a Stable Marriage solver'
|
13
|
+
spec.homepage = 'https://github.com/tcollier/stable-marriage'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_development_dependency 'bundler', '~> 1.12'
|
21
|
+
spec.add_development_dependency 'pry-byebug', '~> 3.3'
|
22
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
23
|
+
spec.add_development_dependency 'rspec', '~> 3.4'
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stable_marriage
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tom Collier
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-07-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.12'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.12'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry-byebug
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.4'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.4'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- tcollier@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- Gemfile
|
77
|
+
- Gemfile.lock
|
78
|
+
- LICENSE
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- lib/stable_marriage.rb
|
82
|
+
- lib/stable_marriage/descending_insertion_sort_array.rb
|
83
|
+
- lib/stable_marriage/match_maker.rb
|
84
|
+
- lib/stable_marriage/match_set.rb
|
85
|
+
- lib/stable_marriage/preference.rb
|
86
|
+
- lib/stable_marriage/version.rb
|
87
|
+
- stable-marriage.gemspec
|
88
|
+
homepage: https://github.com/tcollier/stable-marriage
|
89
|
+
licenses:
|
90
|
+
- MIT
|
91
|
+
metadata: {}
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
requirements: []
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 2.6.11
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: Ruby implementation of a Stable Marriage solver
|
112
|
+
test_files: []
|