Lösningar
till uppgifterna i
Finalen i Programmeringsolympiaden 2006.
Lösningarna
är skrivna i C och sparsamt kommenterade. 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)
Testdata (ZIP)
Uppgift 1 -
Lameller
Den första
uppgiften vållade en hel del problem, det gäller att ha
tungan rätt i mun så att man får med alla
möjligheter. Det enklaste är att lägga ut den
första lamellen på en fix position, t.ex. med den
vänstraste rutan vid koordinat 0. Sedan testar man alla
möjliga placeringar av den andra lamellen. Den kan starta så
långt till vänster som position -N2 men den kan också
starta så långt till höger som +N1 utan att det blir
något mellanrum. För varje placering av den andra lamellen
går man sedan igenom alla koordinater där åtminstone
någon av lamellerna finns och räknar hur många av
dessa som har genomgående hål. Man håller
förstås reda på det minsta antalet hål man
åstadkommit med någon placering hittills.
Lösningsförslag
Uppgift 2 -
Summan blir 1
Får väl
räknas som en standarduppgift. Eftersom de givna siffrorna endast
kan bilda 9! = 9*8*7*6*5*4*3*2*1 = 362880 olika uttryck (eller
ännu färre om man tar hänsyn till att termernas ordning
inte spelar någon roll) så är det bara att gå
igenom alla och testa om värdet blir ett och i så fall
skriva ut svaret. De 9! permutationerna kan genereras med en enkel
rekursiv funktion eller med hjälp av en färdig funktion i
Standard Template Library (STL) om man använder C++, se nedan
för båda lösningarna. För att testa om uttrycket
är lika med ett utan att behöva bekymra sig om
flyttalsprecision kan man använda ett klassiskt trick från
skolmatematiken, multiplicera båda sidor med en gemensam
nämnare, inte nödvändigtvis den minsta utan för
enkelhets skull produkten av de tre nämnarna.
Lösningsförslag
(rekursivt)
Lösningsförslag
(STL)
Uppgift 3 - Förvirrande SMS
Denna uppgift var det
också många som klarade. Återigen räcker det med
backtracking. Man håller reda på var man är i
sifferföljden och var man är i det ordet man håller
på att bilda. När man kommer till slutet av
sifferföljden skrivs ordet ut och man hoppar tillbaka
(backtrackar) för att se om det finns några alternativa
tolkningar. När vi är vid en given siffra, kan vi alltid
låta den stå för den första bokstaven på
knappen, då hoppar vi helt enkelt ett steg framåt. Om vi
ser att nästa siffra är densamma som den vi just nu tittar
på, så har vi ytterligare ett alternativ, nämligen att
låta de siffrorna ihop stå för den andra bokstaven
på knappen och hoppa två steg framåt. Och skulle de
två följande siffrorna vara desamma som den aktuella,
så har vi ett tredje alternativ, nämligen att låta de
tre siffrorna tillsammans stå för den tredje bokstaven
på knappen. Men fler än tre alternativ kan aldrig finnas,
så det behövs inte ens någon loop.
Lösningsförslag
Uppgift 4 -
Ordflätan
Här började många tävlande få problem
eftersom det krävdes en ganska effektiv algoritm för att
klara de svåraste testfallen. Den naiva lösningen är
att ha en rekursiv funktion som fyller i ett vågrätt ord i
taget och sedan kollar man på slutet om det stämmer
lodrätt också. Problemet är att det finns ca 17
miljarder sätt att välja ut fyra av 800 ord. Visserligen
är datorer snabba, men det är ändå lite för
mycket, särskilt om man för varje fall ska kolla om de
lodräta orden är godkända och inte förekommer flera
gånger. Så detta är ett typiskt exempel där man
behöver använda s.k. pruning, "beskärning" av
sökträdet, d.v.s. att försöka se så tidigt
som möjligt när man inte kan nå en lösning med de
val man har gjort och därför bör backtracka direkt (se
även träningssajten).
Det finns många olika sådana optimeringar att göra i
denna uppgift, men man behöver inte alls göra alla för
att klara tidsgränsen, huvudsaken är att man tänker
på någonting. Det kan t.ex. vara att man fyller i orden
växelvis vågrätt och lodrätt så att man
för varje ordval får färre möjligheter eftersom
det begränsas av de tidigare ifyllda orden. Lite knöligt att
skriva tycker jag, så mitt lösningsförslag bygger
istället på en optimering av den naiva algoritmen. När
man sätter dit det andra vågrätta ordet så kan
man ju passa på att kolla, för varje kolumn, om de
påbörjade lodrätta orden kan bli riktiga ord, d.v.s. om
de kombinationerna av två bokstäver förekommer i
början av ord, för annars är det ju ingen idé att
fortsätta. Och likadant när man fyller i det tredje
vågrätta ordet. För att göra den
"preliminära" kontrollen på ett effektivt sätt kan man
använda tabeller där man från början har sparat om
en bokstavskombination är början på ett ord eller inte.
Detta kan göras även för fyrbokstavskombinationer, det
blir ju bara 26^4=456976 möjligheter, och vips så är
också den "slutgiltiga" kontrollen (när alla fyra
vågrätta orden är ifyllda) blixtsnabb.
Lösningsförslag
Uppgift 5 -
Skidåkning
Detta är ett typexempel
på en uppgift som lämpligtvis löses med dynamisk
programmering. Problemet har till och med ett eget namn,
knapsack-problemet, och har varit föremål för mycket
datavetenskaplig forskning. Normalt brukar det formuleras som hur man
ska packa en ryggsäck (knapsack) med en viss storlek på ett
sådant sätt att den totala nyttan maximeras. Man väljer
bland ett antal objekt som vart och ett tar en viss plats och ger en
viss nytta. I en version kan man ta flera exemplar av samma objekt, i
en annan version högst ett. I den version som diskuteras på träningssajten
är nyttan lika med storleken, alltså ett specialfall. I
skidåkningsuppgiften är förstås objekten
nedfarter, deras "storlek" är den sammanlagda lift- och
åktiden och deras nytta är åktiden. Ryggsäckens
storlek motsvaras av den maximala tiden M. Uppenbarligen kan man ta
flera exemplar av samma objekt, d.v.s. åka samma nerfart flera
gånger.
I det generella fallet
är problemet NP-komplett, d.v.s. troligen olösbart på
polynomisk tid. Om storlekarna är heltal i ett någorlunda
litet intervall så finns dock effektiva lösningar med
dynamisk programmering. Det gäller att inse att när man har
förbrukat en viss tid och uppnått en viss åktid,
så spelar det ingen roll för fortsättningen precis
vilka backar man har tagit innan. Så om man i en rekursiv
lösning hela tiden kollar om man redan har hittat en lösning
med exakt samma förbrukad tid men med längre åktid,
så kan man ge upp direkt, det kommer aldrig att "ta ifatt" det
man ligger efter. Detta är alltså också en form av
beskärning av sökträdet, men till skillnad från
förra uppgiften är detta en mycket kraftig sådan, man
inser lätt att det totala antalet ställningar man kommer till
aldrig är fler än t^2 (där t är maxtiden) och i
praktiken mycket färre. Ofta är det lättare att bygga
upp de bästa dellösningarna svarande mot succesivt
ökande förbrukad tid i en tabell så att man slipper
rekursera överhuvudtaget, mer om det på träningssajten.
Lösningsförslag
Uppgift 6 -
Rinnande vatten
Ingen
lyckades lösa detta problem korrekt, men många var på
rätt väg. Det är lätt att bli lurad att tro att man
kan räkna ut "fyllningstiderna" i en snygg följd
uppifrån och ner (likt Pascals triangel) eftersom ett glas
fyllningstid vid första anblicken ser ut att bara bero på
fyllningstider och fyllningshastigheter i raden ovanför. Men det
finns ett problem: medan ett glas håller på att fyllas en
bit ner i pyramiden så kan plötsligt ett glas flera rader
upp bli fullt och ändra alla fyllnadshastigheter i mellanliggande
lager. Men läget är inte alls hopplöst. Man kan
nämligen helt enkelt simulera förloppet i tidsordning och
hålla reda på hur mycket som för tillfället
rinner från varje glas. Det är då lätt att
räkna ut vilket glas som härnäst kommer att fyllas och
vid vilken tidpunkt detta sker. Då hoppar man fram till denna
tidpunkt och uppdaterar alla flöden. På detta sätt
fortsätter man tills det intressanta glaset är fullt. Det som
många gjorde och som endast gav delpoäng var att
istället simulera med ett konstant tidssteg på typ 0.001
sekunder. Dels så förlorar man i exekveringstid, eftersom
man behandlar en massa tidpunkter där det inte händer ett
skvatt. Dels så förlorar man i noggrannhet eftersom glasen
inte nödvändigtvis fylls just vid de simulerade tidpunkterna.
Felen som uppstår samlar på sikt ihop sig och kan bli
ganska stora.
Lösningsförslag
Uppgift 7 -
Avståndstabell
Det
här problemet är som Kortaste vägen
fast tvärtom, alltså man ska här bestämma grafen
när man vet kortaste vägen. Teknikerna man kan använda
påminner också om lösningen av Kortaste vägen.
Man bygger succesivt upp grafen genom att addera fler och fler
bågar (vägar mellan städerna). Man kollar i
avståndstabellen efter det kortaste avståndet som ännu
inte har fått sin förklaring i grafen. Detta måste bli
en ny direktväg, för om det är en sammansatt väg
så måste den involvera minst en ny väg (eftersom den
inte gick att förklara). Denna nya väg måste vara
kortare än den aktuella, vilket är en motsägelse
eftersom man valde det kortaste avståndet.
Lösningsförslag