Tip of the day: stari bilteni

Kada je napolju 30+ stupnjeva, a klimu nemamo ili nam ne radi, mozak se od vrućine razlijeva bespućima pustoši i pustopoljine i nemamo snage za rješavanje zadataka. Ipak, možda još nismo toliko otopljeni da ne bismo mogli barem čitati rješenja (opise algoritama i kodove) dobrih zadataka iz kojih množemo mnogo naučiti.

Naravno da toga na webu ima napretek, ali ovdje bih uputio na jedan dobar domaći izvor za koji vrlo malo ljudi zna, a to su stari bilteni sa HSIN-ovih ljetnih i zimskih škola informatike, u kojima se često u opisu pojedinih radionica (Algoritmi, Olimpijci…) opisuju odabrani zadatci s tih radionica s rješenjima, a nađe se i drugih trikova i zanimljivosti. Primjer je recimo Bilten iz 2010. gdje od 58. stranice legendarni mentor Luka Kalinovčić objašnjava vrlo poučne zadatke Bugs IntegratedIcerinkMonkeys. A ostali bilteni nalaze se ovdje!

Tip of the day: testiranje

Nekada na važnim natjecanjima nije bilo full feedbacka pa smo nervozno čekali rezultate tumarajući po hodnicima, koji su uglavnom dolazili isprintani (rezultati, ne hodnici) za svakog natjecatelja posebno – ishodi po pojedinim primjerima. I danas je slična situacija na županijskom i na HONI-ju gdje nema feedbacka, ali nekada je tako bilo i na državnom (koje se zvalo DMIH), na HIO-u, IOI-u i svugdje. Full feedback pojavio se, ako se ne varam, na IOI-u 2010. Na CEOI-u 2013. u Primoštenu još uvijek ga nije bilo, a na državnom je uveden 2015. gdje je ključnu ulogu odigrao Ante Đerek, između ostalog motiviran e-mail prepiskom nakon žalbi sa županijskog. U jednom od tih mailova Stjepan Glavina napisao je poznatu rečenicu: “Tradicionalni tip natjecanja mora nestati.”

Ovakvi incidenti su razlog zašto mislim da tradicionalni tip natjecanja (HONI, državno, CEOI, IOI prije 2010.) mora nestati. A nisam jedini koji tako misli [1]. Bodovanje je vrlo nepravedno. “Manje” kriva rješenja znaju dobivat više bodova od “više” krivih. Zna se desit i da točna rješenja s malim bugom dobiju manje bodova od potpuno krivih, ili čak nula bodova. Faktor (ne)sreće je prevelik. Drame s bugovima se događaju na svakom takvom natjecanju.

Prije full feedbacka, testiranje rješenja “s brutforsom i generatorom” bila je standardna stvar koju su jači natjecatelji tijekom natjecanja radili na svim zadatcima gdje je to imalo smisla. Jednom sam komentirao da nerado testiram jer volim misliti da sam riješio zadatak, barem tih nekoliko sati, umjesto da si testiranjem srušim rješenje. “Kurdija, to je i ideja”, odgovorio mi je Žužić odmahujući glavom u nevjerici.

Full feedback ima i određenih mana. Neki misle da je njime djelomično izgubljen važan dio natjecanja – spomenuto testiranje, te da rezultati dobiveni odmah “na pladnju” nisu nešto što je inače realno. S organizacijske strane ima više posla – zadatci moraju biti teži nego inače (jer natjecatelji zbog feedbacka troše manje vremena i manje griješe), s više parcijala da se dobije dobra disperzija bodova (koja se prije lakše dobivala zbog raznih neotkrivenih bugova), a greška u evaluaciji ili testnim primjerima košta više nego prije jer utječe na tijek natjecanja.

Osim na onim natjecanjima gdje još uvijek nema feedbacka (mislim da i Codeforces ulazi u tu kategoriju), danas testiranje koristimo i onda kada nam evaluator kaže da rješenje ne valja, ali ne znamo zašto pa moramo naći primjer na kojemu pada. Errichto dobro objašnjava kako se to radi:


Ugrubo i sažeto, evo bash skripte kakvu sam nekada koristio (njegova je malo drugačija):

echo > o1  # napravi prazan prvi output
echo > o2  # napravi prazan drugi output
while diff o1 o2; do     # dok su outputi jednaki
    ./gen.py > t         # generiraj random primjer t
    ./test.exe < t > o1  # o1 = rjesenje(t)
    ./brute.exe < t > o2 # o2 = brutfors(t)
    printf '.'           # ispisi tocku za svaki testirani primjer
done

Poziva se iz Linux terminala:

bash testiraj.sh

… ili (nakon postavljanja chmod +x testiraj.sh):

./testiraj.sh

Update: sad uočavam da sam riječ testiranje u dvama nedavnim postovima (ovom i ovom) koristio u značenju evaluacija tj. isprobavanje na službenim testnim primjerima, dok se u ovom postu testiranje odnosi na stress-testing rješenja za vrijeme natjecanja kada službeni testni primjeri još nisu dostupni. Tu dvoznačnost mogao bih otkloniti tako da u prethodnim postovima svugdje napišem evaluirati umjesto testirati, ali to mi se ne da i vjerojatno je i ovako jasno o čemu se radi.

Tip of the day: testiranje na pythonovski

Kao nastavak prethodnog posta Tip of the day: testiranje zadataka sa HSIN-a, ovdje ću testiranje rješenja napisati u Pythonu (a ne u Bashu), i to s dodatnim funkcionalnostima kao što su automatsko preuzimanje testnih primjera, mjerenje vremena izvođenja i paralelizacija. Ipak preporučujem da najprije pročitate navedeni post radi razumijevanja načina pozivanja programa, preusmjeravanja inputa/outputa i diff-anja, što ćemo koristiti i ovdje. Osim samih skripti, ideja je i pokazati neke mogućnosti Pythona.

Želimo, za početak, zaobići ručno preuzimanje zip datoteke i ručno extractanje odgovarajućih testnih primjera iz nje. Nema problema! Evo skripte download.py, koja kao argumente prima ime zadatka i URL zip datoteke s njegovim testnim primjerima:

#!/usr/bin/python3
from io import BytesIO
from zipfile import ZipFile
from urllib.request import urlopen
from sys import argv

ime_zadatka = argv[1]
url = argv[2]

resp = urlopen(url)
zipfile = ZipFile(BytesIO(resp.read()))
for filename in zipfile.namelist():
    if filename.startswith(ime_zadatka):
        zipfile.extract(filename, '.')

Primjer preuzimanja testnih primjera za zadatak Modulo s 1. kola HONI-ja 2006.:

./download.py modulo https://hsin.hr/honi/arhiva/2006_2007/kolo1_testpodaci.zip

Ishod će biti stvaranje datoteka modulo/modulo.in.1, modulo/modulo.out.1 i tako dalje, gledajući iz direktorija u kojem je skripta pokrenuta. Izmjenu skripte po vlastitim željama ostavljamo čitateljici za vježbu.

Ako se nalazimo unutar direktorija u kojemu je naše rješenje i testni primjeri, skripta koja testira sve primjere, usput mjereći vrijeme izvođenja za svaki primjer, izgledala bi primjerice ovako:

#!/usr/bin/python3
from os import listdir, popen
from time import time
from sys import argv

ime_zadatka = argv[1]
rjesenje = argv[2]

for filename in listdir('.'):
    if '.in.' in filename:
        print('\nTestiram {}...'.format(filename))
        start_time = time()
        popen('./{} < {} > tmp.out'.format(rjesenje, filename)).read()
        print('Vrijeme = {:.3f} sekundi'.format(time() - start_time))
        p = popen('diff -w tmp.out {}'.format(filename.replace('.in.', '.out.')))
        print(p.read() or '---------- OK ----------\n')

Dakle, za svaku *.in.* datoteku (ulazni primjer),  unutar popen-a kao novi proces pokrećemo naše rješenje, preusmjeravamo taj primjer na ulaz, a naš izlaz u datoteku tmp.out koju potom uspoređujemo (diff) sa službenim *.out.* izlazom. Ako je diff prazan, ispisujemo OK. Za mjerenje izvođenja ključna je funkcija time.time() koja daje trenutačno vrijeme u sekundama. Poziv skripte za zadatak Herman izgledao bi npr. ovako:

./test.py herman herman.exe

Nedostatci? Primjeri se neće nužno izvesti prirodnim redoslijedom i možda je moguće usporedno provjeravati više primjera (na više jezgri procesora) radi bržeg testiranja. Obje stvari možemo riješiti tako da napravimo četiri procesa (multiprocessing.Pool(4)) od kojih svaki paralelno testira svoj dio testnih primjera i vraća ishod kao string (npr. “Primjer 1OK, 0.1 sekundi“), koje onda na kraju možemo sortirati abecedno pa će primjer 1 biti prvi.

#!/usr/bin/python3
from os import listdir, popen
from time import time
from sys import argv
from multiprocessing import Pool

ime_zadatka = argv[1]
rjesenje = argv[2]

def testiraj(filename):
    output = '\nTestiram {}...\n'.format(filename)
    ekstenzija = filename.split('.')[-1]
    start_time = time()
    popen('./{} < {} > tmp{}.out'.format(rjesenje, filename, ekstenzija)).read()
    output += 'Vrijeme = {:.3f} sekundi\n'.format(time() - start_time)
    p = popen('diff -w tmp{}.out {}'.format(ekstenzija, filename.replace('.in.', '.out.')))
    output += p.read() or '---------- OK ----------\n'
    return output

with Pool(4) as p:
    ishodi = p.map(testiraj, [filename for filename in listdir('.') if '.in.' in filename])
for i in sorted(ishodi):
    print(i)

I ovdje se neke stvari mogu doraditi, može se dodati timeout tj. ograničiti vrijeme izvođenja, a moguće se i pobrinuti da herman.in.10 ne dođe prije herman.in.2 iako je prije po abecedi. Sve to i mnogo više ostavljamo… znate već.

Tip of the day: testiranje zadataka sa HSIN-a

Mnogi od mojih postova sadrže poveznice na zadatke sa HSIN-a koji (barem zasad) ne postoje ni na kakvom evaluatoru. Zato je smanjena motivacija za njihovo rješavanje: nije moguće jednostavno submitati rješenje i odmah dobiti povratnu informaciju, nego je nužno pronaći i skinuti zipane testne primjere sa hsin.hr i lokalno ih isprobati.

Ovo “isprobati”, naravno, ima svoje nijanse. Mnogi znaju samo kopipejstati primjer u program, što je zbilja odvratno. Određen broj natjecatelja zna prosljeđivati jedan po jedan primjer u program koristeći komandnu liniju, te naredbom (diff ili fc) provjeriti podudaranje izlaza. Tek manji broj njih zna to učiniti za sve primjere odjednom, koristeći skriptu. Pokažimo kako se to radi!

Naprije na sporiji način. Dakle, nakon što smo zipane primjere pronašli negdje na https://hsin.hr/natjecanja.html ili na https://hsin.hr/honi/arhiva/ te ih otpakirali, potrebno je u terminalu navigirati do tog direktorija (npr. cd kodovi/herman ako se zadatak zove Herman). Ako smo rješenje kompajlirali tako da se program zove herman.exe, u terminalu ga pokrećemo na sljedeći način:

./herman.exe

(na Windowsu mislim da je bez ovoga ./). Sada bi se primjer unio “s tipkovnice” ili kopipejstao, što ne želimo. Prosljeđivanje primjera (npr. herman.in.1) izravno u program činimo na sljedeći način:

./herman.exe < herman.in.1

… i odmah vidimo što nam program ispisuje. Želimo li ispis u datoteku, pišemo npr. ovako:

./herman.exe < herman.in.1 > izlaz.txt

Tu će datoteka izlaz.txt biti stvorena ako ne postoji, ili prepisana ako postoji. Usporedbu tog našeg ispisa sa službenim izlazom činimo na sljedeći način:

diff izlaz.txt herman.out.1

(Mislim da je na Windowsu fc umjesto diff). Želimo li zanemariti whitespace, dodajemo opciju -w. Ako diff ne ispiše ništa, znači da su datoteke jednake (rješenje je točno), a inače se ispiše razlika.

To bi valjalo ponoviti za svaki primjer, npr. za peti primjer:

./herman.exe < herman.in.5 > izlaz.txt
diff -w izlaz.txt herman.out.5

Mnogo bolje od kopipejstanja, ali i dalje naporno, pogotovo ako postoji 30 primjera ili ako imamo clustere pa primjeri imaju nastavke 1a, 1b, 1c, 2a… No evo skripte koja to radi sve odjednom, nazovimo je npr. test.sh:

for input in herman.in.*; do
    extension="${input##*.}"
    output=herman.out.${extension}
    ./herman.exe < $input > izlaz.txt
    diff -w izlaz.txt $output
done

(Za Windows nemam pojma.) Da bismo je mogli pokrenuti, treba joj dodati executable flag:

chmod +x test.sh

… i potom je pokrećemo kao što bismo pokrenuli i program:

./test.sh

Riječ je o ružnom jeziku Bash koji je dobar za jednostavno Unix/Linux skriptiranje i ni za što drugo. U njemu i razmaci rade probleme.

Unaprijedimo našu Bash skriptu s još nekoliko stvari. Recimo, želimo da nam ista skripta može poslužiti za svaki zadatak, a ne da svaki put moramo mijenjati ime zadatka u skripti (u ovom slučaju Herman). Rješenje je uvesti varijablu umjesto tog imena, a tu varijablu pišemo kao $1 jer je riječ o prvom argumentu koji ćemo napisati desno od imena skripte pri njezinom pokretanju:

./test.sh herman

A skripta onda izgleda ovako:

for input in $1.in.*; do
    extension="${input##*.}"
    output=$1.out.${extension}
    ./$1.exe < $input > izlaz.txt
    diff -w izlaz.txt $output
done

Ako želimo “customizirati” i naziv rješenja, za koji smo dosad pretpostavljali da je oblika imeZadatka.exe, možemo dodati još jedan argument $2:

for input in $1.in.*; do
    extension="${input##*.}"
    output=$1.out.${extension}
    ./$2 < $input > izlaz.txt
    diff -w izlaz.txt $output
done

… nakon čega je pokrećemo npr. ovako:

./test.sh herman herman.exe

Možemo napisati i varijantu s trećim argumentom – rednim brojem primjera – ako želimo testirati samo određeni primjer. Evo i varijante koja ispisuje koji su primjeri prošli te se zaustavlja na prvom primjeru koji je pao:

for input in $1.in.*; do
    extension="${input##*.}"
    output=$1.out.${extension}
    ./$2 < $input > izlaz.txt
    if diff -w izlaz.txt $output; then
        echo $input 'prosao'
    else
        echo $input 'pao'
        exit 0
    fi
done

Primjer outputa:

herman.in.1 prosao
herman.in.2 prosao
herman.in.3 prosao
1c1
< 0
---
> 565315
herman.in.4 pao

Ni ja ne znam Bash, većinu ovoga sam izguglao. Ako netko ima primjedbi ili prijedloga poboljšanja, neka napiše u komentarima. Za neki od idućih postova nastojat ću napisati Python skriptu koja radi isto, a i više od ovoga.

Tip of the day: svi su oni isti

Na jednom EJOI-u prije nekoliko godina, juniori (možda Nežmah i Pavić) ispričali su mi zanimljiv matematički zadatak. Ovdje ga pišem po sjećanju pa možda nije identičan originalu.

U avion sa 100 sjedala ulazi 100 ljudi jedan po jedan, a svakome je dodijeljeno mjesto na koje treba sjesti. No prva osoba je pijanac koji ne gleda kamo sjeda, nego sjeda na slučajno odabrano mjesto i zaspi te počne glasno hrkati. Svaka sljedeća osoba sjeda na svoje mjesto ako je ono slobodno, a inače (ako je njeno mjesto zauzeto) sjeda na neko drugo, slučajno odabrano slobodno mjesto. Kolika je vjerojatnost da će posljednja osoba sjesti na svoje mjesto?

(Da me ne bi tko uhvatio u rodnoj diskriminaciji, odmah napominjem da pijanac nije nužno muškarac. Pažljiviji izraz bio bi “pijana osoba”.)

Zadatak ima vrlo kratko, jednostavno i lijepo rješenje kojega se nije lako dosjetiti. Kao hint preporučujem zadatak Mravi sa HIO 2004. u čijem se rješenju koristi donekle slična dosjetka. Drugi hint nalazi se u naslovu ovog posta, a rješenje ću za koji dan napisati u komentar.

Tip of the day: Linux

Većinu stvari u životu bolje je naučiti prije nego poslije.

Jedna od njih je pravilno tipanje. Nakon što naučite koji prst mora pritisnuti koju tipku da bi vam tipkanje bilo brže i ugodnije (a sami intuitivno nećete to skužiti), bit će vam žao što ste ikada tipkali pogrešno.

Druga i još važnija stvar je Linux, ako planirate jednom otići na međunarodno natjecanje ili se planirate baviti ikakvim programiranjem ikada u životu.

Preporučio bih i ostale Errichtove objave: https://www.youtube.com/channel/UCBr_Fu6q9iHYQCh13jmpbrg

(Naravno, postoji još stvari u životu koje je bolje naučiti prije nego poslije. Od ljubavi do meditacije. Izguglajte.)

Tip of the day: Stjepanovi snippeti

Kad naučite Python i počnete ga koristiti u stručne ili znanstvene svrhe, više nikada nećete napisati nijedan algoritam: jednostavno ćete iskoristiti neki od desetak tisuća dostupnih modula. Sve je već napisano, čemu duplicirati kod? Malo pretjerujem, naravno, ali nije ovo daleko od istine. Proguglajte malo Python libraries. Naravno, na natjecanju je dostupna samo standardna Python biblioteka, ali natjecanja ćete ionako brzo prerasti.

A do tada, ako se za natjecanja pripremate u C++u, bacite oko na “module” Stjepana Glavine:

https://github.com/stjepang/snippets

Dosta korisno za razne online zadatke, čisto i jednostavno za ubaciti u kod (plug & play). Readme na dnu stranice sve objašnjava. Ako niste upoznati s namespaceovima, primjer korištenja možete vidjeti npr. u rješenju skloniste_flow.cxx u folderu suboptimalnih rješenja s Izbornih priprema (http://hsin.hr/pripreme2019/zadaci/rjesenja.zip).

Naravno, budući da ovo nemate na natjecanjima, ideja je i da vam posluži kao referenca da naučite kako što implementirati. Kad smo već kod toga, prilažem i jedan sličan, stariji dokument, tzv. Zagreb reference (implementacije raznih algoritama) autora Lovre Pužara.

Tip of the day: broj najkraćih putova i Dijkstrin DAG

Nakon guglanja o tome kaže li se putovi ili putevi, još uvijek nisam siguran što je ovdje ispravno. Kažu da su putovi konkretni, a putevi apstraktni. Za putove u našem smislu, tj. u smislu teorije grafova, može se reći i jedno i drugo. Sigurno su apstraktniji od šumskih putova, ali i konkretniji od, recimo, puteva mudrosti. Odlučio sam se za “konkretnu” varijantu: putovi. (Za one koji se zgražaju nad ovim jezičnim pseudoproblemima: nevažno je, naravno, ali je zabavno, i stvar estetike – shvatite to kao formatiranje teksta.) Dakle: putovi.

Floyd-Warshallov algoritam jedan je od najljepših algoritama koje znam jer u svojih samo 5-6 redaka koda krije mnogo mudrosti (puteva mudrosti, jel). Kao što učinak samog algoritma nije nimalo očit, tako možda nije očito ni da se algoritam može jednostavno nadopuniti tako da izračuna i koliko ima najkraćih putova između svakih dvaju vrhova.

Probajte to sami smisliti. Nije teško, ali je malo tricky. Recimo, prvi odgovor (označen zelenom kvačicom) na Stack Overflowu nije ispravan. Točno rješenje dano je u odgovoru ispod njega (kod počinje s procedure FloydWarshallWithCount ()).

Naravno, mana Floyd-Warshalla je složenost od O(N^3) i zato, osim za guste grafove, prednost ipak ima Dijkstrin algoritam koji se također može nadopuniti tako da računa i broj najkraćih putova.

Evo zadatka gdje to možete isprobati: Šetnja2 (IBT 2010.).

Korišteni bridovi koji se nalaze na najkraćim putovima iz nekog fiksnog vrha čine tzv. Dijkstrin DAG. (Ako su najkraći putovi jedinstveni, riječ je o stablu.) On ima glavnu ulogu u zadatku Najkraći (zadatak dana!) s HONI-ja 2008. (Ako vam je dosad promaklo, rješenja svih HONI-ja možete pronaći ovdje.) Update: još dva zadatka predložio je Marin u komentaru dolje.

Tip of the day: AB prebrojavanje

Jučer se na Hrvatskoj informatičkoj olimpijadi pojavio zadatak Ljepotica autora Ivana Paljka. U zadatku se traži zbroj velikih brojeva s određenim svojstvom, koji se nalaze između dvaju velikih brojeva A i B (koji imaju do 1000 znamenaka). Zadatke koji odgovaraju navedenom kalupu obično rješavamo tzv. AB prebrojavanjem.

Riječ je naprosto o dinamičkom programiranju koje je zbog pojavljivanja u ovom specifičnom obliku dobilo posebno ime. Zamišljamo da veliki broj gradimo znamenku po znamenku te nas u nekom trenutku zanima na koliko načina možemo napisati broj do kraja (ili zbroj tih mogućnosti, ili nešto drugo, ovisno o zadatku). Poanta je da nam za odgovor na to pitanje nisu bitne sve dosad odabrane znamenke, tj. stanje je moguće opisati manjim brojem parametara, od kojih je jedan redovito pozicija na kojoj se nalazimo (tj. redni broj trenutačne znamenke), a drugi ovisi o zadatku, tj. o traženom svojstvu broja koji konstruiramo (npr. pamtimo broj preostalih promjena u zadatku Ljepotica).

Da bismo se ograničili na brojeve iz intervala [A, B], treba nam još jedan parametar: jesmo li dosad odabranim znamenkama već osigurali da će broj koji konstruiramo biti strogo manji od B, ili on još uvijek može biti jednak B, tj. podudaraju li se dosadašnje znamenke sa znamenkama broja B. U stanja gdje bismo premašili B nećemo ni ući, a to osiguravamo upravo iz navedenog parametra, znajući moramo li pri odabiru trenutačne znamenke paziti da ne premašimo istu znamenku u broju B (ako smo dosad birali iste znamenke kao u broju B).

Takva dinamika ne vodi računa o donjoj granici A, ali poslije samo provedemo isti algoritam s tom granicom da bismo oduzeli brojeve manje od A koje smo prethodno ubrojili. Drugim riječima, računamo koliko ih ima do B, minus koliko ih ima do A – 1, čime dobivamo koliko ih ima od A do B.

Osim ovih generičkih ideja, svaki zadatak ovog tipa može biti na svoj način osobit i ponekad je potrebna još neka specifična dosjetka, kao npr. u zadatku Umnožak koji se također pojavio na HIO, desetak godina prije. Otprilike u to vrijeme i na HONI-ju se pojavio zadatak Trešnja ovoga tipa. Priču je moguće i malo okrenuti, pa tražiti N-ti broj koji zadovoljava neko svojstvo, kao u zadatku Sretni s IBT-a.

Tipičan zadatak za početnike bio bi npr. Sum of Digits – ovakve zadatke lako je debugirati jer je lako skodirati brutfors i izmišljati testne primjere. Još mnogo objašnjenja i zadataka dostupno je na webu ako tražite Digit DP. Primjerice:

Dvije stablaste fore

Za neko ukorijenjeno stablo, poredajmo vrhove stabla u niz onim redom kojim ih dohvaćamo DFS obilaskom iz korijena. Taj niz ima svojstvo da svako podstablo čini interval, tj. uzastopni podniz. (Ako dva vrha pripadaju nekom podstablu, onda se između njih u nizu nalaze samo vrhovi iz istog podstabla.) Koristeći taj niz, neki upit na podstablu svodi se na upit na intervalu niza. Nekoliko zadataka koji koriste tu foru:

No posljednji zadatak mnogo je lakše riješiti koristeći manje-u-veće trik. Ako imamo neke skupove od ukupno N elemenata i želimo ih spajati jedan u drugi dok ih sve ne spojimo (spajanja čine stablo), i ako pri spajanju dvaju skupova pazimo da uvijek prebacujemo elemente iz manjeg skupa u veći, svaki element će biti pomaknut najviše log(N) puta jer svakim prijelazom dolazi u barem duplo veći skup. To možemo koristiti kad neke informacije dižemo iz listova prema korijenu stabla, u svakom čvoru spajajući skupove koje smo prethodno izračunali za njegovu djecu, kao u navedenom zadatku Distinct colors. Tim je trikom moguće riješiti i Stablo u stablu iz gornje liste – to je uspjelo Anti Đereku dok smo pripremali zadatak. Još jedan zadatak s tim trikom: Loza (HIO 2010.).

Još zadataka slobodno linkajte u komentarima.