510 likes | 675 Views
3. tekmovanje IJS v znanju računalništva. Naloge in rešitve Janez Brank 29. marca 2008. 1.1 GrbaveBesede. Beseda je grbava , če se v njej vsaj dvakrat pojavi mala črka neposredno za veliko Primer: DomNaKrimu, WriteLn Ne pa: naKrimu, Write, NaOH
E N D
3. tekmovanje IJS v znanju računalništva Naloge in rešitveJanez Brank29. marca 2008
1.1 GrbaveBesede • Beseda je grbava, če se v njej vsaj dvakrat pojavi mala črka neposredno za veliko • Primer: DomNaKrimu, WriteLn • Ne pa: naKrimu, Write, NaOH • Napiši program, ki prebere besedilo in izpiše tiste besede v njem, ki so grbave.
1.1 GrbaveBesede • Berimo besedilo znak po znak • V neki spremenljivki štejmo, koliko grb ima trenutna beseda • Če je prejšnji znak velika črka, trenutni znak pa mala, povečamo število grb • Če je prejšnji znak črka, trenutni znak pa presledek (ali konec vrstice), smo na koncu besede • Če ima vsaj dve grbi, jo izpišemo
1.2 Predavalnice • Dani so časi začetka in konca predavanj • Zanima nas največje število predavanj, ki potekajo hkrati • (Da vemo, koliko predavalnic bomo potrebovali) • Lažja različica: časi so podani v urah in minutah • Težja: časi so poljubna realna števila
1.2 Predavalnice • Lažja različica: časi v urah in minutah • In vse se dogaja znotraj enega dneva • Dan ima le 24 · 60 = 1440 minut • Imejmo tabelo 1440 elementov; v njej za vsako minuto piše, koliko predavanj je takrat v teku • Na začetku postavimo vsa ta števila na 0 • Za vsako predavanje gremo od začetnega do končnega časa in povečujemo števila v tabeli za 1 • Na koncu poiščemo največje število v tabeli
1.2 Predavalnice • Težja različica: časi so poljubna realna števila • Do sprememb v številu hkratnih predavanj lahko pride le takrat, ko se kakšno predavanje začne ali konča • Uredimo čase začetka in konca predavanj • Pregledujmo te čase po vrsti: • Ko se neko predavanje začne, povečamo števec za 1 • Ko se neko predavanje konča, zmanjšamo števec za 1 • Sproti si zapomnimo, katera je bila največja dosežena vrednost števca • Če se v nekem trenutku kakšno predavanje začne, kakšno pa konča, upoštevajmo najprej tisto, ki se konča [na to moramo paziti že pri urejanju časov]
1.3 Darila • Imamo n oseb • Vsak je dal vsem drugim darila(različnih vrednosti) • Kdo je imel največ dobička? • Torej največjo razliko med skupno vrednostjo prejetih in podarjenih daril
1.3 Darila • Rešitev: • Zunanja zanka gre po vseh osebah • Z notranjo zanko gremo še enkrat po vseh osebah • Prištevamo vrednosti prejetih daril, odštevamo vrednosti podarjenih daril • Rezultat je dobiček te osebe • Če je to največji doslej znani dobiček, si ga zapomnimo (in tudi številko te osebe).
1.4 Elektronska ključavnica • Napiši program za krmiljenje elektronske ključavnice: • Na njej so tipke 0..9 in “prekliči” • Ko uporabnik natipka pravo geslo, se naj ključavnica odklene • Če se uporabnik zatipka, vse nadaljnje pritiske na tipke ignoriramo, dokler ne pritisne gumba “prekliči”
1.4 Elektronska ključavnica • Program teče v neskončni zanki: • V neki spremenljivki g si zapomnimo doslej natipkani del gesla • Še ena spremenljivka z pa pove, če se je uporabnik že zatipkal • Ob pritisku na “prekliči” obe spremenljivki resetiramo • Če se je uporabnik že zatipkal, pritiske na števke 0..9 ignoriramo • Sicer preverimo, če je bila pritisnjena prava tipka (naslednja števka gesla) • Če ni bila, se je zatipkal (postavimo z na true) • Sicer dodamo novo števko na konec g • Če je g zdaj enak geslu, odklenemo ključavnico in g postavimo spet na prazen niz.
1.5 Kdo je izdal skrivnost? • Dan je seznam telefonskih pogovorov • Za vsak pogovor je znano, katera dva človeka sta se pogovarjala • Pogovori so urejeni po času • Zanima nas, ali je lahko oseba a prek enega ali več klicev posredovala neko informacijo osebi b. • Zanima nas tudi dolžina najkrajšega takega zaporedja klicev.
1.5 Kdo je izdal skrivnost? • Pri tej nalogi je ljudi le 10000 (štirimestne telefonske številke) • Za vsakega v neki tabeli hranimo podatek o tem, kako dolgo je najkrajše znano zaporedje klicev na poti od a do njega (in kdo je njegov predhodnik na tej poti) • Na začetku je znana le pot do a (dolžine 0) • Klice pregledujemo po času • Pri vsakem klicu (x, y): • Če je znana pot dolžine d od a do x, imamo zdaj tudi pot od a do y dolžine d + 1; če tako kratke poti do y doslej nismo poznali, si zapomnimo to. • Možno je tudi obratno (pot od a do y + korak od y do x = mogoče nova najkrajša pot od a do x)
2.1 Roboti • Imamo več seznamov • Elementi vsakega seznama so točke (x, y),x, y {1, …, n} • Zanima nas, ali je kakšna točka prisotna na vseh seznamih.
2.1 Roboti • Rešitev s tabelo: • Imejmo tabelon×n elementov • Na začetku postavimo vse na 0 • Za vsak seznam: • Za vsako točko (x, y) na seznamu: • Povečajmo v tabeli T[x, y] za 1 • Na koncu torej za vsako točko vemo, v koliko seznamih je prisotna • Slabost: O(n2) pomnilnika • Preveč, če je n velik • Boljša različica: razpršena tabela (hash table) • Poraba pomnilnika je sorazmerna s številom točk v seznamih
2.1 Roboti • Rešitev z razlikami seznamov: • Začnimo s prvim seznamom (recimo mu S) • Za vsakega od ostalih seznamov (recimo S'): • Pobrišimo iz S vse elemente, ki jih ni v S' (*) • Na koncu ostanejo v S le tisti elementi, ki so prisotni v vseh seznamih • Kako izvesti korak (*)? • Preprosta rešitev: dve gnezdeni zanki • Bolj učinkovito: sezname uredimo po koordinatah in jih zlivamo (= premikamo se z dvema števcema hkrati po obeh seznamih)
2.2 rsync • Imamo seznam zapisov oblike ime, datum • Naš namen je, da bi imeli na koncu za vsako ime le zapis z najnovejšim datumom. • To počnemo z naslednjim postopkom: • uredi seznam padajoče po datumu • uredi seznam naraščajoče po imenu • za vsak zapis poglej, če ima isto ime kot prejšnji; če ga ima, ga pobriši. • Ureja se takole: • for i := 0 to n – 2 do for j := i + 1 to n – 1 do if T[i].datum <= T[j].datum then swap(T[i], T[j]);for i := 0 to n – 2 do for j := i + 1 to n – 1 do if T[i].ime >= T[j].ime then swap(T[i], T[j]); • Ali ta postopek deluje pravilno? • Če ne, ga moramo popraviti
2.2 rsync • Postopek ne deluje pravilno • Pri urejanju po imenu pridejo skupaj zapisi za isto ime • Na koncu obdržimo le prvega od njih – ta bi moral torej biti najnovejši • Po urejanju po datumu je bil res najnovejši zapis za vsako ime pred vsemi ostalimi zapisi za tisto ime. • Toda ali je urejanje po imenu to lastnost ohranilo? • Žal ne: • Ker uporabljamo “>=” namesto “>”, včasih zamenjamo dva zapisa z istim imenom • Zapis se lahko ob zamenjavi premakne daleč naprej, tudi mimo takih, ki imajo isto ime, a zgodnejši datum • Rešitev: T[i] in T[j] ne zamenjamo, pač pa T[j] vrinemo pred T[i], tako da dobimo T[j] T[i] T[i+1] … T[j-1] T[j+1] …
2.3 Usklajevanje ur • Implementirati moramo protokol za sinhronizacijo ure na odjemalcu • (1) strežnik pošlje svoj trenutni čas t1 odjemalcu • (2) odjemalec si zapomni čas prejema, t2; nato ob času t3 pošlje strežniku odgovor • (3) strežnik si zapomni čas prejema, t4, in ga pošlje odjemalcu • Zdaj lahko odjemalec izračuna razliko med svojo in strežnikovo uro in jo pri sebi popravi.
2.3 Usklajevanje ur • Naloga zahteva, da definiramo vsebino sporočila in napišemo tri podprograme: • Sporočilo naj vsebuje čase t1, t2, t3, t4 in zaporedno številko sporočila (1, 2 ali 3) • SinhronizirajUro mora začeti celoten postopek • To pomeni, da odjemalcu pošlje (1; t1) • SprejemNaStrežniku mora odreagirati na 2: • Takrat pošlje odjemalcu (3; t1, t2, t3, t4) • SprejemNaOdjemalcu mora odreagirati na 1 in 3: • Ko prejme 1, pošlje strežniku (2; t1, t2, t3) • Ko prejme 3, izračuna razliko in popravi uro.
2.4 Društvo ljubiteljev ničel • Dana je tabela n elementov, vsi so 16-bitna cela števila (0..65535) • Radi bi jih vse xorali z nekim 16-bitnim številom c, tako da bi bilo po tem v tabeli čim manj prižganih bitov • a := a xor c pomeni, da če je nek bit v c prižgan, bomo tisti bit v a obrnili (iz 0 v 1 ali iz 1 v 0), ostale bite a-ja pa pustimo pri miru • Kateri c naj izberemo?
2.4 Društvo ljubiteljev ničel • Lepo pri operaciji xor je, da jo lahko računamo po bitih • To, kar se dogaja na nekem bitu, je neodvisno od tega, kar se dogaja na drugih bitih • Za vsak bit poglejmo, koliko števil v tabeli ima ta bit prižgan, koliko pa ugasnjen • Če je več prižganih kot ugasnjenih, postavimo ta bit v c na 1, ta se bodo prižgani spremenili v ugasnjene in obratno • Sicer pa pustimo ta bit v c na 0 • Če je n res velik, se splača za vsako možno vrednost od 0 do 65535 najprej prešteti, kolikokrat se pojavlja v naši tabeli
2.5 Cik cak • Naloga: • dan je opiscikcakaste črte iz znakov/ in \, npr.: \//\//\\/\\/// • Radi bi jo narisali prek več vrstic,pod črto o-je, nad črto pike
2.5 Cik cak • Rešitev: • Recimo, da se črta začne na višini y = 0. • V prvem prehodu po nizu poiščemo najvišjo in najnižjo točko. • Nato za vsako vrstico naredimo še en prehod čez niz; • gledamo, na kakšni višini je črta; • če je črta nad našo vrstico, napišemo o; • če je pod njo, napišemo piko; • sicer napišemo trenutni znak / ali \.
3.1 PINi • Pri tej nalogi so PINi zaporedja n števk • Iz starega PINa dobimo novega takole: • seštejemo števke • pripišemo zadnjo števko vsote • prvo števko pobrišemo • Primer: 1234 2340 3409 4096 … • Vprašanje: po koliko korakih se začnejo ponavljati (za dani začetni PIN)? • Koliko ločenih ciklov obstaja? • Koliko PINov je na krajšem ciklu od danega PINa?
3.1 PINi • Pri tej nalogi je n ≤ 6, zato je možnih največ 106 = milijon PINov • Imejmo neko tabelo 106 elementov, v kateri za vsak PIN piše, ali smo že naleteli nanj in na kako dolgem ciklu je • x := ZacetniPIN; d := 0;while not ZeVideno[x]:ZeVideno[x] := true;x := Naslednji(x); d := d + 1; • Zdaj vemo, kako dolg je cikel; nato naredimo še en obhod po njem in v tabelo vpisujemo, da so ti elementi na ciklu dolžine d. • Potem vzamemo kakšen še ne obiskan PIN in postopek ponovimo pri njem. Dobimo nov cikel, ločen od prejšnjega • itd., dokler ne obiščemo vseh PINov.
3.2 Berberi • Na karirasti mreži sooznačene poti, po katerihso se selili Berberi • Občasno se poti razvejijo • Na prvi razvejitvi so seločili Y-ovi pristaši od M-jevih • Na vsaki razvejitvi nastanenaselje • Tudi na koncu poti nastane naselje • Koliko naselij ima zdaj Y, koliko pa M?
3.2 Berberi • Selitvam sledimo sistematično,od začetka (na levem robu) • Pri razvejitvi gremo najprej po enem in nato po drugem možnem nadaljevanju • Pri vsakem polju si označimo, iz katerega soseda smo vanj prišli • Ko se vrnemo iz polja, si označimo, da je že pregledano • Posebej si zapomnimo, kje je prva razvejitev in ali smo zdaj na sledi Y-ovih ali M-jevih pristašev • Ko smo v razvejitvi ali v polju brez naslednikov, povečamo števec naselij
3.3 Piskrc špagetov • Imamo nškatel špagetov, v i-ti škatli je mi špagetov dolžine di • Špagete lahko razpolavljamo • Iz špageta dolžine 2k nastaneta 2 špageta dolžine k • Iz špageta dolžine 2k + 1 nastane en špaget dolžine k in en špaget dolžine k + 1 • Razpoloviti smemo b špagetov • Radi bi, da bi bil na koncu najdaljši špaget čim krajši.
3.3 Piskrc špagetov • Pametno je v vsakem koraku razpoloviti najdaljši špaget • Če imamo več enako dolgih najdaljših špagetov, moramo razpoloviti vse (celo škatlo) • Vhodni seznam škatel je že urejen po dolžini špagetov (tako zagotavlja besedilo naloge) • Ko razlomimo vse špagete v neki škatli, nastaneta ena ali dve škatli s krajšimi špageti • Te nove škatle odlagamo na konec nekega novega seznama (ki je tako tudi urejen) • Na vsakem koraku prelomimo tisto škatlo, v kateri so najdaljši špageti (lahko je iz originalnega ali pa iz novega seznama)
3.4 Redki nizi • Imamo niz znakovs[1..n] • Najprej vse znake postavimo na c • Nato čez znake s[z1..(z1 + d1 – 1)] vpišemo znake niza t1[1..d1] • Potem naredimo enako za t2, …, tk(vsak ti ima svojo dolžino di in začetni položaj zi) • Primer: n = 20, c = xxxxxxxxxxxxxxxxxxxxxt1 = abcfg, z1 = 10 xxxxxxxxxabcfgxxxxxxt2 = fghxjx, z2 = 7 xxxxxxfghxjxfgxxxxxxt3 = xfg, z3 = 17 xxxxxxfghxjxfgxxxfgx = s • V tako dobljenem nizu s iščemo vse pojavitve niza p • s je dolg do 109 znakov, vsi ti-ji skupaj največ 106 znakov, p največ 100 znakov
3.4 Redki nizi • s je dolg 109 znakov, vsi ti-ji skupaj le 106 večino nizas še vedno pokrivajo znaki c • s razdelimo na kose, sestavljene iz c-jev, inkose, ki jih pokrivajo ti-ji • Pri prejšnjem primeru: xxxxxxfghxjxfgxxxfgx • ti-ji se lahko med seboj prekrivajo • uredimo jih po začetnem indeksu • če se naslednji t začne prej, kot se prejšnji konča, sta del istega kosa, itd. • vsakega od kosov, nastalih iz ti-jev, predstavimo z nizom;za kose iz samih c-jev pa si zapomnimo le dolžino • pojavitev p-ja znotraj kosov oblike ccc..cc ni težko prešteti • tiste pojavitve, ki ležijo (vsaj delno) v kakšnem kosu,nastalem iz ti-jev, pa poiščemo na staromoden način (primerjamo znakep-ja in s-ja enega po enega)
3.5 Obračanje bitov • Imamo tabelow×h polj, vsako polje je lahko črno ali belo • daljša stranica je ≤ 50, krajša je ≤ 21 • Dovoljena operacija: izberemo neko vrstico ali stolpec in spremenimo barvo vseh polj v njej (črna postanejo bela in obratno) • S takimi operacijami bi radi čim bolj zmanjšali število črnih polj
3.5 Obračanje bitov • Nekaj opažanj: • Vrstni red operacij ni pomemben • Nobene vrstice ali stolpca ni smiselno obrniti več kot enkrat • Torej ostane le še vprašanje, katere vrstice in stolpce bi obrnili • V zunanji zanki preizkusimo vse možne izbore stolpcev: • Za vsak izbor stolpcev si ogledamo mrežo, kakršna nastane po obračanju teh stolpcev. • Obrnemo vse tiste vrstice, ki imajo več črnih polj kot belih. • Za učinkovito implementacijo: • Izbrano množico stolpcev predstavimo kot w-bitno celo število. • Ravno tako tudi vsako vrstico. • Za vsako w-bitno celo število potabeliramo število prižganih bitov v njem. • Če je w > h, tabelo diagonalno prevrnemo.
Greed, for want of a better word, is good. Greed is right, greed works. Gordon Gekko, Wall Street (1987) Kino (off-line naloga)
Opis naloge • To je “off-line naloga”: • opis naloge in vhodne podatke smo objavili pred nekaj meseci • tekmovalci so lahko oddajali rešitve našemu ocenjevalnemu strežniku do vključno včeraj • Naloga: sestavi celoletni spored filmov v kinematografu, tako da bo prodanih čim več vstopnic • Dani so podatki: • seznam prebivalcev našega kraja;za vsakega prebivalca poznamo: • katere žanre filmov rad gleda • ob kateri uri je najraje v kinu • seznam filmov, ki jih lahko predvajamo;za vsak film poznamo: • katerim žanrom pripada • popularnost filma • trajanje filma v minutah • število sedežev v kino dvorani
Ocenjevanje • Ocenjevali smo s programom, ki je simuliral odločitve gledalcev: • Za vsako predstavo v celoletnem sporedu: • Za vsakega gledalca: • Oceni verjetnost, da bi ta gledalec šel kupit karto za to predstavo. • Na podlagi te verjetnosti se odloči, ali jo bo res šel kupit ali ne. • Če pride kupit vstopnico več gledalcev, kot je sedežev v dvorani, naključno izberi, kateri so vstopnico res dobili. • Na koncu leta izpiši skupno število prodanih vstopnic. • Verjetnost, da bi gledalec želel kupiti karto, je bila odvisna od: • časa predstave (ali je ob taki uri, ko je ta gledalec rad v kinu) • popularnosti filma • ali je film iz takega žanra, ki ga ta gledalec rad gleda • ali je bil zadnje dni ta gledalec že v kinu • ali je gledalec ta film že videl • Podrobnosti tega postopka (in tudi izvorno kodo ocenjevalnega programa) smo objavili tudi na naši spletni strani.
Poti do rešitve • Pri tej nalogi ni neke očitne “pravilne rešitve” oz. postopka, ki bi dajal najboljše možne sporede • Prostor vseh možnih celoletnih sporedov je ogromen, v njem moramo nekako najti nek čim boljši spored • Uporabimo lahko razne tehnike s področja optimizacijskih metod: • Lokalna optimizacija, taboo search ipd. • Simulirano ohlajanje • Genetski algoritmi • Iskanje v snopu • Optimizacija z roji (swarms) • Najboljši naprej (best-first search), RBFS, ipd. • Paziti moramo, da nam postopek ne bo požrl preveč časa: • Prostor, ki ga preiskujemo, je res ogromen • Računanje kriterijske funkcije je precej drago • Mi smo dosegli precej dobre rezultate s preprostim požrešnim algoritmom
Požrešni algoritem • Začnimo s praznim sporedom. • Dokler ne zapolnimo celega leta: • Preglej vse možne filme, ki jih smemo predvajati: • Za vsak film s simulacijo oceni, koliko vstopnic bi prodali, če bi dodali predstavo tega filma na konec našega sporeda. • Med vsemi filmi izberi najboljšega in ga dodaj na konec sporeda. • Zakaj “požrešen” (greedy) algoritem? • Nikoli se ne vračamo nazaj in spreminjamo delov sporeda, ki smo jih že sestavili • Pri vsakem filmu gledamo le, koliko vstopnic bi prodali zdaj – nič ne razmišljamo, ali je to dobro tudi na dolgi rok • Postopek je zelo preprost, vendar časovno precej potraten • V vsaki simulaciji se skriva zanka po vseh gledalcih
Požrešni algoritem • Kateri film izbrati na vsakem koraku? • Najbolj očitna ideja: tistega, ki proda največ vstopnic. • Toda: filmi so različno dolgi. • Vstopnice za vse pa so enakovredne. • Če predvajamo krajše filme, jih bomo lahko v eno leto stlačili več in bomo mogoče prodali več vstopnic. • Zato je pametno gledati razmerje med številom prodanih vstopnic in trajanjem filma. • Izkaže se, da je v naših podatkih tudi nekaj “filmov”, ki so dolgi po manj kot 5 minut (reklame, videospoti) in so za ta kriterij zelo vabljivi • Nastanejo sporedi z 200000 predstavami… • ki pa skupno prodajo 3x toliko vstopnic kot sporedi, ki jih dobimo, če v vsakem koraku dodamo v spored film, ki proda največ vstopnic (ne glede na dolžino filma) • Na največjem testnem primeru se je naš postopek izvajal 16 dni. Za vseh 10 testnih primerov je porabil več kot en mesec procesorskega časa.
Off-line naloga: Kino Razglasitev rezultatov
Kino – rezultati • Tega dela tekmovanja so se žal udeležili le štirje tekmovalci • so bili pa zato ti toliko bolj navdušeni • 10 oddaj v zadnji uri, zadnja ob 23:58:54 • Točkovanje: • bilo je 10 testnih primerov • pri vsakem testnem primeru: • gledamo le najboljši prejeti spored vsakega tekmovalca • tekmovalce razvrstimo po oceni tega sporeda • prvi dobi 10 točk, drugi 8, tretji 7 • točke seštejemo po vseh testnih primerih
Kino - rezultati • Animacija: za vsak testni primer je skupina stolpcev, ki kaže najboljše rešitve vseh tekmovalcev • Višina stolpcev pove, kako dobra je tista rešitev v primerjavi z najboljšo nam znano rešitvijo za ta testni primer
Kino - rezultati • 1. mesto: Tomaž Hočevar (94 točk) • 2. mesto: Rok Kralj (86 točk) • 3. mesto: Domen Blenkuš (69 točk) • 4. mesto: Jan Berčič (19 točk)
Tekmovanje programov:Štiri v vrsto Blaž Novak
Tekmovanje programov:“4 v vrsto” Naloga: sprogramiraj “pamet” za tekmovalca v znani igri 4 v vrsto 2 podkategoriji: tekmovanje na polju velikosti 7×6 tekmovanje na polju velikosti 20×20
Tekmovalci Prejeli smo 7 rešitev, od tega 6 dijaških 3 v C#, 2 v Delphiju, 2 v C++ Ena od rešitev izločena iz končne razvrstitve na polju 7×6 zaradi prekomerne uporabe tuje kode Ena od rešitev izločena iz končne razvrstitve na polju 20×20 zaradi nestabilnosti
Izvedba tekmovanja Igre na polju 7×6 smo zaradi točnosti rezultatov ponovili 30-krat Igre na polju 20×20 pa 20-krat Le 3 pari iger deterministični, ostali pri izbiri potez uporabljajo nekakšno naključnost 276.000 s procesorskega časa porabljenega za reševanje 1785 iger Točkovanje: Za vsak par igralcev: Za vsako igro dobi zmagovalec +1 točko, poraženec -1 točko (če je neodločeno, ne dobi nihče nič). Vzame se povprečje po vseh igrah. Na koncu seštejemo točke za posameznega igralca.