Skip to content

Instantly share code, notes, and snippets.

@DonaldHays
Created September 26, 2015 20:06
Show Gist options
  • Save DonaldHays/70a779c56e3a537e8475 to your computer and use it in GitHub Desktop.
Save DonaldHays/70a779c56e3a537e8475 to your computer and use it in GitHub Desktop.
Solve's the Secret Santa problem at http://inessential.com/2015/09/25/a_secret_santa_solution
import Foundation
/// Represents a person for the purposes of Secret Santa pairing.
struct Person: Equatable, CustomStringConvertible {
/// The person's given name.
var givenName: String
/// The person's family name.
var familyName: String
/// The person's email address.
var email: String
var description: String {
return "\(givenName) \(familyName)"
}
/// Returns true if the receiver can be a Secret Santa for another person,
/// or false if they cannot. Someone can be another person's Secret Santa
/// if the two people are not the same person, and if they aren't from the
/// same family.
func canGift(other: Person) -> Bool {
// Someone cannot gift themselves, and they cannot gift another member
// of the same family. Both rules are satisfied by checking family name.
return familyName != other.familyName
}
}
func == (lhs: Person, rhs: Person) -> Bool {
return lhs.givenName == rhs.givenName && lhs.familyName == rhs.familyName && lhs.email == rhs.email
}
extension Array {
/// Returns a random element from the receiver, or `nil` if the receiver is
/// empty.
var any: (Int, Element)? {
if count == 0 {
return nil
}
let index = Int(arc4random_uniform(UInt32(count)))
return (index, self[index])
}
/// Returns a copy of the receiver that excludes the element at the
/// specified index.
func withoutElementAtIndex(index: Int) -> [Element] {
var newElements = self
newElements.removeAtIndex(index)
return newElements
}
}
/// Picks Secret Santa matches by pairing gifters with recipients. Returns an
/// array of matches such that no gifter will be matched with themself, and no
/// gifter will be matched with another member of the same family. If there is
/// no valid set of matches, returns `nil`.
///
/// This method solves the problem recursively by picking a random gifter, and
/// then attempting to match against random recipients, seeing if the remaining
/// sets of gifters and recipients can produce a valid list of matches. If a
/// choice for a gifter ultimately cannot be matched with any candidate
/// recipient, then the problem is unsolvable.
func match(gifters gifters: [Person], recipients: [Person]) -> [(gifter: Person, recipient: Person)]? {
// Once there are no more gifters, we have picked everyone, return empty
// array (indicating successful end of chain)
guard gifters.count > 0 else {
return []
}
// Grab a random gifter.
guard let (gifterIndexInGifters, gifter) = gifters.any else {
fatalError("Should've been able to grab a gifter that exists in gifters. Programming error.")
}
let remainingGifters = gifters.withoutElementAtIndex(gifterIndexInGifters)
// Make a list of candidate giftable recipients, and pick from the list
// randomly, seeing if the remaining gifters and recipients can validly
// match.
var candidates = recipients.filter { gifter.canGift($0) }
while candidates.count > 0 {
// Pick the candidate.
guard let (candidateIndexInCandidates, candidate) = candidates.any, candidateIndexInRecipients = recipients.indexOf(candidate) else {
fatalError("Should've been able to grab a candidate that exists in candidates and recipients. Programming error.")
}
// Once we pick a candidate, either we will be able to make valid
// matches from the remaining candidates, or this candidate cannot be
// matched with this gifter. Either way, remove the candidate from the
// candidate list.
candidates.removeAtIndex(candidateIndexInCandidates)
let remainingRecipients = recipients.withoutElementAtIndex(candidateIndexInRecipients)
// If calling match on the remaining gifters and recipients produces an
// array, we have a valid match, otherwise the match is invalid and we
// should try the next candidate.
if let matches = match(gifters: remainingGifters, recipients: remainingRecipients) {
return [(gifter: gifter, recipient: candidate)] + matches
}
}
// If we exhaust all candidates, then there are no valid matches for our
// gifter. We don't have to try any other gifters because if even one can't
// match, the problem we've been given is unsolvable.
return nil
}
// Not exactly parsing input from standard in, but that's not the interesting
// part of this problem anyway.
let people = [
Person(givenName: "Luke", familyName: "Skywalker", email: "luke@theforce.net"),
Person(givenName: "Leia", familyName: "Skywalker", email: "leia@therebellion.org"),
Person(givenName: "Toula", familyName: "Portokalos", email: "toula@manhunter.org"),
Person(givenName: "Gus", familyName: "Portokalos", email: "gus@weareallfruit.net"),
Person(givenName: "Bruce", familyName: "Wayne", email: "bruce@imbatman.com"),
Person(givenName: "Virgil", familyName: "Brigman", email: "virgil@rigworkersunion.org"),
Person(givenName: "Lindsey", familyName: "Brigman", email: "lindsey@iseealiens.net")
]
// Match the array of people against itself. If the function returns nil, then
// the set of people we have is impossible to match.
guard let matches = match(gifters: people, recipients: people) else {
fatalError("Impossible to match Secret Santas")
}
// Print out the Secret Santa matches.
for match in matches {
print("\(match.gifter): You're \(match.recipient)'s Secret Santa!")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment