Kommentarer
och lösningar till uppgifterna i
Finalen i Programmeringsolympiaden 2002.
Lösningarna är
skrivna i C/C++ och följer tävlingsstandarden på så
sätt att de är helt okommenterade och obegripliga. I några
fall används stdin istället för läsning från fil.
Uppgifterna (PDF)
Testdata (ZIP)
Uppgift 1 - Nattlig
förflyttning
Ett enkelt sätt att
lösa uppgiften är med rekursion (s.k. backtracking). I varje steg
kommer alltid två personer att gå över, varefter en person
går tillbaka med ficklampan om det finns några personer kvar
att hämta. Om man kommer på att det alltid är den snabbaste
personen som ska gå tillbaka så hinner man gå igenom alla
möjliga sätt att göra förflyttningen, eftersom det då
bara finns 21*15*10*6*3 = 56700 sätt (2 av 7 kan väljas på
21 sätt, 2 av 6 på 15 sätt o.s.v.)
Detta är naturligtvis
en väldigt dålig metod om man har många personer, eftersom
tidsåtgången för programmet när man har n personer
ungefär blir proportionell mot (n!)^2. Exempelvis fungerar det inte
för 10 personer. Vi fick in många bättre bidrag av typen
dynamisk programmering, vilket fungerar bra eftersom antalet "tillstånd"
bara är 2^8.
Lösningsförslag
Uppgift 2 - Ordlek
Detta är ett typexempel
på backtracking. Man har en lista på vilka bokstäver som
finns kvar att använda. Så testar man först vilken bokstav
som kan sättas som första bokstav i ordet som ska bildas. När
man hittat en giltig bokstav stryker man den från listan och anropar
samma funktion igen (rekurserar) fast nu börjar man från andra
bokstaven i ordet o.s.v. Varje gång man placerar en bokstav måste
man kolla att stavningsreglerna uppfylls. När man sätter dit A
eller G, de bokstäver som kan vara sista bokstav i ordet, kollar man
om det ord som har bildats är längre än det tidigare längsta
ordet. När man har testat alla möliga bokstäver på en
position, går man tillbaka till föregående position, sätter
tillbaka den bokstaven i listan och fortsätter med nästa testbokstav
på den nivån. Allt detta kan skötas med en rekursiv funktion,
eller som i lösningsförslaget några olika för folofjant,
pedal respektive valfri bokstav.
Lösningsförslag
Uppgift 3 - Wildcards
Gå igenom alla städerna
i tur och ordning och se om de matchar söksträngen. Att göra
själva matchningen tillhör praktiskt sett de viktigaste algoritmiska
problemen (används t.ex. inom bioinformatik) och har genererat mängder
av intressanta metoder som kan vara värda att sätta sig in i. Eftersom
stränglängden i denna uppgift var tämligen begränsad,
räcker det dock med en rekursiv funktion av samma typ som i uppgift
2. Om en bokstav matchar exakt eller om söksträngstecknet är
? är det bara att gå vidare med nästa bokstav. Om söksträngstecknet
är * så kan man t.ex. testa att hoppa fram alla möjliga steg
i namnet (0, 1, 2 o.s.v. ), vilket mosvarar att asterisken får ersätta
det antalet tecken. Om man efter ett antal steg finner att namnet och sökstränget
slutar samtidigt så har man hittat en sökträff.
Lösningsförslag
Uppgift 4 - Alfametik
Lämpligen så
börjar man med att lokalisera de unika bokstäverna (max 10 st)
och därefter lagra dessa in en liten tabell. Sedan tittar på alla
möjliga kombinationer att placera ut siffrorna 0-9 på de 10 bokstäverna.
Antalet
möjliga sådana
permutationer är 10! = 10*9*8*7*6*5*4*3*2*1 = 3628800 vilket med dagens
datorer inte tar lång
tid. För att gå igenom alla permutationer kan man antingen
använda rekursion
eller, om man programmerar i C++, STL funktionen next_permutation. För
varje permutation kontrollerar man sedan om additionen stämmer samt
att inga bokstäver som är först på en rad blev tilldelad
en nolla. Exempellösningen
är lite överkomplicerad eftersom den även tillåter siffror
i termerna/summan.
Lösningsförslag
Uppgift 5 - Motorvägar
Denna uppgift löser
man genom att använda sig av ett minimalt uppspännande träd
(se
träningssidan
). Det som kan vara förvillande
är att vissa vägar redan är motorvägar, vilket gör
att den uppspännande grafen inte nödvändigtvis är ett
träd. Likväl är det samma algoritm man använder, t ex
Kruskal eller Prims algoritm.
Lösningsförslag
Uppgift 6 - Ett
markant problem
Detta var tävlingens
svåraste uppgift, vilket var planerat. Om storleken på området
hade varit mindre hade uppgiften varit betydligt enklare. Då kan man
lagra hela området i en stor matris och genomföra flera stycken
floodfills. Dvs, man börjar i en ruta i matrisen som inte ännu
är fylld, "fyller" området (med en DFS eller BFS sökning)
tills man stöter på staket samt lagrar hur många rutor
man fyllde. Detta upprepas tills hela området blivit fyllt, och man
har då storleken
på alla områden.
Men nu kunde området
vara väldigt stort, och att skapa en matris på hela området
på en gång är inte möjligt. Men antalet staket i området
är litet, max 50 st, vilket innebär att antalet unika x resp. y
koordinater som används av staketen är max
100. Med andra ord kan
området skalas ner till ett område med storleken 100x100. Därefter
används algoritmen
ovan, och vid beräknandet av arean så skalar man upp storleken
på rutorna i matrisen.
Lösningsförslag
Uppgift 7 - Snöplogning
Här var det meningen
att man skulle använda sig av dynamisk programmering (se
träningssidan
). Detta är möjligt
tack vare de enkelriktade huvudgatorna. Om man har räknat ut den "maximala
vägen" för varje tidsåtgång när man kommit n steg
i östlig riktning, så kan man direkt räkna ut de maximala
vägarna till korsningarna n+1 steg i östlig riktning, eftersom
man vet att man aldrig kommer att åka tillbaka åt väster.
Låt T(x, y, t) beteckna
maximala nyttan för vägen med tiden t till koordinat (x, y) samt
h(x, 0), h(x, 1) och m(x) nyttorna för den södra huvudgatan, norra
huvudgatan respektive tvärgatorna. Från början ses att T(0,0,0)
= 0, T(0,1,1) = m(0) och T(0,0,2) = m(0). För varje x-koordinat börjar
man med att ta huvudgatorna dit, d.v.s. T(x, y, t) = T(x-1, y, t-1) + h(x,
y). Sedan ska man åka på tvärgatorna och då finns
det fyra sätt att åka: norrut, söderut, norrut + söderut
och söderut + norrut (notera att det aldrig är någon idé
att åka mer än två gånger på samma tvärgata).
Det enklaste är att ta de fyra fallen i tur och ordning. Det tredje
fallet leder till exempel till T(x, 0, t) = T(x, 0, t-2) + m(x) eftersom
man bara tjänar nytta åt ena hållet. Det man sparar i T
är naturligtvis maximum för alla fallen. Det gäller också
att hålla tungan rätt i mun så att man inte råkar
skriva över något värde som man sen har tänkt att använda.
I exempellösningen används en kompaktare tabell med antalet "omvägar"
istället för tiden som sista index till T, men det är naturligtvis
valfritt.
Det finns andra sätt
att lösa uppgiften. Exempelvis kan man gå rekursivt och bara kolla
i varje "tillstånd" (x, y, t) om man har varit där innan och då
haft bättre nytta, i så fall är det ju hopplöst och
man ska backtracka istället. Man kan visa att det värsta teoretiska
fallet då blir ca 200 miljoner rekursiva anrop, vilket fortfarande
går att klara inom tiden. Tyvärr var det många som gjorde
rekursiva lösningar utan denna optimering. Då blir det värsta
fallet upp mot 2^200, vilket tar ett tag.
..
Lösningsförslag
Uppgift 8 - Flyken
till vadstället
Denna uppgift är ett
typexempel på ett kortaste-vägen problem (i det här fallet
egentligen snabbaste-vägen, men det är samma sak). Effektivast
löser man ett sådant problem genom att använda sig av Dijkstras
algoritm (se
träningssidan
) men eftersom grafen var
väldigt speciell så gick den att lösa med rekursion och
dynamisk programmering om man var lite klurig.
Lösningsförslag