|
|
# Solution to Ruby Quiz #165
#
# This procedure *doesn't* find the *best* solution. Instead, it aims
# to give *good* results in an acceptable time interval.
#
# = Description of the algorithm
#
# 1. Find all possible pairs
#
# Given the following preferences matrix:
#
# A: C D E F B
# B: A C D F E
# C: B A F E D
# D: A B E F C
# E: F D A B C
# F: A B E D C
#
# the first step is to find all possible pairs:
#
# A <-> C - score: 1
# A <-> D - score: 1
# A <-> E - score: 8
# A <-> F - score: 9
# A <-> B - score: 16
# B <-> C - score: 1
# B <-> D - score: 5
# B <-> E - score: 25
# B <-> F - score: 10
# F <-> C - score: 20
# F <-> D - score: 18
# F <-> E - score: 4
# E <-> C - score: 25
# E <-> D - score: 5
# D <-> C - score: 32
#
# The score of a pair is given by:
#
# score of pair = score of first player + score of second player
#
# For example, the score of AC pair ( |AC| ) is given by:
#
# |AC| = 0 + 1 = 1
#
# 2. Sort pairs by score
#
# The pairs are sorted by score:
#
# A <-> C - score: 1
# A <-> D - score: 1
# B <-> C - score: 1
# F <-> E - score: 4
# E <-> D - score: 5
# B <-> D - score: 5
# A <-> E - score: 8
# A <-> F - score: 9
# B <-> F - score: 10
# A <-> B - score: 16
# F <-> D - score: 18
# F <-> C - score: 20
# E <-> C - score: 25
# B <-> E - score: 25
# D <-> C - score: 32
#
# 3. Select a subset of pairs
#
# A subset of the "best" pairings is then chosen:
#
# A <-> C - score: 1
# A <-> D - score: 1
# B <-> C - score: 1
# F <-> E - score: 4
#
# 4. For each pair produce a scheme
#
# Now, for each pair in the subset at point 3, the algorithm produces
# a scheme (a possible solution) traversing the set of all pairs
# sorted by score.
#
# For example:
#
# Given the pair AC taken from the "best" pairings subset and traversing
# the set of all the pairs sorted by score we obtain the scheme below:
#
# A <-> C
# F <-> E
# B <-> D
#
# In this example, four schemes will be produced.
#
# 5. Take the best scheme produced
#
# Finally, to give the "best" result, the procedure takes from all the
# generated schemes, the scheme with minimum score. In this case:
#
# A <-> D - score: 1
# B <-> C - score: 1
# F <-> E - score: 4
#
# Total score: 6
#
#
# Pair objects represent a pair of players.
#
class Pair
include Comparable
class << self
#
# Calculates the score of the given pair.
#
# Example:
#
# preferences = {'A' => ['B', 'C'], 'B' => ['A', 'C'], 'C' => ['B', 'A']}
# Pair.score(preferences, 'A', 'B') #=> 0
#
def calc_score(preferences, player, other_player)
preferences[player].index(other_player)**2 + preferences[other_player].index(player)**2
end
end
attr_reader :pair, :score
alias :to_a :pair
#
# Initialize a pair of player with an associated score. If a score
# is not given it defaults to 0.
#
# Example:
#
# pair = Pair.new('David', 'Joseph', 3)
#
def initialize(*args)
@pair, @score = args[0..1], args[2] || 0
end
#
# AC == AC
# AC == CA
#
def ==(other_pair)
@pair == other_pair.pair or @pair == other_pair.pair.reverse
end
def <=>(other_pair)
score <=> other_pair.score
end
#
# Check if the pair includes a player.
#
# Example:
#
# pair = Pair.new('David', 'Joseph', 3)
# pair.include?('David') #=> true
#
def include?(player)
@pair.include?(player)
end
# Pretty print pair.
def to_s
"#{@pair[0]} <-> #{@pair[1]} - score: #{score}\n"
end
end
#
# Scheme objects are responsible to calculate possible pairings.
#
class Scheme
include Comparable
attr_reader :players, :pairs, :pairings
#
# Initialize a scheme passing the players and the set of all
# possible pairs (see SchemeGenerator).
#
def initialize(players, pairs)
@players, @pairs = players, pairs
end
#
# Add a pair to the scheme.
#
# Example:
#
# scheme << Pair.new('David', 'Helen')
#
def <<(pair)
@pairs << pair
end
#
# Compares schemes by their score.
#
def <=>(other_scheme)
score <=> other_scheme.score
end
#
# Calculates the total score of the scheme.
#
def score
@pairings.inject(0) do |sum, pair|
sum + pair.score
end
end
#
# Check if the given player is already in pair.
#
def include?(player)
players_in_pair.include?(player)
end
#
# Pretty print pairings and score.
#
def to_s
@pairings.each { |pair| puts pair.to_s }
puts "#{left_out} was left out" if left_out
puts "\nTotal score: #{score}\n"
end
#
# Traverses the pairs starting from the given pair and produces a
# possible scheme of pairings.
#
def pair_off(pair)
@pairings = []
@pairings << pair
@pairs.sort.each do |pair|
@pairings << pair unless include?(pair.pair[0]) or include?(pair.pair[1])
end
self
end
#
# If players are in odd number, returns an array containing the
# player that was left out.
#
def left_out
@players - players_in_pair if @players.size % 2
end
private
def players_in_pair
@pairings.collect { |pair| pair.pair }.flatten
end
end
#
# SchemeGenerator generates possible schemes.
#
class SchemeGenerator
attr_reader :players, :pairs, :preferences
class << self
#
# Load a preferences matrix from the given file.
#
# Example:
#
# generator = SchemeGenerator.load('example_a')
#
def load(file)
require 'yaml'
preferences = { }
YAML::load_file(file).each { |player, prefs| preferences[player] = prefs.split }
SchemeGenerator.new(preferences)
end
end
#
# Initialize a SchemeGenerator object with the given preferences matrix.
#
def initialize(preferences)
@preferences = preferences
find_all_players
find_all_pairs
end
#
# Calculates the score of the given pair.
#
def calc_pair_score(player, other_player)
Pair.calc_score(@preferences, player, other_player)
end
#
# Produces a set of possible solutions. The method select a subset
# of the pairs with lower scores. Then, for each selected pair, a
# scheme is calculated.
#
# Example:
#
# generator.pair_off.min # returns the "best" solution
#
def pair_off
@schemes = []
# Select all pairs that score from 0 to ((players.size - 1) / 2)**2
# These are considered to be good pairs.
@pairs.select { |pair| pair.score >= 0 and pair.score <= ((players.size - 1) / 2) ** 2 }.each do |pair|
@schemes << Scheme.new(@players, @pairs).pair_off(pair)
end
@schemes
end
private
#
# Grab all players from the first row of preferences matrix.
#
def find_all_players
@players = []
first = @preferences.keys.first
@players = @preferences[first].to_a.flatten << first
end
#
# Find all possible pairs.
#
def find_all_pairs
@pairs = []
players = @players.dup
players.reverse_each do |player|
players.each do |other_player|
@pairs << Pair.new(player, other_player, calc_pair_score(player, other_player)) unless player == other_player
end
players.pop
end
@pairs
end
end
SchemeGenerator.load(ARGV[0]).pair_off.min.to_s if ARGV[0]
|