Lösningar
till uppgifterna i
Kvalet till Programmeringsolympiaden 2005.
Lösningarna
är skrivna i C och en del av dem är tyvärr
okommenterade. Tillsammans med
kommentarerna nedan hoppas jag ändå att de kan vara till
hjälp.
Kom
ihåg
att alla sätt att lösa en uppgift är rätt utom de
som
är fel!
Skriv gärna om ni har frågor, kommentarer, tips om andra
lösningar m.m. Kolla också gärna in träningssajten.
Pär Söderhjelm, par.soderhjelm@teokem.lu.se
Uppgifterna
(PDF) Rättelse: svaren på
testexemplen i uppgift 4 ska vara KALLI och PIKTU.
Testdata (PDF) (tillgängligt inom kort)
Uppgift 1 -
Tomater
Här simulerar man
helt enkelt förloppet dag för dag med hjälp av en array
som talar om huruvida varje tomat är mogen eller ej. Varje dag
kollar man för varje omogen tomat om någon av dess grannar
är mogna. I så fall markerar man den som mogen. En liten
knepighet är att en "nymogen" tomat inte får påverka
sina omogna grannar under samma dag som den själv mognade. Detta
kan lösas antingen genom att ha två olika arrayer för
tomatstatusen före respektive efter den aktuella dagen, eller
genom att, som i mitt lösningsförslag, spara vilken dag som
varje tomat mognade och då endast låta dem som mognade
tidigare påverka.
Lösningsförslag
Uppgift 2 -
Tvåvalsfrågorna
Eftersom antalet
frågor (nf) är begränsat till 12 finns det högst
2^12=4096 olika facit. Därför kan vi gå igenom alla
dessa möjliga facit tills vi hittar ett som för var och en av
deltagarnas svar ger det angivna antalet rätt. Att generera alla
facit kan göras rekursivt genom att ha en funktion som för en
viss fråga sätter först svaret 'j' och sedan anropar
sig själv för nästa fråga, därefter
sätter 'n' och anropar sig själv för nästa
fråga. När funktionen anropar sig själv med alla
frågor redan ifyllda kollar man om facit stämmer. Om man
är lite lat kan man istället använda sig av att varje
tal mellan 0 och (2^nf)-1 direkt ger ett facit om man
översätter det till binär form. Exempelvis, för
nf=2, erhålls 00, 01, 10 och 11, vilket naturligtvis kan
översättas direkt till 'nn', 'nj', 'jn' och 'jj'. Med bitvisa
operatorer blir det hela mycket enkelt, vilket visas i
lösningsförslaget.
Lösningsförslag
Uppgift 3 -
Gräsklippning
En lite udda typ av
uppgift som bygger på att även om gräsklippningen tar
helt olika lång tid varje gång, så får man
samma svar om man tar ett medelvärde över många
klippningar. Praktiskt sett utgör detta inget problem, man
låter en loop gå 10000 gånger, utför en
klippning varje gång och lägger ihop antalet minuter som
varje klippning tar. På slutet delas denna summa med 10000
för att få medelvärdet. Själva klippningen
görs enklast i en loop där varje iteration innebär ett
(tillåtet) steg för gräsklipparen. Notera att varje
steg ska tas oberoende av var gräsklipparen har varit innan,
men programmet måste ändå hålla reda på
vilka rutor som gräsklipparen har varit på för att
kunna avgöra när klippningen är klar. När man tar
ett slumpmässigt steg kan det vara bekvämt att använda
en konstant array med två tal (förflyttning i x- och y-led)
för var och en av de fyra riktningarna.
Lösningsförslag
Uppgift 4 -
Namn
Denna olycksaliga
uppgift är inte så svår egentligen, den blev dock
besvärligare eftersom det var fel på de givna testfallen.
Jag passar på att be om ursäkt för detta misstag.
Man börjar lämpligtvis med att översätta
personnumret till ett ordningstal som anger numrets plats i listan.
Detta kan göras genom att loopa över årets 365 dagar
med en dagräknare. När man kommer till det givna
födelsedatumet multiplicerar man dagräknaren med 1000 och
lägger till de tre sista siffrorna i personnumret. Den andra delen
av uppgiften är att hitta det namn som korresponderar mot detta
ordningstal. Namnsorteringen som nämns i uppgiftstexten
behöver naturligtvis inte göras i praktiken, det är
bättre att generera namnen i bokstavsordning och sluta när
man har kommit till rätt plats i listan (och helst INTE ett steg
för tidigt!). För genereringen kan man använda ett
rekursiv funktion som för en viss bokstavsposition går
igenom alla möjliga bokstäver (de 9 vokalerna om det är
position 2 eller 5, de 20 konsonanterna i övriga fall) och
för varje sådan bokstav anropar sig själv för
nästa position. Man inser lätt att om bokstäverna
gås igenom i alfabetisk ordning i varje steg, så blir
namnen automatiskt sorterade. När funktionen anropas för
position 6 har ett helt namn genererats och en räknare ökas.
Om räknarens position överensstämmer med det sökta
ordningstalet är det bara att avbryta och skriva ut namnet.
Lösningsförslag
Uppgift 5 -
Vända
Detta är ett typiskt
"sökningsproblem", ganska vanligt förekommande på
programmeringsolympiaden, läs vidare på vår träningssajt. Typiskt så finns det en
startposition
och en slutposition som det gäller att uppnå med så
få "drag" som möjligt. Hur svårt ett
sökningsproblem är beror inte så mycket på hur
dragen ser ut, utan i första hand på hur många
"tillstånd" som finns, d.v.s. hur många olika
spelpositioner som kan uppstå totalt. Om antalet tillstånd
är mindre än vad man får plats med i minnet (i
storleksordningen en miljon), går det alltid att lösa
problemet effektivt med s.k. bredden-först-sökning (BFS).
Denna sökning bygger på två principer, dels att man
aldrig går till
samma tillstånd flera gånger (man sparar var man har
varit), och dels att man besöker tillstånden i
"avståndsordning", d.v.s. först besöker man alla
tillstånd som kan nås med ett drag från
startpositionen, därefter alla med två drag o.s.v.
Därmed är man garanterad att hitta den kortaste vägen
till slutpositionen, och tidsåtgången är
jämförbar med antalet tillstånd. Antalet
tillstånd i detta problem är N*2^(N-1), d.v.s. maximalt 448,
vilket gör sökningen väldigt effektiv. Det knepiga med
att använda BFS i detta fall är att man måste kunna
översätta varje position till ett heltal för att kunna
spara om man har varit där innan. Detta kan göras genom att
skapa ett binärt tal med brickornas färger (0 för svart,
1 för vit) samt baka in information om var den tomma platsen finns.
Lösningsförslag (BFS)
Vill man undvika krångel med numrering av tillstånden, kan
man tillgripa den andra generella sökningsmetoden,
djupet-först-sökning (DFS), ofta kallad backtracking.
Från en viss ställning gör en rekursiv funktion alla
tänkbara drag, och för varje drag anropar den sig själv
med den nya ställningen. Resultatet blir att man utforskar alla
fortsättningar på ett visst förstadrag innan man
utför nästa förstadrag, man söker alltså
på djupet istället för på bredden. Då
är man naturligtvis inte längre garanterad att hitta den
kortaste dragserien till slutpositionen först, utan man
måste söka igenom alla olika alternativ och hela tiden
hålla reda på det bästa resultatet. Däremot
bör man naturligtvis aldrig fortsätta framåt i en
dragserie som redan har fler drag än det bästa resultatet
hittills. Därför är det viktigt att ha en övre
gräns på antalet drag, eftersom man annars kan börja
gå runt i loopar och aldrig komma fram till slutpositionen. Om
man inte vet någon övre gräns kan man alltid, som i
lösningsexemplet nedan, chansa på en övre gräns,
köra DFS-lösningen och, om ingen lösning hittas,
öka gränsen och köra igen.
Lösningsförslag
(DFS)
Uppgift 6 -
CD-brännaren
Till skillnad
från föregående uppgift finns det
här ingen effektiv lösning (såvitt jag
vet alltså, det är fritt fram för invändningar), utan här är det
backtracking som gäller, alla fördelningar av katalogerna
måste testas. Tanken med uppgiften är att man inte ska
behöva uppfinna optimeringar med hänsyn till
katalogernas storlekar eller kolla efter specialfall. Däremot
måste man se upp så att man inte upprepade gånger
testar fördelningar av katalogerna som egentligen är samma,
då tar det svåraste fallet för lång tid. Mitt
lösningsförslag bygger på att man fyller skiva efter
skiva med kataloger. En rekursiv funktion testar att lägga in var
och en av de kataloger som ännu inte har lagts på
någon skiva, och för varje val anropar funktionen sig
själv med en mindre katalog att bekymra sig över. Man
håller naturligtvis reda på den bästa lösningen
som man hittills hittat, så att man exemplevis aldrig börjar
fylla den femte skivan om man redan har hittat en lösning med fem
skivor. Två viktiga förbättringar av denna enkla
lösning bör göras. För det första, när vi
placerar den första katalogen på varje skiva är det
onödigt att testa alla möjliga kataloger. Eftersom varje
katalog ändå ska in på någon skiva kan vi utan
problem bestämma oss för att ta den första ännu
otagna katalog vi har. För det andra, inom en given skiva spelar
katalogernas ordning ingen roll. Har vi testat att lägga in
katalogerna A, C, E på en skiva är det helt
överflödigt att kolla C, E, A, eftersom detta ger samma
resultat. Alltså kan vi bestämma att när vi väl
har lagt in en katalog på en skiva, så struntar vi i de
kataloger som ligger före den (i indataordningen), även om
det finns kataloger där som ännu inte är inlagda. Detta
gäller naturligtvis endast så länge vi håller oss
på samma skiva, när vi börjar på en ny skiva
måste alla otagna kataloger komma ifråga igen. Läs
gärna mer om liknande problem som har effektiva
lösningar på träningssajten.
(Dynamisk
programmering -> Knapsackproblemet)
Lösningsförslag