sint, math, handwaiving
HELSINKI - Nadat Sinterklaas ons in Madrid ternauwernood ontkomen was, hopen we 'm volgende week hier in Helsinki te vinden. We proberen het traditionele feest ook buiten de Nederlandse landsgrenzen te populariseren, om te beginnen in Finland. We hebben dan ook al een feestelijke avond gepland, volgende dinsdag 5 december, waarbij de gasten elk een lootje krijgen met daarop de naam van de degene voor wie ze een cadeautje moeten kopen. Met een gedicht, uiteraard.
de laatste lootjes
Het trekken van de lootjes is natuurlijk bij uitstek een taak voor de computer. We kunnen het resultaat van het lootjestrekken voorstellen als een relatie R, met elementen (x,y), waarbij (x,y) betekent "x krijgt het lootje van y". Ik heb in eerste instantie twee eisen aan het eindresulaat, R:- niemand kan zijn eigen lootje trekken: (x,y) ∈ R → x ≠ y (irreflexief);
- als x het lootje trekt voor y, dan mag y niet het lootje trekken voor x: (x,y) ∈ R → (y,x) ∉ R (asymmetrisch).
Hoe kan ik dit probleem oplossen? Het resultaat R bestaat uit n elementen, en ik moet kiezen uit een totaal aantal mogelijkheden van n!. Bij 10 deelnemers zijn er 362880 mogelijkheden, zo vertelt emacs mij:
(defun fac (n) (if (= 1 n) n (* n (fac (- n 1))))) (fac 10) => 3628800Da's behoorlijk veel! Ik kan de lootjes voorstellen als een lijst met namen a,b,c,d,e. Als ik die lijst 'randomiseer' (ie. schud), krijg ik bijvoorbeeld:
b | a | d | e | c |
c | b | a | d | e |
R = {(b,c), (a,b), (d,a), (e,d), (c,e)}, ofwel (x,(x+1) mod n)
Kan ik er nu zeker van zijn dat deze lijst aan mijn eisen (1) en (2) voldoet?
- Een paar wordt beschreven als (x,(x+1) mod n); derhalve is de vraag wanneer geldt: x ≠ (x + 1) mod n. Dat geldt altijd, zolang n > 1;
- Eis (2) stelt dat (x,y) ∈ R → (y,x) ∉ R. Laten we eens bekijken wanneer (y,x) wel ∈ R. Voor y kunnen we x+1 mod n invullen, en we krijgen dan dus (x+1+1 mod n,x). Aangezien dat paar moet voldoen aan (x, x + 1 mod n) geldt dat voor het eerste element van het paar: (x + 2) mod n = x, wat alleen oplossingen heeft voor n > 2. Het tweede element gaat zoals bij eis (1); eindconclusie: eis 2 wordt voldaan, zolang n > 2.
implementatie
Ik wilde de implementatie eigenlijk in Haskell (zie onder) doen, maar kwam uiteindelijk toch bij Ruby terecht. Da's simpel; de kern bestaat uit twee simpele functies, shuffle 'schudt' mijn lijst met namen, en rcs_pairs genereert de lijst met paren uit de corresponderende lijst en de verschoven lijst.srand # seed the random num generator # shuffle the lst in random order def shuffle (lst) return lst.sort{|x,y| 0.5 <=> rand } end # make a list of pairs from a list # by pairing each item n in the list with item n # in a n-element cyclically shifted version of the same list def rcs_pairs (lst,n) pairs = [] 0.upto((lst.length)-1) do |i| pairs.push([lst[i], lst[(i+n)%lst.length]]) end return pairs endDe invoer voor het programma komt vanaf stdin, een naam per regel:
donald katrien george barbara peter lois rupertWe kunnen die name simpel in een lijst lezen, en de resultaten printen:
lst = [] # read strings from stdin while gets lst.push($_.chomp) end # give me the results! tickets = rcs_pairs(shuffle(lst), 1) tickets.each {|n| print "#{n[0]} -> #{n[1]}\n"}en klaar is kees..., de uitvoer:
% ruby tickets.rb <> rupert rupert -> donald donald -> george george -> barbara barbara -> peter peter -> katrien katrien -> lois
Of? Ik bedacht me nog een derde nuttige eis: partners mogen elkaar niet kiezen. Als bijv. in het bovenstaande george en barbara partners zijn, zouden ze niet elkaars lootje mogen trekken. Oftewel, eis nummer 3:
3. (x,y) ∈ R → not P(x,y) , waarbij het predicaat P(x,y) betekent dat x en y partners (in de intermenselijke zin) zijn.
hack
Dat zou ik natuurlijk ook wel in mijn model kunnen opnemen, maar... ik ben geen drs. of dr. maar slechts een eenvoudige ir. en derhalve zoek ik naar een eenvoudige, werkende oplosing... Mijn hack is de volgende: normaalgesproken verwacht mijn program een naam per regel vanaf stdin. Als ik nu eens de beide partners op dezelfde regel zet? De invoer ziet er dan uit als:donald katrien george barbara peter lois rupertRuby staat heterogene lijsten toe, en ik kan dus in plaats van enkelvoudige namen daarnaast ook (a,b)-paren in dezelfde lijst zetten; de inleesroutine wordt nu:
lst = [] # read strings from stdin, version 2 while gets #lst.push($_.chomp.split) end(de 'split' splitst de namen in paren)
Vervolgens sorteer ik de lijst zoals tevoren, en dan 'flatten' ik de lijst. Dat wil zeggen dat ['rupert', ['peter', 'lois'] .... ] nu gewoon ['rupert', 'peter', 'lois' ... ] wordt. Maar omdat ik de dat pas na het schudden doe, weet ik zeker dat de beide partners nog steeds naast elkaar staan in de lijst.
De rest van de oplossing ligt voor de hand: in plaats van de tweede lijst nu een plek te verschuiven, verschuif ik 'm twee plekken. Dan kan niemand meer het lootje van zijn of haar partner trekken (zolang n > 3, analoog aan wat ik boven aannemelijk maak):
# print the pairs, version 2 tickets = rcs_pairs(shuffle(lst).flatten,2) tickets.each {|n| print "#{n[0]} -> #{n[1]}\n"}Het totale script wordt dan:
#!/usr/bin/ruby #Time-stamp: <2006-11-28> # script that takes a list of n newline-separated strings from stdin; # and print n pairs P of those names, with the following properties: # # (a,b) -> a != b ,n > 1: no element is paired with itself # (a,b) E R -> (b,a) ! E R ,n > 2: ie. if (a,b) is part of the set of pairs, # then (b,a) is not # (a,b) are not in some human relationship; # a relationship is indicated by having the two names on the same row # in the input (separated by a ' '), and the script will make sure that # (a,b) !E R and (b,a) !E R srand # seed the random num generator # shuffle the lst in random order def shuffle (lst) return lst.sort{|x,y| 0.5 <=> rand } end # make a list of pairs from a list # by pairing each item n in the list with item n # in a one-element cyclically shifted version of the same list def rcs_pairs (lst,n) pairs = [] 0.upto((lst.length)-1) do |i| pairs.push([lst[i], lst[(i+n)%lst.length]]) end return pairs end lst = [] # read strings from stdin while gets lst.push($_.chomp.split) #lst.push($_.chomp) end # print the pairs #tickets = rcs_pairs(shuffle(lst),1) tickets = rcs_pairs(shuffle(lst).flatten, 2) tickets.each {|n| print "#{n[0]} -> #{n[1]}\n"}En dat geeft me de gewenste lijst:
% ./tickets.rb <> peter katrien -> lois peter -> george lois -> barbara george -> rupert barbara -> donald rupert -> katrienTerzijde: ik wilde dit eigenlijk in Haskell implementeren (met hugs), maar dat bleek wat lastig - eigenlijk om een enkele reden: het verkrijgen van random-getallen in Haskell is niet mogelijk zonder in te gaan op monads. Het opvragen van een random-getal verandert echter de toestand (entropie etc.) van psuedo-randomgenerator, en aangezien puur-functionele talen zoals Haskell nogal nerveus worden van dit soort side-effects, heeft men het monad-concept geïntroduceerd. Maar dat bewaar ik voor een andere keer...