Drop links or images here to add them to the editor.

Het spel Six-Seven! begint met twee stapels waarin 6 en 7 munten liggen — een getallenpaar dat de jongerencultuur lijkt te zijn binnengedrongen met de elegantie van een baksteen die door een raam gegooid wordt. Onze welgemeende excuses als het feit dat ze hier verschijnen enige vorm van irritatie opwekt.

six-seven

Het spel wordt gespeeld door twee spelers die afwisselend aan de beurt komen. De speler die aan de beurt is, gooit met een dobbelsteen. Als hij $$r$$ ($$1 \leq r \leq 6$$) werpt, dan moet hij tussen 1 en $$r$$ munten wegnemen uit één stapel die niet leeg is (of alle resterende munten als die stapel minder dan $$r$$ munten bevat). Bij een worp van 4 mag de speler bijvoorbeeld 1, 2, 3 of 4 munten wegnemen. De speler die de laatste munt wegneemt, is de verliezer.

Wat is de kans dat de speler die als eerste aan de beurt komt het spel wint, als we ervan uitgaan dat beide spelers een optimale strategie volgen?

De kans dat de eerste speler wint is $$\frac{5147}{8748}$$.

Opgave

Breuk

Een breuk is een rationaal getal $$\frac{t}{n}$$ met teller $$t \in \mathbb{Z}$$ en noemer $$n \in \mathbb{Z}_0$$.

De normaalvorm van een breuk $$\frac{t}{n}$$ is de volledige vereenvoudigde versie van de breuk die een positieve noemer heeft. Om een breuk volledig te vereenvoudigen, moet je de teller $$t$$ en de noemer $$n$$ delen door hun grootste gemene deler. Als een breuk een negatieve noemer heeft, kan je zowel de teller als de noemer vermenigvuldigen met -1 (met andere woorden: als de breuk negatief is, dan staat het minteken bij de teller).

Tip: grootste gemene deler berekenen

De grootste gemene deler van twee gehele getallen $$a$$ en $$b$$ kan je berekenen met het algoritme van Euclides:

function ggd(a, b)
    while b ≠ 0
        t := b
        b := a mod b
        a := t
    return a

Definieer een klasse Breuk waarmee breuken $$\frac{t}{n}$$ kunnen voorgesteld worden. Een nieuwe breuk (Breuk) kan aangemaakt worden met de volgende alternatieven voor de argumenten:

Als er bij het aanmaken van een nieuwe breuk andere argumenten worden doorgegeven dan de hierboven opgelijste mogelijkheden, dan moet een Error opgeworpen worden met de boodschap ongeldige breuk.

Op een breuk $$b$$ (Breuk) met teller $$t$$ en noemer $$n$$ moet je minstens de volgende methoden kunnen aanroepen:

Als geen nieuwe breuk kan aangemaakt worden met het argument dat wordt doorgegeven aan de methoden plus, min, maal, gedeeld of max, dan moet een Error opgeworpen worden met de boodschap ongeldige breuk.

Winstkans

Schrijf een functie winstkans waaraan twee argumenten $$a, b \in \mathbb{N}$$ (Number) moeten doorgegeven worden. De functie moet de kans $$P(a, b)$$ (Breuk) teruggeven dat de speler die aan de beurt is in het spel Six-Seven! wint, als er aan het begin van de beurt $$a$$ munten in de linker stapel liggen, $$b$$ in de rechter stapel, en beide spelers een optimale strategie volgen.

Deze kans kan berekend worden met de recurrrente betrekking \[ P(a, b) = \frac{1}{6}\sum_{r=1}^{6}\max\!\left(\max_{1 \le k \le \min(r,a)}\bigl(1 - P(a-k,b)\bigr),\;\max_{1 \le k \le \min(r,b)}\bigl(1 - P(a,b-k)\bigr)\right) \] De stopvoorwaarde \[ P(0, 0) = 1 \] volgt uit de misère-regel ("wie de laatste munt wegneemt verliest"), want als er aan het begin van een beurt geen munten meer over zijn, heeft de tegenstander de laatste munt weggenomen en is hij dus verloren. Meer algemeen, als enkel de rechter stapel leeg is dan geldt voor $$a > 0$$ dat \[ P(a, 0) = \frac{1}{6}\sum_{r=1}^{6}\max_{1 \le k \le \min(r,a)}\bigl(1 - P(a-k,0)\bigr) \] en als enkel de linker stapel leeg is dan geldt voor $$b > 0$$ dat \[ P(0, b) = \frac{1}{6}\sum_{r=1}^{6}\max_{1 \le k \le \min(r,b)}\bigl(1 - P(0,b-k)\bigr) \]

Bij een recursieve implementatie van $$P(a, b)$$ moet dezelfde kans $$P(a', b')$$ meerdere keren berekenen voor $$0 \leq a' \leq a$$ en $$0 \leq b' \leq b$$. Om dat te vermijden, berekenen we achtereenvolgens alle $$P(a', b')$$ voor $$0 \leq a' \leq a$$ en $$0 \leq b' \leq b$$ in de volgende van de groene pijl in onderstaand schema: $$P(0, 0)$$ is gegeven, daarna bereken we achtereenvolgens $$P(0, 1), P(0, 2), \ldots, P(0, b)$$, daarna achtereenvolgens $$P(1, 1), P(1, 2), \ldots, P(1, b)$$, enzoverder tot we finaal $$P(a, b)$$ berekenen.

dynamisch programmeren

Als we alle tussenresultaten opslaan, dan kan elke kans $$P(a', b')$$ volgens de recurrente betrekkingen berekend worden op basis van kansen $$P(a'', b'')$$ die reeds berekend werden: er geldt altijd dat $$0 \leq a'' \leq a'$$, $$0 \leq b'' \leq b'$$ en $$(a'',b'') \neq (a', b')$$. Omdat geldt dat $$P(a', b') \equiv P(b', a')$$, moeten we ook enkel de kansen $$P(a', b')$$ berekenen waarvoor $$a' \leq b'$$.

Voorbeeld

> const breuk1 = new Breuk(2, 6)
> breuk1.teller
1
> breuk1.noemer
3
> breuk1.toString()
"1/3"
> breuk1.abs.toString()
"1/3"
> breuk1.neg.toString()
"-1/3"
> breuk1.inv.toString()
"3/1"

> const breuk2 = new Breuk(1, 2)
> breuk1.plus(breuk2).toString()
"5/6"
> breuk1.min(breuk2).toString()
"-1/6"
> breuk1.maal(breuk2).toString()
"1/6"
> breuk1.gedeeld(breuk2).toString()
"2/3"
> breuk1.max(breuk2).toString()
"1/2"

> const breuk3 = winstkans(6, 7)
> breuk3 instanceof Breuk
true
> breuk3.toString()
"5147/8748"
> winstkans(5, 8).toString()
"39281/69984"
> winstkans(9, 9).toString()
"20526139/45349632"