[QUIZ] Secret Santas (#2)

J

Joe Cheng

Hmm, maybe I'm not awake yet this morning, but to me the solution seems
impossible from the start, by definition. The 5 Skywalkers need to be
santas for non-Skywalkers, of which there are only 4 choices. I can't
make that work out, but please correct me if I'm wrong.

Ahh, you're both totally right. I didn't do a check on the
last-to-first relationship. I guess it's me who's not awake yet :)
 
J

James Edward Gray II

Very nice, Robo. Whenever I've thought of "random except for ____"
type solutions, I've never gotten beyond thinking of "keep picking
random items until you meet the criteria" (which is a dumb way to go
about it).

Filtering the pool is quite nice.

But does it work? I chose the test data after much thought. ;)

My second run (it may take multiple tries):

% ruby Robo.rb < quiz_data.txt
Luke Skywalker is watching over Toula Portokalos
Leia Skywalker is watching over Gus Portokalos
Toula Portokalos is watching over Luke Skywalker
Gus Portokalos is watching over Lindsey Brigman
Bruce Wayne is watching over Leia Skywalker
Virgil Brigman is watching over Bruce Wayne
Lindsey Brigman is watching over nobody

Note the last line. Then examine the earlier lines to figure out why...

James Edward Gray II
 
D

Dennis Ranke

Hi!

I didn't have the time to implement a complete solution, but at least I'd
like to show my algorithm for assigning the santas.
I start by assigning a random santa to everyone without caring about the
constraints. Then I go through the list of people and for each one not
having a correct santa I swap santas with someone else, so that both have
correct santas afterwards.
As far as I can see, this will only fail when no solution is possible and
should be reasonably unbiased.

Here is the code:

class Person
attr_reader :first, :last, :email
attr_accessor :santa

def initialize(line)
m = /(\S+)\s+(\S+)\s+<(.*)>/.match(line)
raise unless m
@first = m[1].capitalize
@last = m[2].capitalize
@email = m[3]
end

def can_be_santa_of?(other)
@last != other.last
end
end

input = $stdin.read

people = []
input.each_line do |line|
line.strip!
people << Person.new(line) unless line.empty?
end

santas = people.dup
people.each do |person|
person.santa = santas.delete_at(rand(santas.size))
end

people.each do |person|
unless person.santa.can_be_santa_of? person
candidates = people.select {|p| p.santa.can_be_santa_of?(person) &&
person.santa.can_be_santa_of?(p)}
raise if candidates.empty?
other = candidates[rand(candidates.size)]
temp = person.santa
person.santa = other.santa
other.santa = temp
finished = false
end
end

people.each do |person|
printf "%s %s -> %s %s\n", person.santa.first, person.santa.last,
person.first, person.last
end
 
G

Gavin Kistner

Hrm, which gives me a good idea for my Ouroboros#separate_duplicates
method; currently if I find a family duplicate I just swap the second
one with it's following neighbor, and either hope that it's not the
same family or wait until the next pass in the loop to fix that one. I
suspect I would get much quicker sorting if I 'filtered' the
pool...search ahead for the next person who isn't in the same family
and swap them.

That smells like it should be an O(n) performer, rather than ~O(n^2).

Indeed, with 3072 people in the pathological case (where half are one
family, one quarter another, one eighth another, etc.) the above
optimization brings the time down to 20 seconds, instead of 254 seconds
using the code I originally created :)

I've updated Ouroboros.rb to reflect the new, more efficient code.
For the overly-curious who want to see the slow code, the new and old
methods are below (with irrelevant bits trimmed out).

def separate_duplicates!
max_crawls = @size*@size
keys = {}
@all.each{ |list_item|
keys[list_item] = block_given? ? yield(list_item) : list_item
}

crawls=0
dup_distance = 0
while dup_distance < @size && (crawls < max_crawls)
if keys[@current] == keys[@current.next]
dup_distance = 0
n = 1
begin; swapper = self[n+=1]; end until (keys[@current] !=
keys[swapper]) || (n==@size)
self.swap( @current.next, swapper )
else
dup_distance += 1
end
self.increment
crawls+=1
end
raise "Error: #separate_duplicates! was unsuccessful after
#{crawls} rounds." if dup_distance < @size
self
end

def separate_duplicates_old!
#...
while dup_distance < @size && (crawls < max_crawls)
if keys[@current] == keys[@current.next]
dup_distance = 0
self.swap( @current.next, self[2] )
else
dup_distance += 1
end
self.increment
crawls+=1
end
#...
end
 
J

Joe Cheng

Fixed my solution to check for tail and head being in same family, and
also add a sorting "bonus" to the family of the head to ensure that
condition doesn't arise if it doesn't have to.

---

# Represents a family.
class Family
attr_reader :name, :members

def initialize(name)
@name = name
@members = []
@bonus = 0
end

# Number of people in family.
def count
@members.length
end

# Give a sorting bonus--a family with
# the bonus will always appear before
# any families with the same count but
# no bonus
def give_bonus
@bonus = 0.1
end

# The count with sorting bonus included
def count_with_bonus
count + @bonus
end

# Pop the last member off.
def pop
@members.pop
end

# Compare by count/bonus.
def <=>(other)
count_with_bonus <=> other.count_with_bonus
end
end

class Person
attr_reader :first_name, :last_name, :email

def initialize(first_name, last_name, email)
@first_name = first_name
@last_name = last_name
@email = email
end

def to_s
"#{@first_name} #{@last_name} <#{@email}>"
end
end

familyTable = Hash.new {|h,k| h[k] = Family.new(k)}

while line = gets
line =~ /(\w+) (\w+) <(.+)>/
first, last, email = $1, $2, $3

familyTable[last].members << Person.new(first, last, email)
end

puts
puts "Processing..."

families = familyTable.values
santas = []

while families.length > 0

families.sort!.reverse!

if families.first.count == 0
# nobody is left; we're done
break
end

if santas.length == 0
# for the very first santa, choose someone from
# the largest family
santas << families.first.pop
families.first.give_bonus
else
success = false

# find largest family that is not one's own
families.each do |family|
if family.name != santas.last.last_name
santas << family.pop
success = santas.last
break
end
end

raise "No solution." unless success
end
end

if santas.length > 0 && santas.first.last_name == santas.last.last_name
raise "No solution."
end

puts "Success!"
puts

lastSanta = santas.last
santas.each do |santa|
puts santa.to_s + " => " + lastSanta.to_s
lastSanta = santa
end
 
M

Martin Ankerl

# This implementation is heavily optimized to be as fast to
# implement as possible. Please read the above sentence very
# carefull ;-) I have tried to find an implementation
# that is as simple to understand as possible, but does work.
require 'net/smtp'

Person = Struct.new("Person", :firstName, :familyName, :email)

# extract people from input stream
def readSantas(io)
santas = Array.new
io.read.scan(/(\S*)\s(\S*)\s\<(\S*)\>/) do |first, family, email|
santas.push Person.new(first, family, email)
end
santas
end

# get an ordering where each santa gets a person who is
# outside his family. This implementation is extremely simple,
# as it assumes it is always possible to find a correct solution.
# Actally it is so simple that it hurts. I would never use this
# in production-level code.
def orderPeople(santas)
isCorrectlyOrdered = false
while !isCorrectlyOrdered
# get a random order
people = santas.sort_by { rand }

# check if the random order does meet the requirements
i = 0
i += 1 while i<santas.size &&
santas.familyName==people.familyName
isCorrectlyOrdered = i<santas.size
end
people
end

# send santa a mail that he/she is responsible for person's presents
def sendMail(santa, person)
msg = ["Subject: Santa news", "\n",
"You are santa for #{person.firstName} #{person.familyName}" ]

Net::SMTP.start("localhost") do |smtp|
smtp.sendmail(msg, "santa delegator", santa.email)
end
end

if __FILE__ == $0
santas = readSantas(STDIN)
people = orderPeople(santas)

santas.each_index do |i|
santa = santas
person = people
puts "'#{santa.firstName} #{santa.familyName}' is santa for
'#{person.firstName} #{person.familyName}'"
# if you really want to send mails, uncomment the following
line.
#sendMail(santa, person)
end
end
 
R

Roeland Moors

Here is my solution.
It doesn't mail and I look for a radom santa until I get a
working combination.

# list of persons
$list = []

# generate list of candidates for santas
def get_candidates(person)
cans = []
$list.each do |p|
cans.push p unless
(p == person) ||
(p['last'] == person['last']) ||
(p['santa'] != -1)
end
cans
end

# get persons
while input = gets
person = Hash.new
person['first'], person['last'], person['mail'] = input.split
person['santa'] = -1
$list.push person
end

# seek santa
def seek_santa
index = 0
wrong = false
$list.each do |person|
candidates = get_candidates(person)
wrong = true if candidates.length == 0
return wrong if wrong
r = rand(candidates.length)
c = candidates[r]
$list.find { |p| p == c }['santa'] = index
index += 1
end
wrong
end

print '.' while !seek_santa
puts '.'

# display solution
$list.each do |person|
s = $list[person['santa']]
puts "#{person['first']} #{person['last']} -> #{s['first']} #{s['last']}"
end
 
G

Gavin Kistner

$santas = $players.sort_by { rand } until check_families?

Does this actually work? I mean, for realistic data sets?

With the pathological case (where half the people are in the same
family) I think[1] that the chances of randomly picking a working
layout for pool size N are

N * ((N/2)-1)! / (N-1)!

If so, the chances of picking a working layout each time for a total
pool of 4 people is 66.66%, the chances for a pool of 6 people is 10%,
8 => 0.95%, 10 => .066%, and so on.

If my calculations are correct, by the time you reach 10 people in one
family and 10 other participants, your chance of picking a working case
drops to 6 in 100000000000. That's gotta take a long time to find a
correct layout.

Perhaps my math is wrong, your test case small, your computer really
fast, or your test case not pathological :)

- Gavin

[1] Statistics is not my strong suit. My reasoning is as follows:
Consider a pool of 6 participants:
Joe Smith
Jane Smith
Jim Smith
Bob Dole
Barbara Bush
Bill Clinton

The correct arrangement for this pathological case is an interleaving
of Smiths and others, for example:
Jane Smith
Bill Clinton
Joe Smith
Bob Done
Jim Smith
Barbara Bush

There is a 1/1 chance that SOMEONE will be picked for the first spot.
There is a 3/5 chance that the next person will be of the opposite
Smith/Non-Smith persuasion.
There is a 2/4 chance that the next person will be of the opposite
Non-Smith/Smith persuasion.
There is a 2/3 chance for the next.
There is a 1/2 chance for the next.
There is a 1/1 chance for the last.
The cumulative chance of all that occurring is the product:
3/5 * 2/4 * 2/3 * 1/2 = 1/10

For an 8 person pool, the chances are:
4/7 * 3/6 * 3/5 * 2/4 * 2/3 * 1/2 = 1/35
Re-arranged, that looks like:
(4*3*3*2*2) / (7*6*5*4*3*2 )
which is
(4 * 3*2 * 3*2 ) / ( 7*6*5*4*3*2 )
which is
(4 * 2 * 3! ) / 7!

Looking at this pattern, then, the chances of randomly picking a
working pattern for pathological pool size of N seems to be:

N/2 * 2 * ((N/2)-1)! / (N-1)! == N * ((N/2)-1)! / (N-1)!

Or, in Ruby:

class Fixnum; def factorial; t=1; 2.upto(self.to_i){ |i| t*=i }; t;
end; end

def chances( pool_size )
pool_size.to_f * (pool_size/2-1).factorial / (pool_size-1).factorial
end

40.times{ |i|
next if i%2==1;
puts "#{i} => #{chances i}"
}
0 => 0.0
2 => 2.0
4 => 0.666666666666667
6 => 0.1
8 => 0.00952380952380952
10 => 0.000661375661375661
12 => 3.60750360750361e-05
14 => 1.61875161875162e-06
16 => 6.1666728333395e-08
18 => 2.04044321691381e-09
20 => 5.96620823659007e-11
22 => 1.56257834767835e-12
24 => 3.70571940160874e-14
26 => 8.02905870348561e-16
28 => 1.60123677847291e-17
30 => 2.95794971392779e-19
32 => 5.08894574439191e-21
34 => 8.19243159608545e-23
36 => 1.23919133386167e-24
38 => 1.76761526601889e-26

(The 2.0 in the case of a pool size of 2 reflects that there are two
correct cases, I think. Or perhaps it reflects the fact that my math is
overly generous by a factor of 2 ;)
 
J

James Edward Gray II

And here is my version of the second quiz. It does not use 'net/smtp'
but shows the chosen santas on the console.

Thomas

Did you run this program on the provided test data?

I believe it has the same catch in it I posted about in Robo's code
earlier today, though it displays it differently. If I run it 10 or 20
times, it eventually hangs on me.

James Edward Gray II
 
J

James Edward Gray II

My solution to the Quiz #2 is at http://phrogz.net/RubyLibs/quiz/2/

When I run the code there now, I get:

% ruby ZecretZanta.rb < quiz_data.txt
Enter the participants in the format 'FirstName LastName <email>':
Exception `NoMethodError' at ZecretZanta.rb:67 - undefined method
`separate_duplicates_old!' for #<Ouroboros:0x1b5fb8>
ZecretZanta.rb:67:in `randomize_recipients': undefined method
`separate_duplicates_old!' for #<Ouroboros:0x1b5fb8> (NoMethodError)
from ZecretZanta.rb:105

Am I missing something?

James Edward Gray II
 
G

Gavin Kistner

When I run the code there now, I get:
Exception `NoMethodError' at ZecretZanta.rb:67 - undefined method
`separate_duplicates_old!' for #<Ouroboros:

Oops! My FTP client must have auto-synchronized the folder while I was
messing around.

I've forced the right version back up.

Note that with $DEBUG false, it will attempt to email (and is set to
use smtp.comcast.net as the server).

With $DEBUG true, it won't attempt to email, but it still needs a valid
SMTP server set just because of the silly way I nested my code in an
attempt to be efficient and DRY.
 
J

Jeremy Hinegardner

This is a bit later than most, but I have refrained from looking at this
thread until I was finished.

When thinking about this quiz and the constraints I came up with the
following realizations:

1) It is impossible to have a secret santa with only 2 people. That
one is pretty evident.

2) It is impossible to have a secret santa if more than half of the
participants are in the same family.

3) The email address must be considered the unique identifier of a
santa. This one is pretty evident too.

The approach I took was to read in all the possible santas and check for
these constraints. Once the constraints were met I took the input as
the pool of possible santas and created essentially a circularly linked
list to represent the chain of senders and receivers of holiday cheer.

If you pick a person (A) in the chain then that person is the secret
santa of the next person in the chain. The person before (A) in the
chain is the secret santa of (A). Doing this also makes the assumption
that a single circular list of people can be created from the input.

The circular list is populated by randomly removing a person from the
pool and starting at the pseudo-beginning of the list. The person is
then walked around the list until a valid point in the list is found for
insertion.

A point is valid if both the person before the insertion point and after
the insertion point are not in the same family as the person to insert.

enjoy,

-jeremy


#!/bin/env ruby
########################################################################
#
# Ruby Quiz #2 - Secret Santa Selector
#
# On standard input is a set of names and addresses in the format:
#
# FIRST_NAME FAMILY_NAME <EMAIL_ADDRESS>
#
# The goal here is to pick secret santas or in a bit simpler terms,
# pick sender's and receivers of email but with the following caveats:
#
# 1) No 2 sender and receivers can have the same family name
#
# 2) No 2 sender and receivers can be each other's sender or
# receiver
#
# 3) A receiver cannot be their own sender
#
# With these caveats in mind, it is immediately apparent that there
# are situations when no solution can be found.
#
# 1) if there are only 2 santas total
# 2) if the number of santas from the same family is greater than
# 1/2 of all the total santas.
#
# Some assumptions that were made
#
# - the email address is the unique Santa Identifier. This seems
# to be a reasonable assumption since that is how the santas are
# notified.
#
# - it is possible for a single chain of senders and receivers to be
# created
#
########################################################################

class Santa

attr_reader :first_name, :family, :email

def initialize(first_name, family, email)
@first_name = first_name
@family = family
@email = email.downcase
end

# since the input may not be totally clean, comparing one Santa's
# family to another needs a bit of holiday magic
def same_family_as(other)
@family.downcase == other.family.downcase
end

# Santas will be compared sometimes. We only want the compare to happen
# on the email address, since that is the only unique portion.
def <=>(other)
@email <=> other.email
end

def eql?(other)
@email.eql?(other.email)
end

alias == eql?

def to_s
"#{first_name} #{family} #{email}"
end
end

# The SantaCircle depicts who is the Secret Santa of who.
#
# From a computer science perspective, the SantaCircle can be thought of
# as a circularly linked list, where the person at one point in the list is
# the secret santa of the next person in the list.

class SantaCircle

def initialize(first_santa=nil)
@circle = Array.new
circle.push(first_santa) unless first_santa.nil?
end

# this is where the magic happens. A Santa is added to the circle
# by traversing the circle and finding the first location where
# it fits. This means that:
#
# - The santa being added is not already in the circle
#
# - The location the santa fits into the circle is valid such that
# the santa before it in the circle can give to it and it can give
# to the santa after it in the circle. This condition is satisfied by
# checking Santa.same_family_as
#
# the method throws an Exception if a santa that is already in the circle is
# added again.
def add(santa)
raise "Santa #{santa.email} already in the Circle" if @circle.include?(santa)

# add the first one
if @circle.empty? then
@circle.insert(0,santa)
return
end

# for each santa currently in the circle, see if the new santa can fit
# before it. The fact that array[-1] is the last element in the array
# works out really nicely here, since the last santa in the array is the
# secret santa of the first santa in the array.
added = false
@circle.each_index do |i|
next if santa.same_family_as(@circle)
next if santa.same_family_as(@circle[i-1])
@circle.insert(i,santa)
added = true
break
end
raise "Santa #{santa.email} unable to be added to the Circle" if not added
end

def to_s
s = ""
@circle.each_index do |i|
s = s + "#{@circle[i-1].email} secret santa of #{@circle.email}\n"
end
return s
end

def each_pair
@circle.each_index do |i|
yield @circle[i-1], @circle
end
end
end


santa_pool = []
family_counts = {}
uniq_emails = {}

# pull in all the possible santas
ARGF.each do |line|
first,last,email = line.split
new_santa = Santa.new(first,last,email)

# keep track of how many santas are in each family
if family_counts.has_key?(new_santa.family) then
family_counts[new_santa.family] += 1
else
family_counts[new_santa.family] = 1
end

# verify that all santas are unique
if uniq_emails.has_key?(new_santa.email) then
puts "Invalid data set: email address #{new_santa.email} exists more than once"
exit 1
else
uniq_emails[new_santa.email] = 1
end

# santa has been a good person, so go jump in the pool.
santa_pool.push(new_santa)
end

# if there are only 2 people, then it makes no sense to have a secret santa
if santa_pool.size == 2 then
puts "It is not possible to have a Secret Santa, there are not enough people"
exit 1
end

# some sanity checking and make sure that one family doesn't have more than 1/2
# of all the possible santas.
max_family_count = 0
max_family = nil
family_counts.each_pair do |family,count|
if count > max_family_count then
max_family_count = count
max_family = family
end
end

if max_family_count > (santa_pool.size / 2.0) then
puts "It is not possible to have a Secret Santa, the #{max_family}'s have too many people"
exit 1
end

# hmmm... secrets everywhere
secrets = SantaCircle.new

# create the secret santas. Randomly pick from the santa pool and add to the
# Santa Circle until the santa pool is empty.
until santa_pool.empty?
santa = santa_pool.delete_at(rand(santa_pool.size))
secrets.add(santa)
end

# If this were were a real secret santa then uncomment some of the following
# lines and see if it works. The email portion of this script is untested. I
# just followed one of the examples on p. 703 of PickAxe II.

selector = "Santa Selector <[email protected]>"

#require 'net/smtp'
#Net::SMTP.start('localhost', 25) do |smtp|
secrets.each_pair do |sender, receiver|

msg = <<SECRET_MESSAGE
From: #{selector}
To: #{sender.to_s}
Subject: Your Secret Mission

You are to be the Secret Santa of:

#{receiver.to_s}

SECRET_MESSAGE

puts "=" * 70
puts msg

# smtp.send_message(msg,"#{selector}","#{sender.to_s}")

end
# end
 
C

Cristi BALAN

Yay, I think I just made my first, hopefully functional, piece of ruby code :)
Took me about 2.5 hours, including finding out about how to use
Comparable and define <=> :) ... including a long time to realize
delete_if is destructive :(

The solution does the following:
- groups persons by family,
- orders the families descending by number of members
- gets a santa as a member from the family with the most number of members
- for this santa, finds a santee as a member from the family with the
mose number of members, that is not the santa's family
rinse, repeat until there are no more santas and all is ok or, until
we can't find a santee for a santa and decide we can't hook these
people up

this seems ok, acts ok, but i can't really prove that it actually
works across all possible cases :)

Here's the code, sorry, it doesn't read from stdin, but it generates
it's own random input.
beware, this thing is probably badly coded :)

#####################

#!ruby
# evilchelu@irc
# mental@gmail

def generate_input(persons=10, families=3)
fams = Array.new(families) {|i| 1}
srand Time.now.to_i
(persons-families).times do
fams[rand(families)] += 1
end
s = ''
fams.each_with_index do |fam, famnr|
fam.times do |i|
s += (97+i).chr + ' ' + (90-famnr).chr + ' ' + '<' + (97+i).chr +
'@' + (90-famnr).chr + ".fam>\n"
end
end
return s.sort_by {rand}
end

class Object
def deep_clone
Marshal::load(Marshal.dump(self))
end
end

class Families < Array
def sum_persons
s = 0
self.each { |el| s+= el.persons.size}
return s
end
def get_person(exceptfamily='')
self.find_all{ |fam| fam.name != exceptfamily
}.sort.reverse.first.persons.shift
end
end

class Family
include Comparable
attr_accessor :name, :persons
def initialize(name)
@name = name
@persons = Array.new
end
def to_s
s = "Family: #{@name}\n"
persons.each {|p| s+= " " + p.to_s + "\n"}
return s
end
def <=>(other)
persons.size <=> other.persons.size
end
end

class Person
include Comparable
attr_accessor :name, :family, :email
def initialize(args)
@name = args[0]
@family = args[1]
@email = args[2]
end
def to_s
return [@name, @family, @email].join(" ").strip
end
def <=>(other)
tmp = family <=> other.family
tmp = name <=> other.name if tmp == 0
return tmp
end
end

def read_data(input)
puts "Input\n#{input}\n"
fams = Families.new
persons = Families.new
input.each do |line|
persons.push Person.new(line.split(/\s/, 3))
fam = fams.find { |fam| fam.name == persons.last.family}
if fam.nil?
fam = Family.new(persons.last.family)
fams.push fam
end
fam.persons.push persons.last unless fam.persons.find{ |pers|
pers.name == persons.last.name}
end
return fams
end

def make_santas(fams)
fams = fams.sort.reverse

santas = fams.deep_clone
santees = fams.deep_clone
s = "List of santas (SANTA - SANTEE)\n"
while santas.sum_persons > 0
begin
santa = santas.get_person
santee = santees.get_person(santa.family)
rescue
end
if (!santa || !santee)
puts "Bad input. Red Sleigh Down"
return
end
s += santa.to_s + ' - ' + santee.to_s + "\n"
end
puts s
end

make_santas(read_data(generate_input()))

#####################
 
J

Joe Cheng

Cristi said:
The solution does the following:
- groups persons by family,
- orders the families descending by number of members
- gets a santa as a member from the family with the most number of members
- for this santa, finds a santee as a member from the family with the
mose number of members, that is not the santa's family
rinse, repeat until there are no more santas and all is ok or, until
we can't find a santee for a santa and decide we can't hook these
people up

This is similar to the approach I took, except that my program ends up
with a circular list where each person gives to the next in the list,
while yours comes up with discrete santa/santee pairs. I think the two
ways probably work equally well...
 
N

Niklas Frykholm

My solution.

--------------------------------------------------

SENDMAIL = "/usr/sbin/sendmail -oi -t"

class Array
def random_member(&block)
return select(&block).random_member if block
return self[rand(size)]
end
def count(&block)
return select(&block).size
end
end

class String
def clean_here_string
indent = self[/^\s*/]
return self.gsub(/^#{indent}/, "")
end
end

class Person
attr_reader :first, :family, :mail
def initialize(first, family, mail)
@first, @family, @mail = first, family, mail
end
def to_s() "#{first} #{family} <#{mail}>" end
end

class AssignSanta
def initialize(persons)
@persons = persons.dup
@santas = persons.dup
@families = persons.collect {|p| p.family}.uniq
@families.each {|f| raise "No santa configuration possible" if
santa_surplus(f) < 0}
end

# Key function -- extra santas available for a family
# if this is negative -- no santa configuration is
possible
# if this is 0 -- next santa must be assigned to
this family
def santa_surplus(family)
return @santas.count {|s| s.family != family} - @persons.count
{|p| p.family == family}
end

def call
while @persons.size() > 0
family = @families.detect {|f| santa_surplus(f)==0 and
@persons.count{|p| p.family == f} > 0}
person = @persons.random_member {|p| family == nil ||
p.family == family}
santa = @santas.random_member{ |s| s.family != person.family }
yield(person, santa)
@persons.delete(person)
@santas.delete(santa)
end
end
end

def mail(from, to, subject, message)
File.popen(SENDMAIL, 'w') { |pipe|
pipe.puts( (<<-HERE).clean_here_string)
From: #{from}
To: #{to}
Subject: #{subject}

#{message}
HERE
}
end

def main
persons = []
STDIN.each_line { |line|
line.scan(/(\w+)\s+(\w+)\s+<(.*)>/) {|first, family, mail|
persons << Person.new(first, family, mail)
}
}

AssignSanta.new(persons).call {|person, santa| mail(person, santa,
"Dear santa", "You are my secret santa!")}
end

main
 
P

Peter

[1] Statistics is not my strong suit. My reasoning is as follows:
Consider a pool of 6 participants:
Joe Smith
Jane Smith
Jim Smith
Bob Dole
Barbara Bush
Bill Clinton

The correct arrangement for this pathological case is an interleaving
of Smiths and others, for example:
Jane Smith
Bill Clinton
Joe Smith
Bob Done
Jim Smith
Barbara Bush

I think for James' code, a correct arrangement would be like this:
Bob Dole
Barbara Bush
Bill Clinton
Joe Smith
Jane Smith
Jim Smith

The way you look at it, each person in your arrangement is the next
person's secret Santa, wrapping around the end of the array as needed,
right? Actually that's like putting people in a circle such that each
person's secret Santa is standing to his/her left (or right). But actually
that leaves out the arrangements with multiple smaller circles, so you
can't generate every solution this way. But James' solution does however.
There is a 1/1 chance that SOMEONE will be picked for the first spot.
There is a 3/5 chance that the next person will be of the opposite
Smith/Non-Smith persuasion.
There is a 2/4 chance that the next person will be of the opposite
Non-Smith/Smith persuasion.
There is a 2/3 chance for the next.
There is a 1/2 chance for the next.
There is a 1/1 chance for the last.
The cumulative chance of all that occurring is the product:
3/5 * 2/4 * 2/3 * 1/2 = 1/10

For an 8 person pool, the chances are:
4/7 * 3/6 * 3/5 * 2/4 * 2/3 * 1/2 = 1/35
Re-arranged, that looks like:
(4*3*3*2*2) / (7*6*5*4*3*2 )
which is
(4 * 3*2 * 3*2 ) / ( 7*6*5*4*3*2 )
which is
(4 * 2 * 3! ) / 7!

Looking at this pattern, then, the chances of randomly picking a
working pattern for pathological pool size of N seems to be:

N/2 * 2 * ((N/2)-1)! / (N-1)! == N * ((N/2)-1)! / (N-1)!

The correct generalization is this:

(2 * (N/2)! * (N/2)!) / N!

(This gives the same numbers as above for N = 3 and N = 4)

To see this: there are N! possible permutations of the N persons. The only
valid ones are the interleaved ones. There are 2 choices for the big
family: the odd or the even positions. Then there are two sets of N/2
persons who need to be assigned to N/2 positions each, with no
restrictions, which for each set can be done in (N/2)! ways. The total
number of valid combinations is 2*(N/2)!*(N/2)! out of N!.

For James' way of generating possibilities, he looses a factor 2, so only
(N/2)! * (N/2)! out of N! are OK. But for N = 10, he has 1 chance out of
252 to generate a working pattern. N = 12: 1 chance out of 924. For each
increase in N of 2, the chances get worse by about a factor 4. So it's not
that bad ;-)

Peter
 
N

Niklas Frykholm

An interesting problem. It seems trivial at first, but when you start to
think about whether a solution is possible or not and in which way you
can assign santas as random as possible, but without painting yourself
into a corner it gets quite complex. Here is a mathematical analysis:


At any stage in the solution we have a number of persons from different
families that need to be assigned a santa, and a number of available
santas. We define the "santa surplus number" SS(F) for a family F as:

SS(F) := (number of available santas for this family) -
(number of family members that haven't been assigned a santa)

The available santas for a family are the available santas that doesn't
belong to that family.

It is clear that if SS(F) < 0 for some family then no santa assignment
is possible, because there are not enough santas for that family.

Suppose that SS(F) >= 0 for all families, then a santa assignment is
always possible. To see this, we look at what happens to the SS number
when a santa is assigned.

Suppose that a person from family F is assigned a santa from family S.
Let O be any other family. The assignment will cause the following
changes to the SS numbers.

SS(F) := SS(F)

The family F looses an available santa, but the number of santas
needed is also reduced by one, so SS stays the same.

SS(S) := SS(S)

The S family does not loose an available santa because the santa
comes from the S family and could never be a santa for the S
family.

SS(O) := SS(O) - 1

All other families loose an available santa.

If SS(O) = 0, for some family O, then the assignment will lead to an
impossible configuration, because SS(O) will become negative. This gives
us the rule for assigning santas without creating an impossible
configuration:

If SS(F) = 0 for some family F, then in the next santa assignment,
either the santa or the person that is assigned a santa must come
from the family F.

We can always make our santa assignments so that they follow this rule
*unless* there are three or more families with SS(F) = 0. But if SS(F)
= 0 for all families, there can never be three families with SS(F) = 0.

To see this, let N be the total number of persons without santas (and
also the number of available santas). Let S(F) be the number of
available santas from family F and P(F) be the number of persons without
santas from F, then

SS(F) = (N - S(F)) - P(F) = N - S(F) - P(F)

So if SS(F) = 0 then:

N = S(F) + P(F)

Suppose that there are two families A and B with SS(A) == 0 and SS(B) ==
0 then:

N = S(A) + P(A)
N = S(B) + P(B)

2N = S(A) + S(B) + P(A) + P(B)

(N - S(A) - S(B)) + (N - P(A) - P(B)) = 0

N - P(A) - P(B) = 0 (since N - S(A) - S(B) >=0)
N - S(A) - S(B) = 0 (and N - P(A) - P(B) >= 0)

N = P(A) + P(B)
N = S(A) + S(B)

In other words, then all santas and all persons come from those two
families. So if two families have SS(F) = 0, there can be no other
families with unassigned santas.

Since there will always be at most two families with SS(F) = 0, we can
always select a santa according to the rule above. And we MUST select a
santa according to that rule in order to not create an impossible
situation. This means that if we randomize the choice of santa while
following the rule, we get a selection that is as random as possible
while obeying the constraints.

// Niklas
 
C

Carlos

An interesting problem. It seems trivial at first, but when you start to
think about whether a solution is possible or not and in which way you
can assign santas as random as possible, but without painting yourself
into a corner it gets quite complex. Here is a mathematical analysis:
[...]

I didn't participate, but I thought it was a trivial case of sorting the
families by number of members and start giving santas from the most populous
down. I should have tested that theory with code, but I didn't have time on
the weekend :(. How can it not work?

Something like this: (NOT TESTED)

Participant = Struct.new :name, :family, :email
participants = []


while line = gets
line =~ /^(\w+) (\w+) (.*+)$/
participants << Participant.new($1, $2, $3)
end

families = {}
participants.each do |p|
families[p.family] ||= []
families[p.family] << p
end

families = families.values.sort_by{ |a| a.size }.reverse

families.each do |family| family.each do |member|
idx = rand(participants.size) until participants[idx].family != member.family
print member.family, "->", participants[idx].family
participants.delete_at idx
end end

--
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,581
Members
45,056
Latest member
GlycogenSupporthealth

Latest Threads

Top