Superieure Streak

Superieure Streak

[ooO]

Een wiskundige doet de afwas - 23

De familie Van der Torus is een heel normale, gemiddelde familie. Voor zover een familie van wiskundigen normaal kan zijn. Ze komen allerlei alledaagse problemen tegen. Kom je zelf uit een wiskundig gezin of ben je een (mogelijk toekomstige) wiskundige, dan kun je je ervaringen, vragen en ideeën delen met de familie Van der Torus via e-mail naar [email protected].

De laatste maanden spelen Milli en Mu een breinbreker op hun telefoon en verzamelen daarbij punten. Dat aantal punten kan negatief zijn, bijvoorbeeld als ze een dag niet gespeeld hebben of fouten maakten. Dit is de reeks scores van Mu over de laatste twaalf dagen:

De app houdt bij wat je beste streak* is: een aaneengesloten periode van dagen met de hoogste totaalscore. Ze wilden graag weten hoe de app dat doet, want met die negatieve scores leek dat best lastig. Mu vond het een voor de hand liggend idee om alle aaneengesloten periodes door te rekenen. Dat deed Mu als volgt in Python (de code is ook te vinden via de knop [Bekijk oplossing] onderaan de pagina):

Let op dat in Python de uitdrukking scores[i:j+1] de scores van dag i tot en met dag j aanduidt, dus zonder j+1. Bij de scores van het voorbeeld komt de streak op 16 punten, wat de som is van [5, -3, 8, -6, 9, -4, 7].

Het programma van Mu is best eenvoudig. Phi kon het echter niet nalaten om te vragen hoe efficiënt het is. Een manier om die efficiëntie te meten is om te tellen hoeveel rekenstappen er nodig zijn. Er zijn veel verschillende soorten rekenstappen, maar je krijgt een goede indruk door te tellen hoe vaak een score in de lijst wordt opgezocht. Milli leek het een goed idee om dat aantal door het programma zelf te laten tellen. Daartoe voegde Milli aan het programma een variabele teller toe, die op $0$ start en die telkens wanneer totaal wordt berekend, verhoogd wordt met j+i-1. Na het verwerken van de twaalf scores uit het voorbeeld staat die teller op $364$. En na het verwerken van het dubbele aantal is dat al opgelopen tot $2600$. Als je handig in wiskunde bent, dan kun je beredeneren dat als er $N$ scores zijn, de teller eindigt op $N(N + 1)(N + 2)/6$, wat altijd een geheel getal is. (Snap je waarom het altijd geheel is?) Dus bij verdubbeling van $N$ wordt het aantal stappen ongeveer $2^3 = 8$ keer zo groot. Voor $N = 1000$ is het aantal stappen al meer dan honderd miljoen.

Milli zag al snel in dat het telkens opnieuw sommeren van een periode veel dubbel werk met zich meebrengt. Want als j één groter wordt, dan is de periode één dag langer en kun je de som van de vorige periode verhogen met de score van de dag die er bij komt. Dat leidt tot het volgende programma, inclusief de teller:

Het programma van Milli heeft voor het verwerken van de twaalf scores uit het voorbeeld nog maar $78$ stappen nodig en voor het dubbele aantal scores $300$. Het is nu wat makkelijker om de formule te vinden: voor $N$ scores kost het $N(N + 1)/2$ stappen. Een verdubbeling van $N$ kost dan nog maar ongeveer $2^2 = 4$ keer zoveel stappen. Dat is een flinke verbetering.

Mu had wat zitten toekijken en er borrelde een sluw idee op om Milli te verslaan. Mu was destijds zeer onder de indruk van binair zoeken, waarbij je het probleem telkens halveert. Hier kan dat niet echt, maar Mu opperde dat als je het probleem in twee helften verdeelt, je die helften apart kan oplossen en dan de oplossingen kan samennemen. Stel je verdeelt scores[i:j] in twee zo gelijk mogelijke delen, zeg links= scores[i:m] en rechts=scores[m:j]. Je bepaalt nu de streak van beide delen en neemt daar de beste van. Het kan zijn dat het nog beter kan door een periode te bekijken die deels in links en deels in rechts ligt. Maar als dat zo is, dan is dat een nietleeg staartstuk van links, samen met een niet-leeg beginstuk van rechts. Het beste staartstuk van links en het beste beginstuk van rechts zijn eenvoudig te bepalen.

Hier is het programma van Mu, met , gedefinieerd als Python functie die je aanroept met streak_binair(scores, 0, len(scores)):

Dat is niet meer een programma waarvan je eenvoudig inziet dat het correct is. Het is een recursief algoritme, dat wil zeggen de functie streak_binair roept zichzelf aan (maar alleen als er meer dan één score is). Is het wel efficiënter? De twaalf scores uit het voorbeeld worden in $56$ stappen verwerkt en die van het dubbele aantal in $136$. Ja, dus. Het is ook niet zo eenvoudig om in te zien dat het totaal aantal stappen bij $N$ scores in de orde van $N \log(N)$ ligt. Verdubbeling van $N$ geeft iets meer dan een verdubbeling van het aantal stappen. En het is dus zelfs behoorlijk wat efficiënter dan Millis programma.

Pi was diep onder de indruk dat Mu dit voor elkaar had gekregen, maar suggereerde dat het toch nog beter kon. Ideaal zou het zijn als elke score maar precies één keer wordt bekeken, want beter dan dat is natuurlijk onmogelijk. Dan kost het voor $N$ scores nog maar $N$ stappen. En verdubbeling van het aantal scores betekent ook verdubbeling van het aantal stappen.

Daar moesten Milli en Mu wel even van slikken. Hoe zou je dat voor elkaar kunnen krijgen? Pi gaf toen als hint dat een belangrijk idee voor een lineaire oplossing al besloten lag in het recursieve programma van Mu, waarin het beste staartstuk een rol speelt. Daarop begon bij Milli iets te dagen. Als je van een beginstuk van alle scores weet wat de beste streak is, en je dat beginstuk uitbreidt met een nieuwe score, dan kan de beste streak die je had, alleen verslagen worden door een staartstuk dat eindigt met die nieuwe score. En het nieuwe beste staartstuk bepalen als je het vorige beste staartstuk al kent is ook eenvoudig. Milli schreef het als volgt op (de teller was nu niet meer nodig):

Waar je voor moet opletten is dat staart + v negatief kan zijn. In dat geval begin je weer met een lege staart. Dat is precies wat de uitdrukking max(0, staart + v) doet. Iedereen was het er snel over eens dat dit programma eenvoudig, elegant en efficiënt is. Maar niet iets waar je meteen op komt.

 

     
   

Volgende keer

Het is je vast opgevallen dat de illustratie van de familie boven elk artikel anders is. Wat is het algoritme achter de volgorde van de familieleden in die illustratie?

   
     

 

footnote* Een streak is de periode van de hoogste score. Wat hier steeds berekend wordt is de score zelf in die periode. Dat is het moeilijkst om te berekenen. Als je die eenmaal hebt is het bepalen van de periode zelf makkelijk.

 

Bekijk oplossing