Univerzitet u Sarajevu Elektrotehnički fakultet u Sarajevu KANTONALNO TAKMIČENJE IZ INFORMATIKE ZA SREDNJE ŠKOLE ZADACI ZA PRIPREMU SA RJEŠENJIMA Priredili: mr Vedran Ljubović, dipl. ing. el. Alvin Abdagić, MoE – dipl. ing. el. Marko Lalić Amer Mešanović Doc. dr Samim Konjicija, dipl. ing. el. Sarajevo, Mart 2012. godine 1 Ovaj dokument će biti aktivno dorađivan. Posljednja verzija dokumenta bit će dostupna na stranici http://takmicenje.etf.unsa.ba. Trenutna verzija dokumenta je: 5.1. Sadržaj: PRAVILA KANTONALNOG TAKMIČENJA IZ INFORMATIKE................................................................5 SILABUS KANTONALNOG TAKMIČENJA IZ INFORMATIKE.................................................................7 1.Strategija rješavanja programskih zadataka.............................................................................................................11 Zadatak 1.1. Prethodni i sljedeći datum................................................................................................................11 Zadatak 1.2. Razlika datuma...................................................................................................................................19 Zadatak 1.3. Rimski brojevi....................................................................................................................................22 Zadatak 1.4. Numerička matematika.....................................................................................................................24 Zadatak 1.5. Bazen (jednostavnija varijanta)........................................................................................................27 2.Operacije s nizovima..................................................................................................................................................28 Zadatak 2.1. Najveći među najmanjima................................................................................................................28 Zadatak 2.2. Histogram...........................................................................................................................................30 Zadatak 2.3. Operacije sa skupovima....................................................................................................................32 3.Prosti brojevi................................................................................................................................................................33 Zadatak 3.1. Razdvojiti proste od složenih..........................................................................................................33 Zadatak 3.2. Kazna...................................................................................................................................................36 Zadatak 3.3. Igra.......................................................................................................................................................38 Zadatak 3.4. Prosti blizanci.....................................................................................................................................41 4.Iscrpna pretraga (exhaustive search, brute-force metod).....................................................................................43 Zadatak 4.1. Konjićev skok.....................................................................................................................................43 5.Binarna pretraga..........................................................................................................................................................46 Zadatak 5.1. Pretraga brojeva.................................................................................................................................46 6.Rekurzija.......................................................................................................................................................................49 Zadatak 6.1. Hanojske kule.....................................................................................................................................51 Zadatak 6.2. Flood fill..............................................................................................................................................53 7.Sortiranje......................................................................................................................................................................57 Zadatak 7.1. Elementarna nepogoda.....................................................................................................................59 8.Efikasno stepenovanje...............................................................................................................................................62 Zadatak 8.1. Efikasno stepenovanje......................................................................................................................62 9.Pohlepni (greedy) algoritmi.......................................................................................................................................64 Zadatak 9.1. Lopov...................................................................................................................................................64 10.Osnovna geometrijska tijela (pravougaonici, kružnice)......................................................................................67 Zadatak 10.1. Majansko prokletstvo......................................................................................................................67 Zadatak 10.2. Obuhvatanje tačaka.........................................................................................................................72 11.Rad sa stringovima....................................................................................................................................................75 Zadatak 11.1. Cenzura.............................................................................................................................................75 Zadatak 11.2. Pravilan jezik....................................................................................................................................75 Zadatak 11.3. Prijemni ispit....................................................................................................................................78 Zadatak 11.4. Spellchecker......................................................................................................................................79 12.Veliki broj...................................................................................................................................................................80 Zadatak 12.1. Zbir i proizvod dva velika broja....................................................................................................80 13.Grafovi i stabla..........................................................................................................................................................84 Zadatak 13.1. Presjedanje........................................................................................................................................84 2 Zadatak 13.2. BIHAMK..........................................................................................................................................87 Zadatak 13.3. Nomenklatura..................................................................................................................................91 Zadatak 13.4. Farma.................................................................................................................................................96 Literatura..........................................................................................................................................................................97 Zadatak Naziv Težina 1.1. Prethodni i sljedeći datum O 1.2. Razlika datuma S 1.3. Rimski brojevi O 1.4. Numerička matematika S 1.5. Bazen (jednostavnija varijanta) O 2.1. Najveći među najmanjima O 2.2. Histogram S 2.3. Operacije sa skupovima O 3.1. Razdvojiti proste od složenih S 3.2. Kazna S 3.3. Igra S 3.4. Prosti blizanci S 4.1. Konjićev skok N 5.1. Pretraga brojeva N 6.1. Hanojske kule N 6.2. Flood fill N 7.1. Elementarna nepogoda N 8.1. Efikasno stepenovanje S 9.1. Lopov N 10.1. Majansko prokletstvo N 10.2. Obuhvatanje tačaka S 11.1. Cenzura S 11.2. Pravilan jezik S-N 11.3. Prijemni ispit S 11.4. Spellchecker S-N 12.1. Zbir i proizvod dva velika broja N 3 13.1. Presjedanje N 13.2. BIHAMK N 13.3. Nomenklatura N 13.4. Farma I Legenda: O – osnovno znanje (očekuje se da zadatak može uraditi učesnik sa znanjem stečenim u redovnoj srednjoškolskoj nastavi, bez posebne pripreme) S – srednje (preporučujemo takmičarima da ponove ove oblasti) N – napredne teme (teme koje su moguće na kantonalnom takmičenju (max. jedan zadatak), potrebna je posebna priprema) I – teme koje neće biti obuhvaćene kantonalnim takmičenjem, ali su moguće na državnom takmičenju iz informatike i međunarodnoj olimpijadi (IOI) 4 PRAVILA KANTONALNOG TAKMIČENJA IZ INFORMATIKE Svi zadaci na Kantonalnom takmičenjimu iz informatike isključivo su programerski zadaci i težište je na dizajniranju ispravnog i (vremenski i memorijski) efikasnog algoritma. Zadaci se mogu rješavati u bilo kojem od ponuđenih programskih jezika. Ulazne i izlazne operacije ograničavaju se na osnovne operacije (čitanje iz tekstualne datoteke i upis u tekstualnu datoteku), tako se pažnja takmičara može usmjeriti na sam algoritam. Tačan pregled oblasti i tema koje su obuhvaćene takmičarskim zadacima (silabus) dostupan je u nastavku dokumenta. Oprema, pribor i materijal za takmičenje, tok takmičenja Za takmičenje se koriste standardni PC računari sa ograničenim mrežnim pristupom. Takmičari će na radnim mjestima dobiti printane verzije zadataka za takmičenje, dovoljno praznih papira, pribor za pisanje i pristupne podatke za takmičarski web interfejs. Takmičarima nije dozvoljeno unositi bilo kakav dodatni pribor, literaturu ili opremu. Takmičarima je dopuštena samo usmena komunikacija sa osobom koja je dežurna u kabinetu ili sa administratorom i to isključivo na temu lične ili tehničke prirode a nikako na temu zadataka. Bilo kakvo odstupanje od ovih pravila rezultirat će diskvalifikacijom. Programski jezici i okruženje Takmičarima je dopušteno korištenje programskih jezika Pascal i C/C++ za izradu rješenja. Na samom takmičenju, računari će biti sa instaliranim Linux (Ubuntu) operativnim sistemom, te FreePascal i Code::Block razvojnim okruženjima. Svi detalji o okruženju mogu se preuzeti sa web stranice http://takmicenje.etf.unsa.ba Broj zadataka i trajanje takmičenja Takmičari imaju 120 minuta za izradu rješenja. Takmičari će usmeno biti napomenuti onda kada do kraja takmičenja ostane 15 minuta i 5 minuta. Rješavaju se tri zadatka. Ograničenja i zahtjevi na program Vaš program treba da ulazne podatke čita iz ulazne datoteke, te da svoja rješenja zapisuje u izlaznu datoteku. Nazivi i format sadražaja obje datoteke bit će precizno definiran za svaki zadatak. Vaš program ne smije koristiti bilo kakve druge datoteke osim dvije navedene. Vaš program treba da se izvršava unutar određenih vremenskih i memorijskih ograničenja koja će biti navedena za svaki zadatak. Zabranjeno je pozivanje bilo kakvih drugih sistemskih funkcija kao i fork-anje (otvaranje dodatnih thread-ova). Ispravnost zapisane izlazne datoteke bit će provjeravana samo ukoliko je program sa izvršavanjem završio “normalno”, odnosno ako je operativnom sistemu vratio vrijednost 0. Sistem bodovanja Evaluacija takmičarskih rješenja vrši se automatiziranim sistemom odmah po završetku takmičenja. Za svaki zadatak komisija unaprijed definiše skup testova koje će sistem provesti. Svaki test se evaluira individualno i nosi određeni broj bodova. Test se sastoji od sadržaja ulazne datoteke i očekivanog sadržaja izlazne datoteke. Test se smatra uspješnim i za njega se dodjeljuju bodovi onda kada takmičarski program pod ranije navedenim ograničenjima za specifični sadržaj ulazne datoteke generiše očekivani sadržaj izlazne datoteke. Neophodno je pridržavati se tačno definisanih formata obje datoteke. Svaki zadatak će uključivati dovoljan broj testova različite kompleksnosti kako bi se na taj način osigurala adekvatna distribucija bodova u skladu sa vremenskom i memorijskom efikasnošću takmičarskog rješenja (uz neophodnu ispravnost rješenja). Takmičarski web interfejs 5 Takmičari će za vrijeme takmičenja imati ograničen mrežni pristup samo prema serveru za takmičenje kroz web preglednik u vidu takmičarskog web interfejsa. Ovaj interfejs prvenstveno omogućava takmičarima da šalju svoju rješenja za vrijeme takmičenja. Jedino rješenja poslata na ovaj način će biti evaluirana na ranije opisani način. Nakon slanja rješenja, sistem će rješenje odmah testirati na jednom ili više tzv. trivijalnih testova i rezultate ponuditi takmičaru. Sadržaj ulaznih datoteka trivijalnih testova kao i očekivani sadržaj izlaznih datoteka ovih testova bit će dostupan takmičarima u sklopu postavke zadatka. Trivijalni testovi su po složenosti jako jednostavni i služe kako bi se testiralo da li takmičar ispravno koristi ulazne i izlazne datoteka, pridržava se navedenog formata ulaznih i izlaznih podataka i slično. Trivijalni testni slučajevi ne donose nikakve poene, ali se rješenja koja ne prolaze trivijalne testove neće evaluirati po završetku takmičenja i bodovat će se sa 0 bodova. Ne postoji ograničenje na broj koliko puta takmičar smije poslati rješenje nekog zadataka. Posljenje rješenje koje je prošlo trivijalne testove za određeni zadatak će biti testirano po završetku takmičenja. Takmičarski web interfejs će se koristiti i za eventualna pitanja koja takmičari mogu imati za komisiju. Takmičari će prije samog takmičenja imati priliku da se upoznaju sa takmičarskim web interfejsom. Termini kada će ovo biti moguće će biti objavljeni na stranici http://takmicenje.etf.unsa.ba Pitanja Takmičari pitanja postavljaju i komisija na njih daje odgovor kroz takmičarski web interfejs. Komisija na pitanja takmičara može dati odgovor DA, NE ili BEZ KOMENTARA. Pitanja trebaju biti postavljena tako da je na njih moguć DA ili NE odgovor. Ukoliko pitanje nije tako postavljeno, komisija daje odgovor BEZ KOMENTARA. Osim toga, ukoliko takmičar postavi pitanje na koje komisija ne može dati odgovor zato što bi to u nepovoljan položaj dovela druge takmičare, daje se odgovor BEZ KOMENTARA. U slučajevima kada se daje odgovor BEZ KOMENTARA, komisija može dodatno pojasniti zašto takmičar nije dobio odgovor na svoje pitanje. Rezultati i reklamacije Po završetku automatske evaluacije, objavljuju se preliminarni rezultati. Takmičari mogu korištenjem pristupnih podataka koje se koristili i za vrijeme takmičenja, pristupiti takmičarskom web interfejsu. Interfejs će po zavšetku takmičenja omogućiti detaljan pregled svih testova i rezultata testiranja. Svi eventualni prigovori podnose se u pismenoj formi komisiji koja ih razmatra. 6 SILABUS KANTONALNOG TAKMIČENJA IZ INFORMATIKE (pregled oblasti i tema koje su obuhvaćene takmičarskim zadacima) Legenda zahtjevnosti pojedinih oblasti: O – osnovno znanje (očekuje se da zadatak može uraditi učesnik sa znanjem stečenim u redovnoj srednjoškolskoj nastavi, bez posebne pripreme) S – srednje (preporučujemo takmičarima da ponove ove oblasti) N – napredne teme (teme koje su moguće na kantonalnom takmičenju (max. jedan zadatak), potrebna je posebna priprema) I – teme koje neće biti obuhvaćene kantonalnim takmičenjem, ali su moguće na državnom takmičenju iz informatike i međunarodnoj olimpijadi (IOI) Znanja iz domena osnovne računarske pismenosti koja su preduslov za učešće na takmičenju su: - Osnovna struktura i način rada računara (CPU, memorija, ulaz/izlaz) - Upotreba standardnog grafičkog okruženja - Upotreba uobičajenih pomoćnih aplikacija u operativnom sistemu - Upotreba integrisanog razvojnog okruženja (IDE) - Rad sa datotečnim sistemom (kreiranje direktorija/foldera, kopiranje, premještanje i brisanje datoteka) - Upotreba web preglednika i rad sa internetom (posebno: slanje datoteke putem web preglednika) 1. MATEMATIKA Težina TEMA O Cijeli brojevi (engl. integers) i brojevni sistemi O Matematičke operacije i poređenja O Osobine cijelih brojeva (pozitivni/negativni, parni/neparni, djeljivost, prosti brojevi /engl. primes/) O Razlomci, postotni račun O Tačke u Dekartovom koordinatnom sistemu (dvodimenzionalnom), vektori O Euklidova udaljenost, Pitagorina teorema I Duži, osobine presjeka S Uglovi O Osnovni geometrijski oblici: trougao, pravougaonik, kvadrat, krug 7 I Višeugaonik (poligon) O Matematske funkcije, relacije, skupovi I Dirihleov princip O Osnove matematske logike (logičke operacije, istinosne tabele) I Napredne teme iz matematske logike (predikatska logika, modus ponens i modus tolens) I Tehnike dokazivanja (direktan dokaz, dokaz preko kontradikcije, matematska indukcija) N Nizovi, redovi, aritmetičke i geometrijske progresije, Fibonaćijevi brojevi S Permutacije i kombinacije S Faktorijel, binomni koeficijenti, Paskalova jednačina, binomni teorem N Stabla S Neusmjereni i usmjereni grafovi S Strategije prolaska kroz grafove I Spanning trees, decorated graphs, multigraphs 2. OSNOVE PROGRAMIRANJA I STRUKTURE PODATAKA Težina TEMA O Osnove sintakse i semantike programskog jezika Pascal ili C O Promjenljive (varijable), tipovi podataka (boolean, integer, character), izrazi, naredba dodjele O Jednostavan ulaz/izlaz O Uslovi i grananje O Petlje O Funkcije, prosljeđivanje parametara O Strukturna dekompozicija problema (razbijanje problema na podprobleme) S Strategije rješavanja programskih zadataka S Pojam algoritma, uloga algoritma u procesu rješavanja zadatka S Strategije uklanjanja grešaka (debugging) N Osobine algoritama: korektnost, efikasnost O Nizovi (arrays) O Slogovi (records) O Stringovi (strings) S Statičke (static) i globalne (global) varijable N Linkovane liste (linked list) - implementacija preko niza N Grafovi (graph) i stabla (tree) - implementacija preko niza 8 I Stekovi (stack) i redovi (queue) - implementacija preko niza I Gomile (heap), Fenwick-ovo stablo I Strategije za izbor optimalne strukture podataka I Apstraktni tipovi podataka (abstract data types - ADT), prioritetni red (priority queue), dinamički skup (dynamic set), dinamička mapa (dynamic map) N Rekurzija (recursion), jednostavne rekurzivne procedure I Rekurzivne matematske funkcije I Strategije "zavadi-pa-vladaj" (divide-and-conquer) 3. ALGORITMI Težina TEMA I Formalna specifikacija algoritama, korektnost, invarijantnost I Asimptotska analiza gornje granice kompleksnosti, big-O notacija I Standardni slučajevi kompleksnosti I Vremenski i prostorni kompromisi u algoritmima O Jednostavne strategije dizajna petlje S Algoritmi brutalne sile (brute-force), odnosno iscrpne pretrage (exhaustive search) N Pohlepni (greedy) algoritmi I Algoritmi "zavadi-pa-vladaj" (divide-and-conquer) I Backtracking algoritmi (rekurzivni i nerekurzivni) I Branch-and-bound algoritmi S Prepoznavanje uzoraka i algoritmi za rad sa stringovima (pri čemu se ne zahtijeva razumijevanje korektnosti i efikasnosti ovih algoritama) I Dinamičko programiranje (dynamic programming) I Algoritmi diskretne aproksimacije (discrete approximation) O Algoritmi za konverziju brojevnih sistema O Euklidov algoritam S Provjera da li je broj prost S Erastotenovo sito S Faktorizacija S Efikasno stepenovanje N Operacije nad cijelim brojevima proizvoljnog broja cifara O Popunjavanje niza 9 S Pomjeranje niza, izbacivanje elementa iz niza, rotiranje niza S Okretanje niza S Promjena veličine niza O Traženje najvećeg/najmanjeg člana u nizu S Histogram niza (prebrojavanje članova niza) O Sumiranje niza (uključujući parcijalne sume) O Sekvencijalna obrada članova niza (uključujući sekvencijalno pretraživanje niza) S Binarna pretraga (binary search) S Jednostavni algoritmi sortiranja: selection sort, insertion sort, bubble sort I Quick sort, Heap sort, Merge sort N Algoritmi za kretanje kroz grafove (implementirane preko niza), uključući kretanje po dubini (depth-first search - DFS) i po širini (breadth-first search - BFS) N Algoritmi za kretanje kroz stablo (implementirano preko niza) I Algoritmi najkraćeg puta (Dijkstra, Bellman-Ford, Floyd-Warshall) I Algoritmi minimalnog stabla (Jarnik-Prim, Kruskal) I Topološko sortiranje I Algoritmi za određivanje Eulerovog puta/ciklusa I Osnove teorije igara, minimaks algoritmi I Geometrijski algoritmi (presjeci duži, lokacija tačke u poligonu, algoritmi za određivanje konveksnog omotača /convex hull/ itd.) ali: S Lociranje tačke u osnovnim geometrijskim oblicima: kružnica, pravougaonik, kvadrat 10 1. Strategija rješavanja programskih zadataka U nastavku smo dali nekoliko primjera zadataka u kojima se diskutuje generalan pristup rješavanju programskih zadataka kod kojih ne postoji neki specifičan algoritam ili model rješavanja, nego je potrebno koristiti zdrav razum, dekompoziciju problema i proceduralni pristup. Zadatak 1.1. Prethodni i sljedeći datum Težina: O Ovaj zadatak je bio na Kantonalnom takmičenju iz informatike 2010. godine Dat je niz datuma u obliku "dd mm gggg". Za svaki datum potrebno je ispisati prethodni i sljedeći datum. Da bi neka godina bila prestupna mora biti djeljiva sa 4. Međutim, od onih godina koje su djeljive sa 4, godine koje su djeljive sa 100 a nisu sa 400 nisu prestupne. Npr. 1900. nije bila prestupna dok je 2000. godina bila prestupna. U prestupnoj godini mjesec februar ima 29 dana dok u ostalim godina ima 28. Ulaz: Najprije je dat jedan cijeli broj N (maksimalno 100) koji predstavlja broj datuma. Nakon toga, svaka linija ulaza sastoji se od tri cijela broja razdvojena znakom razmak koji predstavljaju dan, mjesec i godinu. Izlaz: Za svaku liniju ulaza treba ispisati tekst oblika: Datum: dd.mm.gggg Predhodni dd.mm.gggg. Naredni dd.mm.gggg. godine ili, ukoliko je datum na ulaznoj liniji neispravan, treba ispisati: Datum: dd.mm.gggg neispravan. Primjer ulaza: 5 28 2 1900 1 3 1900 28 2 2000 1 3 2000 29 2 2001 Primjer izlaza: Datum: 28.2.1900 Prethodni 27.2.1900. Naredni 1.3.1900. godine Datum: 1.3.1900 Prethodni 28.2.1900. Naredni 2.3.1900. godine Datum: 28.2.2000 Prethodni 27.2.2000. Naredni 29.2.2000. godine Datum: 1.3.2000 Prethodni 29.2.2000. Naredni 2.3.2000. godine Datum: 29.2.2001 neispravan. Pojašnjenje: Kao i svi programski zadaci, ovaj zadatak treba rješavati dio po dio krećući od stvari koje su vam poznate, te koristiti osobinu programskih jezika da dijelove problema izdvojite u zasebne cjeline (funkcije) kako biste ih odvojeno rješavali i testirali. Najprije ćemo uraditi najlakši dio zadatka, a to je napraviti petlju za unos niza od n datuma i njihov ispis na 11 ekran u traženom formatu: "Datum: dd.mm.yyyy". Biće nam potrebne sljedeće varijable (promjenljive): • cjelobrojne promjenljive za dan, mjesec i godinu • cjelobrojna promjenljiva n za broj datuma • pošto za blok koda koji se izvršava poznat broj puta koristimo for petlju, uvešćemo i cjelobrojnu promjenljivu i kao kontrolnu promjenljivu for petlje. Programski jezik C: int n,i; scanf("%d",&n); int dd,mm,gg; for(i=0;i<n;i++) { scanf("%d %d %d",&dd,&mm,&gg); printf("Datum: %d.%d.%d ",dd,mm,gg); } Okvirni program u Pascal-u koji ćemo popunjavati blokovima koda: program Datumi(output); var i,n,dd,mm,gg : integer; begin readln(n); for i := 1 to n do begin readln(dd,mm,gg); writeln('Datum: ',dd,'.',mm,'.',gg); end; end. Najjednostavnije rješenje je povećati varijablu dd za jedan kako bismo dobili sljedeći datum, odnosno umanjiti za jedan kako bismo dobili prethodni: printf("Prethodni: %d.%d.%d ",dd-1,mm,gg); printf("Sljedeci: %d.%d.%d ",dd+1,mm,gg); Međutim ovo rješenje bi bilo netačno za određeni broj slučajeva. Da vidimo koji su to slučajevi: • Ako je dd=1, oduzimanjem 1 dobićemo 0 što je neispravan datum, nego bismo u tom slučaju trebali umanjiti mm za jedan, a dd treba imati broj dana u prethodnom mjesecu. Npr. ako je unijet datum 1.2.2012 prethodni datum će biti 31.1.2012 pošto januar ima 31 dan. Da bismo ovo mogli utvrditi, potrebno nam je da znamo koliko koji mjesec u godini ima dana. U pitanju je 12 cijelih brojeva dana u mjesecima, pa ćemo deklarisati niz od 12 elemenata koji imaju te vrijednosti. Programski jezik C: int broj_dana[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31}; Programski jezik Pascal: broj_dana: array[1..12] of integer = (31,28,31,30,31,30,31,31,30,31,30,31); 12 U programskom jeziku C prvi indeks u nizu je 0. Da bismo mogli dobiti tačan broj dana naredbom tipa broj_dana[mm], možemo deklarisati niz od 13 elemenata pri čemu je vrijednost prvog člana nula ili možemo uvijek od mjeseca oduzimati broj 1. Opredijelili smo se za prvu varijantu. Pascal nema ovakav problem. Zatim određujemo prethodni broj dana u kodu na sljedeći način (C): prethodni_dan = dd-1; prethodni_mjesec = mm; prethodna_godina = gg; /* provjeravamo da li je potrebno smanjivati mjesec */ if(prethodni_dan<1) { prethodni_mjesec--; /* podesavamo da je prethodni dan zadnji dan u prethodnom mjesecu */ prethodni_dan = broj_dana[prethodni_mjesec]; } Pascal: prethodni_dan := dd-1; prethodni_mjesec := mm; prethodna_godina := gg; { provjeravamo da li je potrebno smanjivati mjesec } if prethodni_dan<1 then begin prethodni_mjesec := prethodni_mjesec-1; { podesavamo da je prethodni dan zadnji dan u prethodnom mjesecu } prethodni_dan := broj_dana[prethodni_mjesec]; end; Ali šta ako je mjesec bio januar, odnosno ako tražimo prethodni datum od datuma 1.1.2012 ? Ispravan odgovor je 31.12.2011. Dakle, ako je prethodni_mjesec postao 0, moramo umanjiti godinu za jedan a mjesec postaviti na 12. Dakle kod sada glasi ovako (C): prethodni_dan = dd-1; prethodni_mjesec = mm; prethodna_godina = gg; /* provjeravamo da li je potrebno smanjivati mjesec */ if(prethodni_dan<1) { prethodni_mjesec--; /* provjeravamo da li je potrebmo smanjivati godinu */ if(prethodni_mjesec<1) { prethodni_mjesec = 12; 13 } /* prethodna_godina--; podesavamo da je prethodni dan zadnji dan u prethodnom mjesecu */ prethodni_dan = broj_dana[prethodni_mjesec]; } Pascal: prethodni_dan := dd-1; prethodni_mjesec := mm; prethodna_godina := gg; { provjeravamo da li je potrebno smanjivati mjesec } if prethodni_dan<1 then begin prethodni_mjesec := prethodni_mjesec-1; { provjeravamo da li je potrebmo smanjivati godinu } if prethodni_mjesec<1 then begin prethodni_mjesec := 12; prethodna_godina := prethodna_godina-1; end; { podesavamo da je prethodni dan zadnji dan u prethodnom mjesecu } prethodni_dan := broj_dana[prethodni_mjesec]; end; Analognim kodom rješavamo pitanje sljedećeg datuma. Uvešćemo još tri varijable naredni_dan, naredni_mjesec i naredna_godina te postaviti uslove za slučaj da je varijabla naredni_dan poprimila vrijednost veću od broja dana u trenutnom mjesecu (u kojem slučaju se naredni_mjesec uvećava za jedan), kao i slučaj kada je naredni_mjesec dobio vrijednost 13 (u kojem slučaju se naredna_godina uvećava za jedan). Ostaje još jedan problematičan slučaj a to je prestupna godina. U prestupnoj godini kao što je 2012. februar ima 29 dana umjesto uobičajenih 28. Napravićemo pomoćnu funkciju koja provjerava da li je godina gg prestupna, pa ako ona vrati logičku istinu (u C-u broj 1) odmah nakon unosa datuma ćemo postaviti broj dana u februaru na 29, u suprotnom na 28. Funkcija glasi ovako u C-u: /* Odredjuje da li je godina prestupna */ int prestupna(int gg) { /*godina je prestupna ako je djeljiva sa 400*/ if(gg%400==0) return 1; /*godina je prestupna ako je djeljiva sa 4, ali ne sa 100*/ if(gg%4==0 && gg%100!=0) return 1; } return 0; 14 Pascal: function prestupna(gg : integer) : boolean; begin if (gg mod 400 = 0) then prestupna := true else if (gg mod 4 = 0) and (gg mod 100 <> 0) then prestupna := true else prestupna := false; end; Konačno, trebali bismo ispisati poruku za neispravne datume na ulazu. Napravićemo pomoćnu funkciju koja provjerava da li je datum ispravan i vraća logičku istinu (u C-u 1) ako jeste, u suprotnom logičku neistinu. Datum je neispravan ako je: • dan ili mjesec manji od 1 • dan veći od broja dana u mjesecu • mjesec veći od 12 Ako funkcija odredi da je datum neispravan, preskočićemo sve ostale naredbe u glavnoj petlji naredbom continue. Funkcija u C-u glasi: int ispravan(int dd,int mm,int gg) { if(dd<1 || dd>broj_dana[mm]) return 0; if(mm<1 || mm>12) return 0; } return 1; A u Pascalu: function ispravan(dd,mm,gg : integer) : boolean; begin if (dd<1) or (dd>broj_dana[mm]) then ispravan := false else if (mm<1) or (mm>12) then ispravan := false else ispravan := true; end; Time je program završen. Programski kod (C): #include <stdio.h> #include <stdlib.h> /*Niz deklariran kao globalna varijabla (dostupna je svakoj funkciji u programu) koji sadrzi broj dana svakog mjeseca, ubacena je nula na pocetku jer indeksiranje u C-u pocinje od nule */ int broj_dana[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31}; 15 /* Odredjuje da li je godina prestupna */ int prestupna(int gg) { /*godina je prestupna ako je djeljiva sa 400*/ if(gg%400==0) return 1; /*godina je prestupna ako je djeljiva sa 4, ali ne sa 100*/ if(gg%4==0 && gg%100!=0) return 1; } return 0; /* Pretpostavljeno je da se radi o pozitivnoj godini, tj. poslije nove ere funkcija ispituje da li je ispravan uneseni datum, tj. da li su dani i mjeseci u datim granicama */ int ispravan(int dd,int mm,int gg) { if(dd<1 || dd>broj_dana[mm]) return 0; if(mm<1 || mm>12) return 0; return 1; } int main() { int n,i; scanf("%d",&n); int dd,mm,gg; int naredni_mjesec,naredni_dan,naredna_godina; int prethodni_mjesec,prethodni_dan,prethodna_godina; for(i=0;i<n;i++) { scanf("%d %d %d",&dd,&mm,&gg); printf("Datum: %d.%d.%d ",dd,mm,gg); /* podesavamo broj dana februara u zavisnosti da li je godina prestupna */ if(prestupna(gg)) broj_dana[2] = 29; else broj_dana[2] = 28; /* provjeravamo ispravnost datuma */ if(!ispravan(dd,mm,gg)) { printf("neispravan\n"); continue; } naredni_dan = dd+1; naredni_mjesec = mm; 16 naredna_godina = gg; /* ispitujemo da li je naredni dan u sljedecem mjesecu, u tom slucaju povecavamo broj mjeseca, a dan stavljamo da je 1 */ if(naredni_dan>broj_dana[mm]) { naredni_dan=1; naredni_mjesec++; } /* ukoliko je naredni_mjesec veci od 12 (tj. jedak 13), potrebno je povecati godinu za 1 */ if(naredni_mjesec>12) { naredni_mjesec = 1; naredna_godina++; } prethodni_dan = dd-1; prethodni_mjesec = mm; prethodna_godina = gg; /* provjeravamo da li je potrebno smanjivati mjesec */ if(prethodni_dan<1) { prethodni_mjesec--; /* provjeravamo da li je potrebmo smanjivati godinu */ if(prethodni_mjesec<1) { prethodni_mjesec = 12; prethodna_godina--; } /* podesavamo da je prethodni dan zadnji dan u prethodnom mjesecu */ prethodni_dan = broj_dana[prethodni_mjesec]; } printf("Predhodni %d.%d.%d. Naredni %d.%d.%d. godine\n", prethodni_dan, prethodni_mjesec, prethodna_godina, naredni_dan, naredni_mjesec, naredna_godina); } } return 0; Programski kod (Pascal): program Datumi(output); 17 var i,n,dd,mm,gg : integer; prethodni_dan, prethodni_mjesec, prethodna_godina, naredni_dan, naredni_mjesec, naredna_godina : integer; broj_dana: array[1..12] of integer = (31,28,31,30,31,30,31,31,30,31,30,31); function prestupna(gg : integer) : boolean; begin if (gg mod 400 = 0) then prestupna := true else if (gg mod 4 = 0) and (gg mod 100 <> 0) then prestupna := true else prestupna := false; end; function ispravan(dd,mm,gg : integer) : boolean; begin if (dd<1) or (dd>broj_dana[mm]) then ispravan := false else if (mm<1) or (mm>12) then ispravan := false else ispravan := true; end; begin readln(n); for i := 1 to n do begin readln(dd,mm,gg); write('Datum: ',dd,'.',mm,'.',gg); if prestupna(gg) then broj_dana[2] := 29 else broj_dana[2] := 28; if not ispravan(dd,mm,gg) then begin writeln(' neispravan'); continue; end; prethodni_dan := dd-1; prethodni_mjesec := mm; prethodna_godina := gg; { provjeravamo da li je potrebno smanjivati mjesec } if prethodni_dan<1 then begin prethodni_mjesec := prethodni_mjesec-1; { provjeravamo da li je potrebno smanjivati godinu } if prethodni_mjesec<1 then begin prethodni_mjesec := 12; prethodna_godina := prethodna_godina-1; end; { podesavamo da je prethodni dan posljednji dan u prethodnom mjesecu } prethodni_dan := broj_dana[prethodni_mjesec]; end; naredni_dan := dd+1; 18 naredni_mjesec := mm; naredna_godina := gg; { provjeravamo da li je potrebno povecati mjesec } if naredni_dan>broj_dana[mm] then begin naredni_mjesec := naredni_mjesec+1; { provjeravamo da li je potrebno povecati godinu } if naredni_mjesec>12 then begin naredni_mjesec := 1; naredna_godina := naredna_godina+1; end; { podesavamo da je naredni dan 1 } naredni_dan := 1; end; write(' Prethodni: ', prethodni_dan, '.', prethodni_mjesec, '.', prethodna_godina, '.'); writeln(' Naredni: ', naredni_dan, '.', naredni_mjesec, '.', naredna_godina, '.'); end. end; Zadatak 1.2. Razlika datuma Težina: S Ovaj zadatak je bio na Kantonalnom takmičenju iz informatike 2008. godine Napisati program koji učitava dva datuma i ispisuje broj dana koliko je proteklo između ta dva datuma. Ulaz: Dvije linije koje predstavljaju datume. Svaka linija sastoji se od tri cijela broja razdvojena razmakom koji predstavljaju dan, mjesec i godinu. Izlaz: Jedan pozitivan cijeli broj koji predstavlja broj dana koji su protekli između dva datuma. Ukoliko je prvi datum bio poslije drugog treba ispisati apsolutnu vrijednost razlike. Ukoliko je jedan od datuma ilegalan treba ispisati "Ilegalan datum". Princip rješavanja: Osnovni princip je sljedeći: najprije oba datuma trebamo pretvoriti u dva cijela broja koji predstavljaju broj dana od nekog hipotetskog datuma 1.1.1. godine do željenog datuma, a zatim jednostavno oduzmemo ta dva broja. U slučaju da je drugi datum poslije prvog, dobićemo negativan broj pa trebamo uzeti apsolutnu vrijednost tog broja. Pri tome pravimo pretpostavku da se isključivo koristi Gregorijanski kalendar; odnosno ovakav program 19 nema nikakvog smisla za datume ranije od 1582. godine. Prema tome i broj dana od "1.1.1." ustvari nije tačan, ali je za potrebe rješavanja zadatka zadovoljavajući. Pretvaranje datuma dd.mm.gg. u željeni broj dana se obavlja na sljedeći način: • najprije saberemo broj dana u godinama od 1. do (gg-1). (zato što je godina gg još uvijek u toku), pri čemu za prestupne godine dodajemo 366 dana, a za ostale 365; ovdje će nam pomoći funkcija za provjeru da li je godina prestupna koju smo razvili u prethodnom zadatku; • zatim saberemo dane za sve mjesece koji su protekli, od januara do mjeseca (mm-1). iz istih razloga; ovdje će nam trebati broj dana u mjesecu, što smo također riješili u prethodnom zadatku; • konačno saberemo dane koji su protekli u tekućem mjesecu a to je broj dd. Radi ljepšeg rješenja u ovom zadatku uvodimo i koncept struktura u programskom jeziku C (ključna riječ struct). Zbog ovoga je neznatno izmijenjena funkcija ispravan(). Programski kod (C): /* napomena: u proracunu nije uzeta u obzir Gregorijanski, ali jesu prestupne godine */ promjena kalendara sa Julijanskog na #include <stdio.h> #include <stdlib.h> /*Niz deklariran kao globalna varijabla (dostupna je svakoj funkciji u programu) koji sadrzi broj dana svakog mjeseca, ubacena je nula na pocetku jer indeksiranje u C-u pocinje od nule */ int broj_dana[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31}; struct datum { int dan; int mje; int god; }; /* Odredjuje da li je godina prestupna */ int prestupna(int gg) { /*godina je prestupna ako je djeljiva sa 400*/ if(gg%400==0) return 1; /*godina je prestupna ako je djeljiva sa 4, ali ne sa 100*/ if(gg%4==0 && gg%100!=0) return 1; } return 0; 20 /* Pretpostavljeno je da se radi o pozitivnoj godini, tj. poslije nove ere funkcija ispituje da li je ispravan uneseni datum, tj. da li su dani i mjeseci u datim granicama */ int ispravan(struct datum d) { if(d.dan<1 || d.dan>broj_dana[mm]) return 0; if(d.mje<1 || d.mje>12) return 0; } return 1; /* Broj dana od 1.1.1. godine do datuma */ int broj_dana(struct datum d) { int rezultat = 0, i; /* Najprije sabiramo godine */ for (i=1; i<d.god; i++) { if (prestupna(i)) rezultat += 366; else rezultat += 365; } /* zatim mjesece */ for (i=1; i<d.mje; i++) { rezultat += broj_dana[i]; } /* Dodatni dan u februaru za prestupnu godinu */ if (i == 2 && prestupna(d.god)) rezultat++; /* i na kraju dane */ rezultat += d.dan; return rezultat; } /* Glavni program */ int main() { /* Deklaracija i unos datuma */ struct datum prvi, drugi; int razlika; scanf("%d %d %d", &prvi.dan, &prvi.mje, &prvi.god); scanf("%d %d %d", &drugi.dan, &drugi.mje, &drugi.god); /* Da li su ispravni datumi */ if (!ispravan(prvi) || !ispravan(drugi)) { printf("Ilegalan datum\n"); return 0; 21 } /* Uzimamo razliku */ razlika = broj_dana(prvi) - broj_dana(drugi); /* Apsolutna vrijednost razlike */ if (razlika < 0) razlika = -razlika; printf ("%d", razlika); return 0; } Zadatak 1.3. Rimski brojevi Težina: O Ovaj zadatak je bio na Kantonalnom takmičenju iz informatike 2010. godine Napisati program koji učitava parove rimskih brojeva, a zatim ispisuje zbir ovih parova, takođe u formi rimskog broja. O rimskim brojevima Rimskim brojevima se ne mogu predstaviti nula ili negativni brojevi, a za predstavljanje broja 4000 i većih koriste se simboli koji se ne mogu predstaviti ASCII znakovima. Iz tog razloga će svi rimski brojevi obavezno u opsegu 1-3999. Rimske cifre su: I V X L C D M 1 5 10 50 100 500 1000 Ostali brojevi dobijaju se kombinovanjem ovih vrijednosti, od većih ka manjim ciframa npr. 2637 = MMDCXXXVII Izuzetak su cifre 4 i 9 koje se dobijaju po sljedećoj tabeli: IV IX XL XC CD CM 4 9 40 90 400 900 Ulaz: Najprije je dat jedan cijeli broj N (maksimalno 100) koji predstavlja broj rimskih brojeva. Nakon toga. svaki red ulaza predstavlja jedan rimski broj opisan kombinacijom velikih slova čije je značenje dato iznad. Izlaz: Za svaka dva broja na ulazu biće ispisan jedan rimski broj koji predstavlja njihov zbir. U slučaju da zbir brojeva bude veći od 3999, umjesto zbira biće ispisana poruka "Prekoracenje". U slučaju da se na ulazu 22 nalazi neparan broj redova posljednji red će biti ignorisan. Programski kod (C++): #include <string> #include <iostream> #include <fstream> using namespace std; string rimcifre[13] = { "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I" }; int arapcifre[13] = { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}; int rimski2arapski(string rimski) { int k(0); int arapski(0); for (int i(0); i<13; i++) { while (rimski.substr(k, rimcifre[i].length()) == rimcifre[i]) { arapski += arapcifre[i]; k += rimcifre[i].length(); } } return arapski; } string arapski2rimski(int arapski) { string rimski(""); for (int i(0); i<13; i++) { while (arapski>=arapcifre[i]) { arapski -= arapcifre[i]; rimski += rimcifre[i]; } } return rimski; } int main() { string sabirak1, sabirak2,zbir; getline(cin,sabirak1); getline(cin,sabirak2); zbir = arapski2rimski( rimski2arapski(sabirak1) + rimski2arapski(sabirak2) ); } cout<<zbir<<endl; return 0; 23 Zadatak 1.4. Numerička matematika Težina: S Ovaj zadatak je bio na Kantonalnom takmičenju iz informatike 2007. godine Metodom polovljenja intervala naći rješenje funkcije (nulu funkcije): f(x) = a1xN+a2xN-1+...aN-x+aN+1 na intervalu (A,B) sa greškom manjom od ε (N<10). Rezultat ispisati na ekran. Ulaz: Na ulazu se nalazi redom (svaki u zasebnom redu): - cijeli brojevi A i B razdvojeni razmakom - realan broj ε - prirodan broj N (manji od 10) koji definiše red funkcije - realni brojevi koji predstavljaju vrijednosti koeficijenata a1, a2,... aN+1 Izlaz: Na izlazu treba ispisati realan broj koji predstavlja vrijednost na intervalu x∈( A , B) za koju je f(x)=0. Pretpostaviti da će uvijek postojati tačno jedna takva vrijednost na zadatom intervalu. Pojašnjenje: Metoda polovljenja intervala (metoda bisekcije, engl. bisection method) je jedna od osnovnih metoda numeričke matematike. Ovom metodom može se odrediti vrijednost x∈(A , B) za koju je f(x)≈0 (ustvari f (x)∈(−ε ,ε) gdje je ε neka unaprijed zadata maksimalna greška). Uslovi za primjenu metode su da je funkcija neprekidna na intervalu (A,B) i da ima jedno rješenje (nulu) na tom intervalu. Matematički se može dokazati da iz ova dva uslova proizlazi da vrijednosti f(A) i f(B) moraju imati različit predznak. Princip rada algoritma je sljedeći: neka je trenutni interval (A,B): • Najprije se izračunava vrijednost funkcije na sredini intervala f(S) pri čemu je S=(A+B)/2. • Ako je ta vrijednost u opsegu (-ε,ε) onda je S naša tražena vrijednost x i petlja se prekida. • Ako f(S) ima isti predznak kao f(A) novi interval je (S,B), a ako ima isti predznak kao f(B) novi interval je (A,S). Drugim riječima, interval se sužava oko tačke u kojoj se nalazi nula funkcije kao na slici. Da bismo riješili ovaj programski zadatak, najprije ćemo napisati funkciju koja izračunava vrijednost datog polinoma za dati niz koeficijenata: f(x) = a1xN+a2xN-1+...aN-x+aN+1 Slika 1: Metoda polovljenja intervala 24 Ova funkcija bi mogla izgledati ovako: /* Izracunavanje vrijednosti polinoma. a = niz koeficijenata, N = red polinoma, x = tacka u kojoj trazimo vrijednost */ float polinom(float a[], int N, float x) { float rezultat = 0; int i; for (i=0; i<=N; i++) { rezultat += a[i] * pow(x, N-i); } return rezultat; } Niz a i vrijednost N su date na ulazu, dok umjesto x uvrštavamo S. Zatim možemo postaviti okvirnu petlju. Petlja se izvršava sve dok je f(S) po apsolutnoj vrijednosti veće od ε, odnosno: while (polinom(a,N,S) > epsilon || polinom(a,N,S) < -epsilon) { ... } U petlji ćemo izračunati novu vrijednost za A, B i S. Za ovo je potrebno da odredimo predznak vrijednosti f(A), f(B) i f(S). Možemo napraviti pomoćnu funkciju za određivanje predznaka (ovakva funkcija se u matematici obično zove signum ili sgn): int sgn(float x) { if (x<0) return -1; return 1; } Sada popunjavamo u petlji dio koji smo označili sa tri tačke: while (polinom(a,N,S) > epsilon || polinom(a,N,S) < -epsilon) { if (sgn(polinom(a,N,S)) == sgn(polinom(a,N,A)) { A = S; /* Novi pocetak intervala */ } else { B = S; /* Novi kraj intervala */ } S = (A+B)/2; /* Nova sredina intervala */ } Ovakvo rješenje je prilično neefikasno. Naime, najsporiji dio ovog programa je izračunavanje polinoma koje se u ovoj petlji izvršava čak 4 puta. Da bismo to popravili, uvesti ćemo pomoćne varijable fS i fA koje predstavljaju ranije izračunate vrijednosti polinoma: while (fS > epsilon || fS < -epsilon) { if (sgn(fS) == sgn(fA)) { A = S; /* Novi pocetak intervala */ fA = fS; /* Koristimo ranije izracunatu vrijednost polinoma */ } else { B = S; /* Novi kraj intervala */ } S = (A+B)/2; /* Nova sredina intervala */ fS = polinom(a,N,S); } 25 Vidimo da se u ovoj petlji funkcija polinom() poziva samo jednom u odnosu na ranija 4 puta. Ostaje još samo da napravimo deklaracije, ulaz i izlaz vrijednosti. Programski kod (C): #include <stdio.h> #include <math.h> /* Funkcija pow() */ /* Izracunavanje vrijednosti polinoma. a = niz koeficijenata, N = red polinoma, x = tacka u kojoj trazimo vrijednost */ float polinom(float a[], int N, float x) { float rezultat = 0; int i; for (i=0; i<=N; i++) { rezultat += a[i] * pow(x, N-i); } return rezultat; } /* Predznak broja x */ int sgn(float x) { if (x<0) return -1; return 1; } int main() { float a[10], A, B, S, fA, fS, epsilon; int N, i; /* Unos vrijednosti */ /* Pretpostavljamo da sve vrijednosti zadovoljavaju uslove zadatka */ scanf("%f %f", &A, &B); scanf("%f", &epsilon); scanf("%d", &N); for (i=0; i<=N; i++) { scanf("%f", &a[i]); } /* Postavljam pocetne vrijednosti za S, fA, fS */ S = (A+B)/2; fA = polinom(a,N,A); fS = polinom(a,N,S); /* Polovljenje intervala */ while (fS > epsilon || fS < -epsilon) { if (sgn(fS) == sgn(fA)) { A = S; /* Novi pocetak intervala */ fA = fS; /* Koristimo ranije izracunatu vrijednost polinoma */ } else { B = S; /* Novi kraj intervala */ } S = (A+B)/2; /* Nova sredina intervala */ fS = polinom(a,N,S); } 26 } printf ("%f", S); return 0; Zadatak 1.5. Bazen (jednostavnija varijanta) Težina: O Ovaj zadatak je bio na Kantonalnom takmičenju iz informatike 2003. godine Izračunati broj pločica potrebnih za prekrivanje bazena. U obračunu predvidjeti otpad pločica: Ako postoji ostatak dijeljenja dužina/širine površine bazena i dužine/širine pločice dodaj jedan red pločica. Ulaz: U prvom redu ulaza su data tri cijela broja x, y i z koji predstavljaju dimenzije bazena (dužina, širina i visina) u cm. U drugom redu su dva cijela broja koji predstavljaju dimenzije jedne pločice (dužina i širina) u cm. Izlaz: Jedan cijeli broj koji predstavlja broj pločica potrebnih za prekrivanje bazena. Programski kod (Pascal): Program Bazen(input, output); Var a,b,x,y,z,p : Integer; (*a duzina, b sirina plocice*) (*x duzina, y sirina, z visina bazena*) Function po_duz(k,l : Integer) : Integer; Begin End; If (k mod l = 0) Then po_duz := k div l Else po_duz := (k div l) +1 Function po_pov(b1,b2,p1,p2 : Integer) : Integer; Begin End; po_pov := po_duz(b1, p1) * po_duz(b2, p2) Begin Write(‘Unesi dimenzije bazena: ’); Readln(x,y,z); Write(‘Unesi dimenzije plocice: ’); Readln(a,b); p := 2*(po_pov(x, z, a, b) + po_pov(y, z, a, b)) + po_pov(x, y, a, b); Writeln(‘Broj potrebnih plocica je: ’,p) End. 27 2. Operacije s nizovima Zadatak 2.1. Najveći među najmanjima Težina: O Data je matrica dimenzija R x K, vaš zadatak je da za svaku kolonu te matrice odredite najmanji element, te zatim da među tim elementima odredite najveći. Elementi matrice mogu biti proizvoljni brojevi. Ulaz: U prvom redu ulaza se nalaze dva broja r i k ( r,k ≤ 1000 ) koji predstavljaju broj redova i kolona matrice, respektivno. U sljedećih r redova će se nalaziti po k brojeva koji predstavljaju elemente matrice. Izlaz: U jedini red izlaza potrebno je ispisati traženi broj. Pojašnjenje (Nalaženje minimuma i maksimuma niza): U takmičarskim zadacima često se zahtjeva od programera da nađe najefikasnije rješenje nekog problema, što se često postiže traženjem najveće ili najmanje određene vrijednosti. Zbog toga ćemo ovdje ukratko opisati proceduru nalaženja najmanjeg/najvećeg elementa niza. Za nalaženje najvećeg elementa nekog niza, polazimo od sljedećeg: - Deklarišemo varijablu npr. max i dodijelimo joj vrijednost prvog člana niza. Eventualno možemo koristiti najmanju moguću vrijednost ako je ona poznata (npr. ako znamo da su svi članovi pozitivni možemo postaviti max na 0). - Prethodno će možda biti potrebno provjeriti da li niz ima ijedan element, jer u tom slučaju traženje maksimuma/minimuma neće imati smisla. - Analiziramo svaki član niza redom te ukoliko je on veći od vrijednosti varijable max, dodijelimo vrijednost tog člana niza našoj varijabli max. Ponekad je potrebno i dodatno čuvati indeks najvećeg člana, što se jednostavno implementira pomoću dodatne varijable. Implementacija navedene procedure u programskom jeziku C: int max = a[0]; for(i=1;i<a_velicina;i++) { if(a[i]>max) { max = a[i]; } } Implementacija navedene procedure u programskom jeziku Pascal: max := niz[1]; for i := 2 to velicina do begin if niz[i]>max then max := niz[i]; end Procedura je analogna i za nalaženje najmanjeg elementa niza. Primijetimo da navedeni proces ima O(n) kompleksnost. Najefikasniji algoritmi za sortiranje niza imaju 28 O(n log n) kompleksnost i zbog toga ih ne trebamo koristiti osim ako nam nije potreban cijeli sortiran niz. Programski kod (C): #include <stdio.h> #include <stdlib.h> int main() { int r,k; float matrica[1000][1000]; int i,j,prvi; float min, max; scanf("%d %d",&r,&k); for(i=0; i<r; i++) { for(j=0;j<k;j++) { scanf("%f",&matrica[i][j]); } } /* kod za odredjivanje najmanjeg elementa, odmah nakon sto smo ga odredili, poredimo ga sa dosadasnjim najvecim elementom, da ih ne bismo morali cuvati u zasebnom nizu. Ovo se moglo implementirati odmah tokom ulaza, i ne bi postojala potreba za matricom, ali radi jednostavnosti uradjena je ovakva implementacija */ max = 0; prvi = 1; for(i=0;i<r;i++) { min = matrica[i][0]; for(j=1; j<k;j++) { if(matrica[i][j]<min) { min = matrica[i][j]; } } } if(min > max || prvi == 1) { max = min; prvi = 0; } printf("%f",max); return 0; 29 } Programski kod (Pascal): program NajveciNajmanjih(output); var i,j,max,min,prvi,r,k : integer; matrica: Array[1..1000] of Array[1..1000] of Integer; begin readln(r); readln(k); for i := 1 to r do begin for j := 1 to k do begin readln(matrica[i][j]); end; end; max := 0; prvi := 1; for i := 1 to r do begin min := matrica[i][1]; for j := 2 to k do begin if matrica[i][j]<min then min := matrica[i][j]; end; if (min>max) or (prvi=1) then begin max := min; prvi := 0; end; end; end. writeln(max); Zadatak 2.2. Histogram Težina: S Ovaj zadatak je bio na Kantonalnom takmičenju iz informatike 2003. i 2008. godine. Potrebno je odrediti histogram niza, odnosno broj ponavljanja različitih članova niza prirodnih brojeva manjih od 1000. Ulaz: Cijeli broj N koji predstavlja broj članova niza (maksimalno 100). Nakon toga slijedi N brojeva, svaki u zasebnom redu, pri čemu su svi brojevi na intervalu [0,999]. Izlaz: Svaki red sadrži dva broja razdvojena razmakom. Prvi broj je član niza, a drugi broj je broj ponavljanja toga 30 člana u nizu. Članovi niza trebaju biti poredani po veličini u rastućem redoslijedu i ne smiju se ponavljati. U slučaju da se član niza ne javlja niti jednom, ne treba ga ispisati. Primjer ulaza: 5 234 12 234 8 12 Primjer izlaza: 81 12 2 234 2 Programski kod (C): #include <stdio.h> int main() { int histogram[1000]; int i,n,broj; /* Postavljamo sve članove niza histogram na 0 */ for (i=0; i<1000; i++) histogram[i] = 0; /* Učitavamo broj članova niza */ scanf ("%d", &n); /* Učitavamo članove i istovremeno računamo njihov histogram */ for (i=0; i<n; i++) { scanf("%d", &broj); histogram[broj]++; } /* Ispisujemo histogram */ for (i=0; i<1000; i++) { if (histogram[i]>0) { printf("%d %d\n", i, histogram[i]); } } return 0; } Programski kod (Pascal): program HistogramProg(output); var i,n,broj : integer; histogram: Array[1..1000] of Integer; 31 begin { Postavljamo sve članove niza histogram na 0 } for i := 1 to 1000 do histogram[i] := 0; { Učitavamo broj članova niza } readln(n); { Učitavamo članove i istovremeno računamo njihov histogram } for i := 1 to n do begin readln(broj); histogram[broj+1] := histogram[broj+1] + 1; end; end. { Ispisujemo histogram } for i := 1 to 1000 do begin if histogram[i]>0 then begin writeln(i-1, ' ', histogram[i]); end; end; Zadatak 2.3. Operacije sa skupovima Težina: O Ovaj zadatak je bio na Kantonalnom takmičenju iz informatike 2003. godine Data su dva skupa cijelih brojeva X i Y. Ispisati nizove koji predstavljaju: a) uniju skupova X i Y b) presjek skupova X i Y c) razliku skupova X i Y 32 3. Prosti brojevi Prost broj se definiše kao prirodan broj koji je djeljiv samo sa jedinicom i samim sobom. Ovdje treba naglasiti riječ samo. Naime svaki prirodan broj je djeljiv sa jedinicom i samim sobom, ali prosti brojevi nisu djeljivi niti sa jednim drugim brojem. Jedini način da provjerimo da li je neki broj N prost je da ga probamo podijeliti sa svim brojevima na intervalu (1,N). Ustvari dovoljno je da idemo do √ N (uključivo) jer N sigurno nije djeljiv većim brojevima. Pri rješavanju zadatka možemo koristiti operator modulo, pa ako on vrati nulu znači da je prvi operand djeljiv drugim. Kod u programskom jeziku C: prost = 1; for (i=2; i<=sqrt(N); i++) { if (n%i == 0) { prost = 0; break; } } Kod u programskom jeziku Pascal: prost := true; for i := 2 to Trunc(sqrt(n)) do { cijeli i realni brojevi se ne mogu direktno porediti u Pascalu } begin if n mod i = 0 then begin prost := false; break; end; end; Primjena break naredbe za prekid petlje ako smo ustanovli da broj nije prost u praksi može donijeti znatno poboljšanje performansi. Ovdje je potrebno naglasiti da broj 1 nije niti prost niti složen. Kod dat iznad će za N=1 vratiti da je prost, pa ukoliko to ima efekta na vaš program trebate dodati poseban uslov koji provjerava da li je u pitanju broj 1 ili ne. Ova relativna kompleksnost provjere da li je broj prost dovela je do vrlo raširene upotrebe prostih brojeva u kriptografiji. Ako biste našli efikasniji algoritam za provjeru prostosti broja, čeka vas svjetska slava jer biste mogli "provaliti" mnoge poznate oblike kriptografske zaštite! Zadatak 3.1. Razdvojiti proste od složenih Težina: S Na ulazu se nalazi niz od maksimalno 100 pozitivnih cijelih brojeva. Program treba da ispiše iste te brojeve presložene tako da se najprije ispišu svi članovi niza koji su prosti brojevi, a zatim svi članovi koji su složeni 33 brojevi. Unutar skupova prostih i složenih brojeva treba biti očuvan redoslijed iz polaznog niza, dakle brojevi trebaju biti dati istim redom kao i u polaznom nizu. Ulaz: Broj N koji označava veličinu niza, nakon čega slijedi N pozitivnih cijelih brojeva. Izlaz: Cijeli brojevi presloženi na opisani način razdvojeni znakom novi red. Primjer ulaza: 5 8 7 9 3 5 Primjer izlaza: 7 3 5 8 9 Programski kod (C): #include <stdio.h> int main() { int ulaz[100], prosti[100], slozeni[100]; int n, i, j, prost, brprostih, brslozenih; scanf("%d", &n); /* Unosimo niz ulaz */ for (i=0; i<n; i++) scanf("%d", &ulaz[i]); /* Stavljamo clanove niza ulaz u odgovarajuce nizove */ brprostih = brslozenih = 0; for (i=0; i<n; i++) { /* Da li je ulaz[i] prost broj? */ prost=1; for (j=2; j<sqrt(ulaz[i]); j++) { if (ulaz[i] % j == 0) { prost=0; break; } } /* Ako je prost i veci od jedan stavljamo u niz prosti */ if (ulaz[i]>1 && prost==1) { /* brprostih nam sluzi kao indeks u nizu prosti */ prosti[brprostih] = ulaz[i]; 34 brprostih++; } else { } /* a brslozenih u nizu slozeni */ slozeni[brslozenih] = ulaz[i]; brslozenih++; } /* Ispis niza prosti */ for (i=0; i<brprostih; i++) printf("%d\n", prosti[i]); /* Ispis niza slozeni */ for (i=0; i<brslozenih; i++) printf("%d\n", slozeni[i]); } return 0; Programski kod (Pascal): program RazdvajanjeProstih(output); var ulaz,prosti,slozeni: Array[1..100] of integer; n,i,j,brprostih,brslozenih : integer; prost : boolean; begin readln(n); { unosimo niz brojeva } for i := 1 to n do readln(ulaz[i]); { stavljamo clanove niza u odgovarajuce nizove } brprostih := 1; brslozenih := 1; for i := 1 to n do begin { da li je ulaz[i] prost broj? } prost := true; for j := 2 to Trunc(sqrt(ulaz[i])) do begin if ulaz[i] mod j = 0 then begin prost := false; break; end; end; { ako jeste, stavljamo ga u niz prostih } if (ulaz[i]>1) and prost then begin prosti[brprostih] := ulaz[i]; brprostih := brprostih + 1; end else { a ako nije, u niz slozenih } 35 begin end; slozeni[brslozenih] := ulaz[i]; brslozenih := brslozenih + 1; end; { ispisujemo sve proste brojeve } for i := 1 to brprostih-1 do writeln(brprostih[i]); end. { ispisujemo sve slozene brojeve } for i := 1 to brslozenih-1 do writeln(brslozenih[i]); Zadatak 3.2. Kazna Težina: S Mali Alvin nikada nije volio matematiku, zbog čega je često nemiran na času. Zbog toga mu nastavnik često zadaje poseban problem tokom časa, kao pokušaj da izađe na kraj sa Alvinovim nestašlucima. Profesor bi mu napisao u svesku broj N (gdje je N ≤ 2 31-1), i Alvinov zadatak bi bio da napiše najmanji broj takav da je proizvod njegovih cifara jednak broju N. Naravno, prije toga bi profesor uvijek provjerio da li je to moguće. Kako Alvin ne želi da dugo radi zadatke, zamolio je vas da mu pomognete da vara tako što ćete napisati program koji rješava dati problem. Ulaz: Jedan broj N (gdje je N ≤ 231-1) Izlaz: Jedan broj koji zadovoljava navedene uslove Pojašnjenje (Faktorizacija): U matematici, faktorizacija broja N predstavlja njegovu dekompoziciju na skup prostih brojeva sa tom osobinom kad se pomnože da daju broj N. Tako su: faktorizacije brojeva 60, 1024 i 296846, dok su npr. brojevi 2, 3 i 5 prosti faktori broja 60. Nalaženje prostih faktora je problem sa kojim se takmičari često susreću. Ovaj problem rješavamo bruteforce metodom, tj. ispitivanjem djeljivosti sa svakim manji brojem. Najjednostavnija faktorizacija broja se postiže tako što redom provjeravamo svaki broj koji je manji od N. Ukoliko je N djeljivo tim brojem, dijelimo ga sve dok je taj uslov ispunjen, a zatim prelazimo na sljedeći veći broj. for(i=2; i<=N;i++) { while(N%i==0) { N /=i; faktori[indeks] = i; 36 indeks++; } } Moguće je napraviti određene optimizacije da bismo smanjili vrijeme izvršavanja našeg koda. Kao prvo, primjetimo da ukoliko smo provjerili djeljivost od N brojem 2, ne moramo provjeravati njegovu djeljivost bilo kojim parnim brojem. Tako da djeljivost brojem 2 možemo ispitati posebno izvan for–petlje, dok ćemo u for petlji ispitivati djeljivost samo sa neparnim brojevima. Sljedeća optimizacija koju možemo napraviti je i gornja granica do koje je potrebno provjeravati djeljivost. Matematički je jednostavno pokazati kako je dovoljno da provjeravamo djeljivost sa prostim brojevima koji su manji od , sa još malim dodatkom na kraju koda. Sada naš kod dobija svoj konačni oblik: while(!(N&1)) { /* provjera djeljivosti brojem 2 koristeci binarnu logiku – dodatna optimizacija */ N=N>>1; /* djeljenje brojem 2 pomocu pomjeranja bita – dodatna optimizacija */ faktori[broj_faktora++] = 2; } korjen = (int) sqrt(n); for(i=3;i<=korjen;i+=2) { while(N%i==0) { N/=i; faktori[broj_faktora++] = i; } } if(N>0) faktori[broj_faktora++] = N; Moguće je da je da nakon for–petlje u n „ostane“ još jedan prost broj, koji na ovaj način ubacujemo u niz, npr. ako je n = 26, provjeru ćemo raditi do int ()= 5, a znamo da je 26 = 2*13 Programski kod (C): /* Primjetimo da se navedeni problem svodi na nalazenje svih prostih faktora broja N, i na njihov ispis od najmanjeg ka najvecem */ #include <stdio.h> #include <stdlib.h> #include <math.h> /* n je broj koji zelimo faktorizirati faktori pretstavljaju niz koji popunjavamo prostim faktorima, funkcija se ne brine o tome da il je taj niz dovoljno velik kao rezultat funkcija vraca velicinu niza faktora */ int faktorizacija(int n, int *faktori) { 37 int i,korjen; int broj_faktora=0; while(!(n&1)) { n=n>>1; faktori[broj_faktora++] = 2; } korjen = (int) sqrt(n); for(i=3;i<=korjen;i+=2) { while(n%i==0) { n/=i; faktori[broj_faktora++] = i; } } if(n>0) faktori[broj_faktora++] = n; } return broj_faktora; int main() { int i,n; /* niz "faktori" je velicine 32 jer je autor logicki zakljucio da je to najveci moguci broj prostih faktora kako je n<=2^31 -1 i kako je 2 najmanji prosti faktor, 31 pretstavlja najveci moguci broj prostih faktora (i tome je dodata jedinica radi dodatne sigurnosti. Tokom takmicenja autor bi vjerovatno napravio ovaj niz da bude velicine npr. 50 */ int faktori[32]; } int br_faktora = faktorizacija(12,faktori); for(i=0;i<br_faktora;i++) printf("%d",faktori[i]); return 0; Zadatak 3.3. Igra Težina: S Ivica i Haso su iz dosade odlučili da naprave takmičenje iz matematike. To rade na sljedeći način: obojica dobiju po N brojeva ki ( 0<i≤N ) i njihov zadatak je da za svaki od brojeva k i odrede koji je ki-ti prosti broj po redu. Na kraju, provjeravaju svoje rezultate i pobjednik je onaj koji je tačno odredio najviše prostih brojeva. Vaš zadatak je da napišete program koji će odrediti pobjednika. 38 Ulaz: U prvom redu ulaza nalazi se cijeli broj N (N ≤ 10 000). Zatim u sljedećih N redova se nalaze brojevi k i (ki ≤ 2000) i mi za Ivicu, gdje je m i broj koji je odredio Ivica da je k i-ti prosti broj. Nakon toga slijedi još N redova u kojima se nalaze brojevi ki i mi, ali ovaj put za Hasu. Izlaz: U jedini red izlaza potrebno je ispisati pobjednika, ukoliko postoji: „Ivica“ ili „Haso“ ili „Nerjeseno“. Primjer ulaza: 3 20 71 40 173 500 3581 15 47 50 227 600 4327 Primjer izlaza: Ivica Pojašnjenje (Eratostenovo sito): Eratostenovo sito je jednostavan i star algoritam za nalaženje svih prostih brojeva do unaprijed date granice. To čini tako što iterativno označava kao složene brojeve umnoške prostih brojeva, počevši od broja 2. Višekratnici datog prostog broja se generišu počevši od tog prostog broja, kao aritmetički niz brojeva kojem je prvi element i razlika upravo taj prosti broj. Eratostenovo sito je jedan od najefikasnijih načina da se nađu svi manji prosti brojevi (ispod oko 10 miliona). Postupak dobivanja prostih brojeva pomoću Eratostenovog sita: - napišemo proizvoljan broj uzastopnih prirodnih brojeva počevši od 2 - zaokružimo najmanji neoznačeni broj - precrtamo sve njegove višekratnike, koji nisu već označeni - ponavljamo postupak od 2. koraka dok svi brojevi nisu označeni (zaokruženi ili precrtani) Postupak završi u konačno mnogo koraka, jer na početku imamo konačno mnogo brojeva, a u svakom koraku barem jedan broj označimo. Zaokruženi brojevi su prosti brojevi. Precrtani brojevi su složeni brojevi. Dobra grafička ilustracija algoritma se može naći na stranici: http://upload.wikimedia.org/wikipedia/commons/b/b9/Sieve_of_Eratosthenes_animation.gif Algoritam, implementiran u programskom jeziku C : for( i=2; i<=gornja_granica; i++) { if(!sito[i]) { prosti_brojevi[velicina++] = i; 39 } tmp = i+i; while(tmp<=gornja_granica) { sito[tmp] = 1; tmp+=i; } } Programski kod (C): #include <stdio.h> #include <stdlib.h> /* funkcija prima kao argumente gornju granicu do koje ispitujemo postojanje prostih brojeva te niz u koji cemo stavljati te proste brojeve kao rezultat vraca velicinu niza */ int Eratostenovo_sito (int gornja_granica, int prosti_brojevi[]) { int velicina=0; int i; int sito[18000]; memset(sito,0,sizeof sito); int tmp; for( i=2; i<=gornja_granica; i++) { if(!sito[i]) { prosti_brojevi[velicina++] = i; tmp = i+i; while(tmp<=gornja_granica) { sito[tmp] = 1; tmp+=i; } } } return velicina; } int main() { /* Ideja zadatka jeste da na pocetku jednom pokrenemo funkciju Eratostenovo sito i tako dobijemo niz prostih brojeva, tako da tokom citanja ulaza mozemo direktno ispitivati Nakon sto je implementirana funkcija Eratostenovo_sito, lagano se moze doci do zakljucka da je dovoljan niz od 18000 elemenata da bi sadrzao 2000 prostih brojeva */ int prosti_brojevi[2100]; 40 int N; int i, ivica=0, haso=0; int k, m; scanf("%d",&N); Eratostenovo_sito(18000,prosti_brojevi); velicini niza */ /* Nije nam potreban podatak o for(i=0;i<N;i++) { scanf("%d %d",&k,&m); if(prosti_brojevi[k-1]==m) ivica++; } for(i=0;i<N;i++) { scanf("%d %d",&k,&m); if(prosti_brojevi[k-1]==m) haso++; } if(ivica>haso) printf("Ivica"); else if(haso>ivica) printf("Haso"); else printf("Nerjeseno"); } return 0; Zadatak 3.4. Prosti blizanci Težina: S Prosti blizanci su parovi prostih brojeva koji su razdvojeni samo jednim parnim brojem npr. 11 i 13 su prosti blizanci. Napisati program koji prima cijeli broj N i određuje broj parova prostih blizanaca koji su manji od broja N. Ulaz: Cijeli broj N (N ≤ 2 000). Izlaz: Cijeli broj koji odgovara broju prostih blizanaca manjih od N. Primjer ulaza: 100 Primjer izlaza: 8 41 Pojašnjenje: Rješenje navedeno ispod generiše Erastotenovo sito (opisano u prethodnom zadatku), a zatim prolazi kroz sve tako generisane brojeve i nalazi blizance. Programski kod (C): #include <stdio.h> int Eratostenovo_sito (int gornja_granica, int prosti_brojevi[]) { int velicina=0; int i; int sito[18000]; memset(sito,0,sizeof sito); int tmp; } for( i=2; i<=gornja_granica; i++) { if(!sito[i]) { prosti_brojevi[velicina++] = i; tmp = i+i; while(tmp<=gornja_granica) { sito[tmp] = 1; tmp+=i; } } } return velicina; int main() { int prosti_brojevi[2100]; int N, prostih, blizanaca; scanf("%d",&N); prostih = Eratostenovo_sito(N,prosti_brojevi); blizanaca = 0; for(i=1; i<prostih; i++) { if (prosti_brojevi[i] – prosti_brojevi[i-1] == 2) blizanaca++; } printf("%d", blizanaca); } return 0; 42 4. Iscrpna pretraga (exhaustive search, brute-force metod) Nekada je jedino moguće rješenje određenog zadatka to da isprobamo sve dostupne varijante i nađemo najpovoljniju. Ovakvo rješenje je naravno primjenjivo samo tamo gdje je broj mogućih rješenja ograničen. U svakom slučaju vrijedi razmisliti o efikasnijem rješenju. Zadatak 4.1. Konjićev skok Težina: N Odrediti koliko različitih 7-cifrenih brojeva je moguće formirati tako što se za svaku sljedeću cifru može uzeti samo ona do čijeg se polja može doći pomijeranjem šahovske figure konjića od polja prethodne cifre, pri čemu je tabla organizovana kao standardna telefonska tastatura. Ulaz Početna cifra broja; cijeli broj od 1 do 9. Izlaz Traženi broj 7-cifrenih brojeva koji su formirani na opisani način. Primjeri: Ulaz 3 Izlaz 104 Programski kod sa pojašnjenjem: Napomena: u rješenju se koristi rekurzija koja će biti pojašnjena kasnije. Moguće je riješiti ovaj zadatak i bez korištenja rekurzije. /** * Zadatak ce biti rijesen generisanjem svih mogucih brojeva koji * zadovoljavaju date uslove i njihovim prebrojavanjem. */ #include <stdio.h> // Matrica koja opisuje tastaturu. char tastatura[][3] = { {'1', '2', '3'}, {'4', '5', '6'}, {'7', '8', '9'}, {'*', '0', '#'} }; // Matrica koja opisuje nacin kretanja konja na ploci. // Svaka vrsta predstavlja jedan do mogucih poteza, pri cemu 43 // broj u prvoj koloni predstavlja pomak figure po redovima, a // drugi pomak po kolonama u odnosu na trenutnu lokaciju. int potezi[][2] = { {-1, -2}, {-1, +2}, {-2, -1}, {-2, +1}, {+1, -2}, {+1, +2}, {+2, -1}, {+2, +1} }; // Broj elemenata u nizu potezi int const BR_POTEZA = 8; // Niz koji ce sadrzavati broj koji se gradi char broj[7]; int total = 0; /** * Funkcija prima koordinate (x, y) na kojoj se trenutno nalazi * figura i parcijalno izgradjen broj. * Depth predstavlja dubinu rekurzije, tj. cifru koju ce odrediti * taj poziv funkcije. */ void rekurzija(int x, int y, int cifra) { // Uslov za zaustavljanje rekurzije, tj. pretrazivanja. // Ukoliko je broj cifara jednak 7, pronadjen je novi // broj koji zadovoljava zadane uslove. if (cifra == 7) { ++total; return; } // Treba naci koja su to polja dostupna iz trenutne pozicije figure. for (int i = 0; i < BR_POTEZA; ++i) { // Red na koji se dolazi potezom i iz trentne pozicije int const destX = x + potezi[i][0]; // Kolona na koju se dolazi potezom i iz trenutne pozicije int const destY = y + potezi[i][1]; // Provjeriti da li je polje validno -- da li se nalazi unutar // granica ploce. if (destX < 0 || destX >= 4 || destY < 0 || destY >= 3) continue; // Polja sa znakovima * i # se ne uzimaju u obzir... if (destX == 3 && (destY == 0 || destY == 2)) continue; // Kada je utvrdjeno da je novo polje validno, cifra koja se // nalazi na njemu se dodaje u trenutni broj, te se funkcija // poziva rekurzivno sa novom pozicijom figure na ploci. broj[cifra] = tastatura[destX][destY]; rekurzija(destX, destY, cifra + 1); } } int main() { 44 // Pocetna cifra! int p_cifra; scanf("%d", &p_cifra); // Odrediti pocetni red i kolonu na osnovu unesenog broja. int red= p_cifra / 3; int kol = p_cifra % 3 - 1; // Prvu cifru dodajemo u broj broj[0] = tastatura[red][kol]; // Poziva se funkcija rekurzija s dubinom 1, jer je potrebno poceti // od druge cifre trazenog broja (prva je zadata). rekurzija(red, kol, 1); printf("%d\n", total); return 0; } 45 5. Binarna pretraga Binarna pretraga je metoda efikasnog pronalaska datog elementa u nizu. Za razliku od iscrpne pretrage u kojoj moramo uporediti svaki element u nizu sa traženom vrijednošću, što je definicija algoritamske kompleksnosti O(n), binarna pretraga ima kompleksnost O(log n). No, da bi se binarna pretraga mogla koristiti nužan preduslov je da je niz sortiran (složen po veličini). Binarna pretraga je učenicima poznatija pod imenom "metoda polovljenja intervala". Recimo da se pretražuje neki skup vrijednosti na intervalu [P,Q]. Uzima se vrijednost u sredini tog intervala (P+Q)/2 te se ona poredi sa traženom vrijednošću. Ako je tražena vrijednost veća od vrijednosti u sredini, novi interval je dat kao početak i sredina prethodnog intervala, odnosno [P, (P+Q/2]. Ako je tražena vrijednost manja, novi interval je sredina i kraj prethodnog intervala dakle [(P+Q)/2, Q]. Procedura se ponavlja sve dok se vrijednost ne pronađe ili dok dužina intervala ne postane 1 u kojem slučaju zaključujemo da se vrijednost ne nalazi u intervalu. Zadatak 5.1. Pretraga brojeva Težina: N Dat je niz brojeva duzine n sortiran u rastućem redoslijedu. Treba odrediti redni broj prvog pojavljivanja svakog od m sljedećih brojeva. Ukoliko se broj ne nalazi u nizu, ispisati poruku "Nije pronadjen". Ulaz Na prvoj liniji se nalazi prirodan broj n – dužina niza (maksimalno 100). Zatim slijedi n cijelih brojeva sortiranih u rastućem redoslijedu. U sljedećem redu je broj m (takođe maksimalno 100). Slijedi m cijelih brojeva za koje treba dati redni broj prvog pojavljivanja u nizu. Izlaz Za svaki od datih m cijelih brojeva, traženi redni broj u nizu ili poruku “Nije pronadjen” (bez navodnika) na posebnoj liniji. Primjer ulaza: 5 1 1 3 4 5 3 3 8 1 Primjer izlaza: 46 3 Nije pronadjen 1 Programski kod (C): /** * Kao ulaz je dat sortiran niz brojeva duzine n. * Zatim slijedi m brojeva za koje treba naci redni broj * prvog pojavljivanja u nizu. Ukoliko se broj ne nalazi * u nizu, ispisati poruku "Nije pronadjen". */ #include <stdio.h> /** * Binarna pretraga niza radi tako sto u svakom koraku * polovi interval u kojem se moze naci trazeni broj. * To se radi tako sto se trazeni broj uporedi sa * brojem u sredini intervala, te ukoliko je trazeni broj * veci pretraga nastavi u gornjoj polovini intervala, a ukoliko * je manji u donjoj polovini. * Ukoliko interval postane prazan, dati broj se ne nalazi u nizu. */ int binary_search(int niz[], int len, int broj) { int lower = 0; int upper = len - 1; // Interval niza za koji u svakoj interaciji petlje vrijedi // da se nalazi broj (ukoliko je u nizu) je [lower, upper] while (lower <= upper) { // Odredjujemo indeks elementa iz sredine intervala int i = (lower + upper) / 2; if (broj < niz[i]) { // Ako je trazeni broj manji od srednjeg elementa intervala // u sljedecoj iteraciji je interval ogranicen na donju polovinu upper = i - 1; } else if (broj > niz[i]) { // Ako je trazeni broj veci od srednjeg elementa intervala // u sljedecoj iteraciji je interval ogranicen na gornju polovinu lower = i + 1; } else if (lower != upper) { // Srednji element intervala je jednak trazenom broju, ali u intervalu // se nalazi vise od jednog elementa, pa se ne moze garantovati da je // pronadjeno prvo pojavljivanje trazenog elementa. Interval je ogranicen // u sljedecoj iteraciji na donju polovinu s ukljucenim srednjim elementom. upper = i; } else { // U intervalu se nalazi samo jedan element koji je jednak trazenom. return i; } } 47 // Interval je postao prazan u toku pretrage, sto znaci da dati element nije // pronadjen u nizu. return -1; } int main() { int n; scanf("%d", &n); int niz[10000]; for (int i = 0; i < n; ++i) { scanf("%d", &niz[i]); } int k; scanf("%d", &k); for (int i = 0; i < k; ++i) { int broj; scanf("%d", &broj); // Funkcija binary_search vraca index niza na kojem // se nalazi prvo pojavljivanje datog broja u datom nizu. // Vraca -1 ukoliko se broj ne pojavljuje u nizu. int index = binary_search(niz, n, broj); if (index == -1) { printf("Nije pronadjen\n"); } else { printf("%d\n", index + 1); } } return 0; } 48 6. Rekurzija Rekurzivna funkcija je funkcija koja poziva samu sebe. Najčešće u pitanju je neka funkcija f(x) koju možemo izraziti preko f(x-1). Jedan primjer rekurzivne funkcije je faktorijel. Faktorijel broja n (piše se n!) se definiše kao proizvod svih brojeva od 1 do n: n!=1⋅2⋅3⋅...⋅(n−1)⋅n Odnosno možemo pisati: n !=n⋅(n−1)! Drugim riječima, faktorijel broja n je n što množi faktorijel broja n-1. Ovu funkciju možemo opisati sljedećom C funkcijom: Slika 2: Rekurzija int faktorijel(int n) { return n * faktorijel(n-1); } Pascal: function faktorijel(n : integer) : integer; begin faktorijel := n * faktorijel(n-1); end; Ono što je ovdje zbunjujuće za većinu učenika je da se, kada funkcija pozove samu sebe, stanje izvršenja kao i vrijednosti svih promjenljivih sačuvaju u memoriji te se kreiraju nove instance svih tih varijabli, pri čemu nova varijabla n ima vrijednost staro n minus 1. Recimo da smo gore definisanu funkciju faktorijel pozvali sa brojem 4: faktorijel(4). Izvršenje programa teče ovako: • najprije se kreira promjenljiva n čija je vrijednost 4; • zatim izvršenje programa dođe do izraza n*faktorijel(n-1) koji se ne može izračunati dok se ne izračuna vrijednost faktorijel(n-1); • da bi se pozvala funkcija faktorijel(n-1) najprije treba izračunati koliko je n-1, a to je 3; • poziva se funkcija faktorijel sa parametrom 3; ovom prilikom se kompletno stanje izvršenja smješta u memoriju (n=4); • itd. proces se ponavlja za 2, 1, 0 itd. Ovdje uočavamo jednu od najvažnijih stvari u vezi rekurzije: svaka rekurzivna funkcija mora da terminira, odnosno mora se postaviti granica do koje će rekurzija ići, u suprotnom rekurzija će se nastaviti sa -1, -2 itd. te će se program krahirati pošto postoji maksimalan broj puta koliko funkcija može pozvati samu sebe. U matematici, faktorijel ima smisla samo za prirodne (pozitivne) brojeve, pa ćemo dodati da je faktorijel od 1 jednak 1, čime ćemo osigurati da se rekurzija završi na tom mjestu: C: int faktorijel(int n) { 49 if (n==1) return 1; else return n * faktorijel(n-1); } Pascal: function faktorijel(n : integer) : integer; begin if n=1 then faktorijel := 1 else faktorijel := n * faktorijel(n-1); end; Dakle, sada kompletan rekurzivni proces za poziv faktorijel(4) izgleda ovako: • n=4 • n nije jednako 1, izvršava se else • n*faktorijel(n-1) - moramo odrediti faktorijel(n-1) • n-1 = 3 • pozivamo faktorijel(3) ◦ n=3 ◦ n nije jednako 1, izvršava se else ◦ n*faktorijel(n-1) - moramo odrediti faktorijel(n-1) ◦ n-1 = 2 ◦ pozivamo faktorijel(2) ▪ n=2 ▪ n nije jednako 1, izvršava se else ▪ n*faktorijel(n-1) - moramo odrediti faktorijel(n-1) ▪ n-1 = 1 ▪ pozivamo faktorijel(1) • n=1 • pošto je n=1, vraća se vrijednost 1 ▪ n*1 = 2*1 = 2 ▪ vraća se vrijednost 2 ◦ n*2 = 3*2 = 6 ◦ vraća se vrijednost 6 • n*6 = 4*6 = 24 • vraća se vrijednost 24 4! = 24 što je tačan rezultat. Svaki rekurzivni zadatak može se riješiti i bez rekurzije. Međutim, rekurzija nam omogućuje prirodniji način rješavanja problema koji je često bliži matematičkoj definiciji. Ako imamo problem za n koji se može izraziti preko neke funkcije od n-1, onda se takav problem može riješiti rekurzivno. 50 Zadatak 6.1. Hanojske kule Težina: N Postoji legenda da se negdje u Indiji nalazi hram 1 i u njemu tri velika stuba oko kojih su ovješena 64 velika zlatna diska. Hinduski svećenici vrijedno premještaju diskove sa jednog stuba na drugi, ispunjavajući drevno proročanstvo da će smak svijeta nastupiti onda kada svi diskovi budu premješteni sa prvog stuba na drugi (koristeći treći kao pomoćni). Trik je u tome da je svaki disk različite veličine i da se veći disk nikada ne smije nalaziti na manjem (jer bi u tom slučaju propao). Ako pretpostavimo da svećeniku treba jedna sekunda da premjesti disk, kada će nastupiti smak svijeta? Ovaj broj je poznat i iznosi 264-1 sekundi. Generalno, za n diskova potrebno je 2n-1 poteza da bi se igra riješila. Napišite program koji ispisuje sve poteze potrebne za rješavanje igre za zadati broj n diskova. Ulaz Prirodan broj n: broj diskova. Izlaz U svakom redu opisan je jedan korak u igri. Tri stuba su označena slovima A, B i C, a diskovi su označeni brojevima 1, 2, ... n tako da je disk 1 najmanji a disk n najveći. Jedan potez je opisan brojem koji odgovara disku, nakon čega slijede dva slova koja odgovaraju stubu sa kojeg je disk premješten i stubu na koji je premješten. Npr. 8CB znači da je disk 8 prenesen sa stuba C na stub B. Primjer ulaza: 3 Primjer izlaza: 1AC 2AB 1CB 3AC 1BA 2BC 1AC Pojašnjenje: Najprije probajte riješiti ovu interesantnu igru na papiru i to sa nekim manjim brojem diskova, recimo 4. Ako razmišljate na ispravan način, trebali biste nadoći na opšti princip rješavanja zadatka za n diskova. Taj princip glasi ovako: Da bismo pomjerili n diskova sa stuba A na stub C, potrebno je pratiti sljedeće korake (pogledajte sliku 3): Slika 3: Opšti princip rješavanja problema Hanojske kule 1 U drugoj varijanti legende hram se nalazi u Hanoju, Vijetnam, pa je pod tim imenom zadatak poznatiji. 51 • korak 1: pomjerimo (n-1) diskova sa stuba A na pomoćni stub B, koristeći C kao pomoćni stub; • korak 2: pošto je sada na dnu stuba A ostao najveći disk, premjestimo ga direktno na stub C; • korak 3: sada pomjerimo (n-1) diskova sa stuba B na stub C koristeći stub A kao pomoćni. Ovo rješenje problema je rekurzivno, odnosno, rješenje problema za n smo opisali preko rješenja za n-1 koje smo iskoristili u koracima 1 i 3. Ovaj opis nam omogućuje da napišemo funkciju prebaci koja prebacuje n diskova sa stuba x na stub y koristeći stub z kao pomoćni. U slučaju da je broj diskova n=1, treba samo ispisati na ekranu slova x i y. Na ovaj način smo osigurali da se ova rekurzivna funkcija terminira. Programski jezik C: void prebaci(int n, char sa_stuba, char na_stub, char pomocni_stub) { /* Prebacujemo (n-1) diskova sa polaznog stuba na pomocni stub, koristeci odredisni stub kao pomocni. Ako je n=1 ovaj korak ne moramo raditi. */ if (n>1) prebaci(n-1, sa_stuba, pomocni_stub, na_stub); /* Najveci disk n je ostao na dnu, njega mozemo direktno prebaciti. */ printf(“%d%c%c\n”, n, sa_stuba, na_stub); /* Sada mozemo prebaciti (n-1) diskova sa pomocnog stuba na odredisni */ if (n>1) prebaci(n-1, pomocni_stub, na_stub, sa_stuba); } Pascal: procedure prebaci(n : integer, sa_stuba, na_stub, pomocni_stub : char); begin { Prebacujemo (n-1) diskova sa polaznog stuba na pomocni stub, koristeci odredisni stub kao pomocni. Ako je n=1 ovaj korak ne moramo raditi. } if n>1 then prebaci(n-1, sa_stuba, pomocni_stub, na_stub); { Najveci disk n je ostao na dnu, njega mozemo direktno prebaciti. } writeln(n, sa_stuba, na_stub); { Sada mozemo prebaciti (n-1) diskova sa pomocnog stuba na odredisni } if n>1 prebaci(n-1, pomocni_stub, na_stub, sa_stuba); end; Preostaje još da napišemo glavni program koji poziva ovu funkciju. Programski kod (C): #include <stdio.h> /* Funkcija prebaci obavlja sav bitan posao u ovom programu */ void prebaci(int n, char sa_stuba, char na_stub, char pomocni_stub) { /* Prebacujemo (n-1) diskova sa polaznog stuba na pomocni stub, koristeci odredisni stub kao pomocni. Ako je n=1 ovaj korak ne moramo raditi. */ if (n>1) prebaci(n-1, sa_stuba, pomocni_stub, na_stub); 52 /* Najveci disk n je ostao na dnu, njega mozemo direktno prebaciti. */ printf("%d%c%c\n", n, sa_stuba, na_stub); /* Sada mozemo prebaciti (n-1) diskova sa pomocnog stuba na odredisni */ if (n>1) prebaci(n-1, pomocni_stub, na_stub, sa_stuba); } int main() { int n; scanf("%d", &n); prebaci(n, 'A', 'C', 'B'); return 0; } Programski kod (Pascal): program Hanojske_kule; var broj_diskova : integer; { Procedura prebaci obavlja sav bitan posao u ovom programu } procedure prebaci(n : integer, sa_stuba, na_stub, pomocni_stub : char); begin { Prebacujemo (n-1) diskova sa polaznog stuba na pomocni stub, koristeci odredisni stub kao pomocni. Ako je n=1 ovaj korak ne moramo raditi. } if n>1 then prebaci(n-1, sa_stuba, pomocni_stub, na_stub); { Najveci disk n je ostao na dnu, njega mozemo direktno prebaciti. } writeln(n, sa_stuba, na_stub); end; { Sada mozemo prebaciti (n-1) diskova sa pomocnog stuba na odredisni } if n>1 prebaci(n-1, pomocni_stub, na_stub, sa_stuba); begin readln(broj_diskova); prebaci(broj_diskova, 'A', 'C', 'B'); end. Zadatak 6.2. Flood fill Težina: N Ovaj zadatak je bio na Kantonalnom takmičenju iz informatike 2010. godine Bitmapa je tabela (matrica) sastavljena od nula i jedinica, koja može predstavljati recimo neku crno-bijelu sliku. Napravite program koji učitava kvadratnu bitmapu (dimenzija NxN) i koordinate jedne tačke u toj bitmapi. Ako je na datim koordinatama vrijednost 1 program ne radi ništa. Ali ako se tu nalazi 0, program treba zamijeniti vrijednost jedinicom, a zatim zamijeniti i sve susjedne tačke sve dok se ne dođe do ruba matrice ili do znaka 1. Drugim riječima, program treba da se ponaša kao “flood fill” (punjenje) u grafičkim aplikacijama: treba da popuni dio matrice sastavljen od nula omeđen rubovima matrice i jedinicama. 53 Prilikom popunjavanja kroz se matricu kreće samo u smjerovima gore, dolje, lijevo i desno, ali ne i po dijagonali. Ulaz: Na ulazu se najprije nalazi broj N koji predstavlja dimenzije bitmape (ne veći od 100), a zatim odgovarajući broj nula i jedinica, te konačno dva broja u opsegu 1-N koji predstavljaju koordinate tačke od koje treba započeti popunjavanje. Koordinate (1,1) odgovaraju tački u gornjem lijevom uglu matrice. Izlaz: Na izlazu treba ispisati sadržaj matrice nakon popunjavanja, tako da su elementi matrice navedeni bez razmaka a na kraju reda matrice ispisuje se znak za novi red. Primjer ulaza: 10 0000000000 0000000000 0011111100 0010000100 0010000100 0010000100 0010000100 0011111100 0000000000 0000000000 5 5 Primjer izlaza: 0000000000 0000000000 0011111100 0011111100 0011111100 0011111100 0011111100 0011111100 0000000000 0000000000 Primjer ulaza: 10 0000100000 0001010000 0010001000 0100000100 1000000010 0100000100 0010001000 0001010000 0000100000 0000000000 5 5 54 Primjer izlaza: 0000100000 0001110000 0011111000 0111111100 1111111110 0111111100 0011111000 0001110000 0000100000 0000000000 Pojašnjenje: U računarskoj grafici postoje brojni algoritmi za flood fill, sa više ili manje efikasnosti. No u rješenju ovog zadatka nije se tražila posebna efikasnost, sve što se tražilo je korektno rješenje. Zato ovdje možemo primijeniti rekurzivni algoritam koji najočiglednije slijedi iz postavke problema. Rekurzivni algoritam rješavanja ovog problema je sljedeći: za koordinate x,y: • ako je koordinata x ili y izvan opsega matrice, ne radi ništa • u suprotnom, ako se na koordinatama (x,y) nalazi jedinica, ne radi ništa • u suprotnom: ◦ na koordinate (x,y) postavi jedinicu ◦ zamijeni vrijednosti na koordinatama: (x-1,y), (x+1,y), (x,y-1), (x,y+1) Pri prevođenju ovog pseudokoda u C ili Pascal trebamo voditi računa da indeksi niza veličine N u C-u su 0(N-1) a u Pascalu 1-N. Programski kod (C): #include <stdio.h> int matrica[100][100]; /* Moramo proslijediti velicinu matrice funkciji posto je ona ucitana u main-u */ void floodfill(int x, int y, int velicina) { /* Ako su koordinate izvan opsega matrice, ne radimo nista (prekid funkcije) */ if (x<0 || y<0 || x>=velicina || y>=velicina) return; /* Ako se na koordinatama nalazi jedinica, ne radimo nista */ if (matrica[x][y] == 1) return; /* Postavljamo 1 na koordinate */ matrica[x][y]=1; /* Rekurzivno pozivamo funkciju za tacke iznad, ispod, lijevo i desno */ floodfill(x-1,y,velicina); floodfill(x+1,y,velicina); floodfill(x,y-1,velicina); floodfill(x,y+1,velicina); } int main() 55 { int i,j,velicina,x,y; scanf("%d", &velicina); /* Ucitavamo vrijednosti u matrici */ for (i=0; i<velicina; i++) { for (j=0; j<velicina; j++) { scanf("%d", &matrica[i][j]); } } /* Ucitavamo polazne koordinate x i y */ scanf ("%d %d", &x, &y); /* Poziv funkcije */ floodfill(x,y,velicina); /* Ispis matrice */ for (i=0; i<velicina; i++) { for (j=0; j<velicina; j++) { printf("%d", matrica[i][j]); } /* Novi red na kraju reda matrice */ printf("\n"); } } return 0; 56 7. Sortiranje Sortiranje je preuređenje nekog niza tako da su elementi poredani po veličini, bilo u rastućem ili u opadajućem redoslijedu. Kod brojeva obično je intuitivno jasno šta predstavlja sortiranje. Kod tekstualnih podataka (stringova) obično se podaci sortiraju po tzv. leksikografskom odnosno abecednom poretku: AAA AAB AAC... ABA ABB... BAA... BAB... ZAA... ZZZ Pored toga vi možete sortirati podatke proizvoljnog tipa (geometrijska tijela, učenike, predmete) po bilo kojem kriteriju koji definišete (obim, površina, visina, ocjena). Postoji više algoritama za sortiranje koji se razlikuju prije svega po performansama u različitim situacijama. Npr. neki algoritmi imaju vrlo dobre performanse ako je niz već uglavnom sortiran (npr. za dodavanje novog elementa u sortiran niz), dok neki daju optimalne performanse kod niza elemenata čija je raspodjela u skupu mogućih vrijednosti slučajna. Međutim, algoritme sortiranja razlikujemo i po njihovoj kompleksnosti odnosno jednostavnosti razumijevanja. Na ovom nivou takmičenja odlučili smo se da vam prezentujemo osnovne algoritme tzv. kvadratične kompleksnosti, odnosno kompleksnosti O(n2), a to su: Bubble sort, Selection sort i Insertion sort). U nastavku imate programski kod ovih algoritama sa pojašnjenjima u formi komentara. Programski kod sa pojašnjenjem: /** * Implementacije bubble, selection i insertion sorta. */ #include <stdio.h> #define bool int #define true 1 #define false 0 /** * Bubble sort sortira niz tako sto u svakom prolazu kroz niz * uporedjuje susjedne elemente i mijenja im mjesto ukoliko nisu * u ispravnom odnosu. To znaci da ukoliko je potrebno sortirati * niz u rastucem redoslijedu elementi trebaju zamijeniti poziciju * ukoliko je onaj s manjim indeksom veci od svog sljedbenika. * Algoritam zavrsava s izvrsavanjem kada u prolazu kroz niz se ne * izvrsi ni jedna izmjena, sto znaci da su svi elementi u ispravnom * medjusobnom odnosu, tj. da je niz sortiran. 57 */ void bubble_sort(int niz[], int len) { // "Beskonacna" petlja, ne znamo unaprijed koji je // potrebni broj iteracija za sortiranje niza. for (;;) { // Varijabla koja prati da li je doslo do // neke izmjene u nizu u ovom prolazu. bool swapped = false; for (int i = 0; i < len - 1; ++i) { if (niz[i] > niz[i + 1]) { // swap int tmp = niz[i]; niz[i] = niz[i + 1]; niz[i + 1] = tmp; swapped = true; } } if (!swapped) break; } } /** * Selection sort sortira niz tako sto pronalazi najmanji element * u nizu i stavlja ga na prvu poziciju. Zatim, u ostatku niza * pronalazi najmanji element i stavlja ga na sljedecu poziciju. * Ova procedura se ponavlja sve dok se odgovarajuci element ne * stavi na zadnju poziciju niza. */ void selection_sort(int niz[], int len) { // i je pozicija u nizu na koju treba staviti sljedeci najmanji // element. for (int i = 0; i < len; ++i) { // Na kojem indeksu u nizu se nalazi trenutno najmanji // pronadjeni element. int min_index = i; // Provjeravaju se samo elementi iza indeksa i, obzirom da je // dio niza [0, i - 1] vec sortiran. for (int j = i + 1; j < len; ++j) { if (niz[j] < niz[min_index]) { min_index = j; } } // Kada je pronadjen najmanji element stavlja se na odgovarajucu // poziciju u nizu. int tmp = niz[i]; niz[i] = niz[min_index]; niz[min_index] = tmp; } } /** * Insertion sort radi tako sto u sortirani dio niza * dodaje element da odgovarajucu poziciju pri cemu se * svi elementi veci od njega pomijeraju prema desno. * Na pocetku samo prvi element pripada sortiranom dijelu * niza. 58 */ void insertion_sort(int niz[], int len) { for (int i = 1; i < len; ++i) { // Sljedeci element koji se dodaje u sortirani // dio niza [0, i - 1] je element na indeksu i. int novi = niz[i]; int j; for (j = i - 1; j >= 0; --j) { // Sve elemente vece od elementa koji se dodaje pomijeriti // prema desno. if (niz[j] > novi) { niz[j + 1] = niz[j]; } else { // Indeks na koji treba staviti novi element je // poslije elementa niz[j] obzirom da je niz[j] <= novi break; } } niz[j + 1] = novi; } } int main() { // Duzina niza int n; scanf("%d", &n); int niz[1000]; for (int i = 0; i < n; ++i) { scanf("%d", &niz[i]); } // Funkcijama za sortiranje je potrebno proslijediti i duzinu niza. // bubble_sort(niz, n); // selection_sort(niz, n); insertion_sort(niz, n); for (int i = 0; i < n; ++i) printf("%d ", niz[i]); printf("\n"); return 0; } Zadatak 7.1. Elementarna nepogoda Težina: S Tokom nedavnih vremenskih nepogoda izmjerena je visina snijega u gradovima BiH te je napravljen spisak oblika "Ime grada – visina snijega u cm". Potrebno je ispisati isti spisak u kojem su gradovi poredani po visini snijega, tako da se na prvom mjestu nalazi grad sa najvećom visinom snijega, zatim drugi po visini, treći itd. Ukoliko je u dva grada izmjerena ista visina snijega, potrebno je njihova imena ispisati abecednim odnosno leksikografskim poretkom. Ulaz Na prvoj liniji se nalazi prirodan broj n – broj gradova (maksimalno 100). U sljedećih n linija nalazi se ime grada (dužine maksimalno 20 znakova, bez razmaka), zatim razmak te cijeli 59 broj koji predstavlja visinu snijega u tom gradu. Izlaz Izlaz se sastoji od n linija istog oblika kao ulaz, dakle naziv grada, razmak te visina snijega, pri čemu je izlaz sortiran kako je opisano u zadatku. Primjer ulaza: 5 Sarajevo 150 Mostar 80 Tuzla 120 Zenica 110 Bihac 150 Primjer izlaza: Bihac 150 Sarajevo 150 Tuzla 120 Zenica 110 Mostar 80 Programski kod (C): #include <stdio.h> #include <string.h> struct snijeg { char grad[20]; int visina; }; int main() { int n,i,j,max_index; struct snijeg niz[100],tmp; /* Ulaz podataka */ scanf("%d", &n); for (i=0; i<n; i++) { printf ("Grad %d: ",i); scanf("%s %d", niz[i].grad, &niz[i].visina); } /* Primjenjujemo modifikovani selection sort u opadajucem redoslijedu */ for (i = 0; i < n; ++i) { /* Na kojem indeksu u nizu se nalazi trenutno najveci pronadjeni element. */ max_index = i; /* Provjeravaju se samo elementi iza indeksa i, obzirom da je dio niza [0, i - 1] vec sortiran. */ 60 for (j = i + 1; j < n; ++j) { /* Provjera visine snijega */ if (niz[j].visina > niz[max_index].visina) { max_index = j; } leksikografski */ } /* Ako je visina snijega ista, poredimo imena gradova if (niz[j].visina == niz[max_index].visina) { if (strcmp(niz[i].grad, niz[max_index].grad) < 0 ) { max_index = j; } } /* Kada je pronadjen najveci element stavlja se na odgovarajucu poziciju u nizu. */ tmp = niz[i]; niz[i] = niz[max_index]; niz[max_index] = tmp; } /* Ispis */ for (i=0; i<n; i++) printf("%s %d\n", niz[i].grad, niz[i].visina); } return 0; 61 8. Efikasno stepenovanje Zadatak 8.1. Efikasno stepenovanje Težina: N Odrediti zadnjih 6 cifara broja xn. Ulaz Jedna linija sa dva cijela broja, x i n, razdvojena jednim razmakom. 0<= x <= 2147483647 i 0 <= n <= 2147483647 Izlaz Zadnjih 6 cifara broja xn. Primjer: Ulaz 2345 2345 Izlaz 515625 Pojašnjenje: Najprije je potrebno primijetiti da su ograničenja na veličinu ulaznih podataka zadatka prevelika da bi se zadatak riješio naivnom metodom - računanje stepena pomoću funkcije pow, a zatim uzimanje zadnjih 6 cifara rezultata. Jedna metoda za efikasno stepenovanje brojeva je sljedeća. Neka treba naći x n. Najprije napišemo broj n kao sumu stepena dvojke n = k1 + k2 + ... + km Sada imamo da je: xn = x(k1 + k2 + ... + km) = xk1 * xk2 * ... * xkm Koristeći činjenicu da je svaki od elemenata k i stepen dvojke, xki možemo izračunati rekurzivnom formulom: xki = (x(ki / 2))2 Koristeći binarnu reprezentaciju eksponenta n, gornji proizvod možemo računati na sljedeći način. Idući po bitima eksponenta s desna na lijevo (od najmanje značajnog bita, do najznačajnijeg bita) ukoliko je dati bit 1, potrebno je da u konačni proizvod uđe faktor x k, gdje je k = 2i, a i je pozicija bita koji se razmatra (za najmanje značajan bit i = 0, itd...). Činjenica da se traži zadnjih 6 cifara znači da je potrebno naći rezultat x n po modulu 1000000. 62 Programski kod (C): /** * Odrediti zadnjih 6 cifara broja x^n. */ #include <stdio.h> int power(int x, int n) { long long sol = 1; // Varijabla b ce predstavljati faktor koji ulazi u proizvod. // Za najmanje znacajan bit to je x^(2^0) = x^1 = x long long b = x; while (n) { if (n & 1 == 1) { // Bit koji se razmatra je 1, pa treba konacno rjesenje // pomnoziti odgovarajucom vrijednosti x^k sol *= b; // Rezultat po po modulu 1000000 sol %= 1000000; } // Binary shift udesno eksponenta -- razmatra se sljedeci bit n >>= 1; // Za taj bit faktor koji bi trebao uci u proizvod (ukoliko je bit 1) // je x^(2*k) = x^k * x^k, s obzirom na gore navedenu rekurzivnu formulu. b *= b; b %= 1000000; } return sol; } int main() { int x; int n; scanf("%d %d", &x, &n); printf("%d\n", power(x, n)); return 0; } 63 9. Pohlepni (greedy) algoritmi Zadatak 9.1. Lopov Težina: N Lopov se našao u piljari gdje se nalazi više različitih gajbi voća i povrća. Svaka gajba ima poznatu težinu u kg (w), kao i vrijednost svakog kg namirnice iz te gajbe. Lopov ne može nositi više od M kilograma. Nije obavezno uzeti cijelu gajbu, već lopov može uzeti samo neki dio voća ili povrća iz nje. Odrediti koliko najviše lopov može zaraditi u datoj situaciji. Ulaz: cijeli broj n - broj gajbi 0 <= 20000 <= n n parova cijelih brojeva - ukupna težina i vrijednost kilograma svake od gajbi cijeli broj M - maksimalna težina koju lopov moze ponijeti Izlaz: Najveća zarada lopova pod datim uslovima. Primjer: Ulaz 5 15 4 21 11 65 36 15 Izlaz 72 Pojašnjenje: Može se primijetiti da će lopov najviše zaraditi ukoliko odabere gajbu koja ima najveću vrijednost po jedinici težine i uzme najveći mogući broj kilograma tog proizvoda. Ukoliko je moguće uzeti cijelu gajbu, onda prelazi na onu koja ima sljedeću najbolju vrijednost po jedinici težine. Ukoliko ne može uzeti cijelu gajbu, uzima najveći dio koji može nositi i računa se odgovarajuća vrijednost. Za svrhu implementacije je moguće prvo sortirati niz predmeta, te, zatim, redom uzimati elemente i dodavati na ukupnu vrijednost. Ovaj pristup rješavanju problema se zove pohlepni (greedy) algoritam iz razloga što se u svakom koraku bira ona vrijednost koja najviše poboljsava rješenje tj. izbor ne zavisi ni od jednog od prethodnih izbora. 64 Programski kod (C): #include <stdio.h> #define bool int #define true 1 #define false 0 typedef struct Predmet { int weight; int value; } Predmet; /** * Funkcija vraca true ukoliko je vrijednost prvog predmeta po jedinici tezine * veca od vrijednosti drugog predmeta po jedinici tezine. */ bool cmp(Predmet p1, Predmet p2) { return p1.value > p2.value; } void insertion_sort(Predmet niz[], int len) { for (int i = 1; i < len; ++i) { // Sljedeci element koji se dodaje u sortirani // dio niza [0, i - 1] je element na indeksu i. Predmet novi = niz[i]; int j; for (j = i - 1; j >= 0; --j) { // Sve elemente vece od elementa koji se dodaje pomjeriti // prema desno. if (!cmp(niz[j], novi)) { niz[j + 1] = niz[j]; } else { // Indeks na koji treba staviti novi element je // poslije elementa niz[j] obzirom da je niz[j] <= novi break; } } niz[j + 1] = novi; } } int main() { int n; scanf("%d", &n); Predmet predmeti[10000]; for (int i = 0; i < n; ++i) { scanf("%d %d", &predmeti[i].weight, &predmeti[i].value); } int weight; scanf("%d", &weight); // Sortira predmete po opadajucoj vrijednosti po jedinici tezine. insertion_sort(predmeti, n); 65 int value = 0; for (int i = 0; i < n && weight != 0; ++i) { if (weight >= predmeti[i].weight) { // Lopov moze ponijeti cijelu gajbu. weight -= predmeti[i].weight; value += predmeti[i].value * predmeti[i].weight; } else { // Lopov ne moze ponijeti cijelu gajbu, odredjuje se vrijednost // onog dijela koji je moguce ponijeti. value += predmeti[i].value * weight; weight = 0; } } printf("%d\n", value); return 0; } 66 10. Osnovna geometrijska tijela (pravougaonici, kružnice) Zadatak 10.1. Majansko prokletstvo Težina: S Naučnici su posmatrajući nebo otkrili veliku količinu meteora koji idu ka Zemlji i otkrili su da će pasti na jedan od najbitnijih gradova na Zemlji i to na datum 21.12.2012. godine. Maje su ipak bile upravu! Naučnici su otkrili tačne koordinate na koje će pasti meteori, a vaš je zadatak da odredite koliku će tačno štetu oni napraviti. Ulaz: Prvi red ulaza će sadržati brojeve m, n, k, r (m,n,k <1000, r<10000) – koji redom predstavljaju broj kuća oblika pravougaonika, kvadrata i kružnice (kvadratu i pravougaoniku su strane paralelne odabranom koordinatnom sistemu). Sljedećih m redova će sadržati po četri broja: prvi i drugi broj su x i y koordinate donje lijeve tačke pravougaonika, dok su treći i četvrti broj koordinate gornje desne tačke pravougaonika. Zatim slijedi n redova koji sadrže tri broja: prvi i drugi broj su x i y koordinate donje lijeve tačke kvadrata dok treći broj predstavlja dužinu njegovih strana. Nakon toga slijedi k redova sa po tri broja: prvi i drugi broj su x i y koordinate centra kruga dok treći broj predstavlja dužinu njegovog poluprečnika. Nakon toga slijedi r linija sa po dva broja – x i y koordinatom na koju će pasti pojedini meteor. Koordinate svih tačaka su cjelobrojnog tipa, dužine stranica i poluprečnika ne moraju biti. Izlaz: U jedini red izaza potrebno je ispisati koliko će tačno kuća oštetiti meteori. Primjer ulaza: 1115 0042 505 14 1 3 31 74 93 5 11 13 2 Primjer izlaza: 3 Pojašnjenje (Lociranje tačke u osnovnim geometrijskim oblicima: kružnica, pravougaonik, kvadrat): - Pravougaonik: Pravougaonici su u potpunosti određeni koordinatama dvije naspramne tačke. Na slici su to A(x A,yY) i B(xB,yB) . Iz tih koordinata možemo dobiti informacije o svim drugim osobonama pravougaonika, kao što su dužine stranica, koordinate druge dvije tačke, površina, obim itd. 67 Sa slike je jasno da je: xB = xC yB = yA xD = xA yD = yC a = x C - xA b = y C - yA (ove formule važe ukoliko su stranice pravougaonika paralelne koordinatnim osama, što je skoro uvijek i slučaj) Neka imamo tačku E(xE, yE). Da li ona pripada pravougaoniku ABCD određujemo iz uslova: x A ≤ xE ≤ xC i yA ≤ yE ≤ yC. U programskom jeziku C, to izgleda: if( E.x>= A.x && E.x<=C.x && E.y>=A.y && E.y <= C.y) { //tacka pripada pravougaoniku ABCD } else { //tacka ne pripada pravougaoniku } - Kvadrat Kao što znamo, kvadrat predstavlja specijalni slučaj pravougaonika te ga je moguće zadati na potpuno isti način. Drugi način na koji se zadaju kvadrati je pomoću koodinate jedne tačke (u našem slučaju to je tačka A) i dužine jedne stranice, iz čega možemo dobiti sve ostale podatke. Ovaj način zadavanja je jedino moguć ukoliko su stranice kvadrata paralelne koordinatnim osama (što je skoro uvijek i slučaj), inače bismo morali zadati još jedan podatak o kvadratu. xB = xA+ a yB = yA xD = xA yD = yA + a xC = xA + a yX = yX + a Ispitivanje pripadnosti tačke kvadratu se vrši analogno kao kod pravougaonika. 68 - Kružnica Kružnica je u potpunosti određen koordinatama centra i dužinom poluprečnika. Određivanje da li tačka pripada krugu vršimo ispitivanjem udaljenosti te tačke od centra kruga. Ukolika je ta udaljenost veća od poluprečnika, tačka ne pripada krugu. U suprotnom, pripada. Sa slike vidimo da je d1 > r te tačka A ne pripada krugu, dok je d2≤r i pripada. Udaljenost dvije tačke računamo po formuli: U programskom jeziku C, odgovarajući kod je: d = sqrt ( (A.x-S.x)* (A.x-S.x) + (A.y-S.y)* (A.y-S.y) ); if (d>r) { // ne pripada } else { //pripada } Programski kod (C): #include #include #include #include <stdio.h> <stdlib.h> <string.h> <math.h> struct tacka { int x; int y; }; struct pravougaonik { struct tacka donja_lijeva; struct tacka gornja_desna; }; int pripada_pravougaoniku(struct tacka t,struct pravougaonik p) 69 { return (t.x>=p.donja_lijeva.x && t.y>=p.donja_lijeva.y &&t.x<=p.gornja_desna.x && t.y<=p.gornja_desna.y); } struct kvadrat { struct tacka donja_lijeva; float a; }; int pripada_kvadratu(struct tacka t,struct kvadrat k) { return (t.x>=k.donja_lijeva.x && t.y t.x<=k.donja_lijeva.x+k.a && t.y <= k.donja_lijeva.y+k.a); } >=k.donja_lijeva.y && struct krug { struct tacka s; float r; }; float udaljenost(struct tacka A, struct tacka B) { return sqrt ( (A.x-B.x)* (A.x-B.x) + (A.y-B.y)* (A.y-B.y) ); } int pripada_krugu(struct tacka t, struct krug k) { float d = udaljenost(t,k.s); return !(d >k.r); } int main() { int m,n,k,r; struct pravougaonik pravougaonici[1000]; struct kvadrat kvadrati[1000]; struct krug krugovi[1000]; struct tacka tmp; unisteni_pravougaonici[1000],unisteni_kvadrati[1000],unisteni_krugovi[1000]; int /* nizovi za pracenje da li je objekat vec unisten - ne moze se dva puta unistiti*/ memset(unisteni_pravougaonici,0,sizeof unisteni_pravougaonici); memset(unisteni_kvadrati,0,sizeof unisteni_kvadrati); memset(unisteni_krugovi,0,sizeof unisteni_krugovi); int rj=0; int i,j; scanf("%d %d %d %d",&m,&n,&k,&r); 70 for(i=0;i<m;i++) { scanf("%d %d %d %d",&pravougaonici[i].donja_lijeva.x,&pravougaonici[i].donja_lijeva.y,&pravougaoni ci[i].gornja_desna.x,&pravougaonici[i].gornja_desna.y); } for(i=0;i<n;i++) { scanf("%d %d %f",&kvadrati[i].donja_lijeva.x,&kvadrati[i].donja_lijeva.y,&kvadrati[i].a); } for(i=0;i<k;i++) { scanf("%d %d %f",&krugovi[i].s.x,&krugovi[i].s.y,&krugovi[i].r); } for(i=0;i<r;i++) { scanf("%d %d",&tmp.x,&tmp.y); for(j=0;j<m;j++) { if(pripada_pravougaoniku(tmp,pravougaonici[j]) unisteni_pravougaonici[j]) { unisteni_pravougaonici[j]=1; rj++; } } for(j=0;j<n;j++) { if(pripada_kvadratu(tmp,kvadrati[j]) && !unisteni_kvadrati[j]) { unisteni_kvadrati[j]=1; rj++; } } } for(j=0;j<k;j++) { if(pripada_krugu(tmp,krugovi[j]) && !unisteni_krugovi[j]) { unisteni_krugovi[j] = 1; rj++; } } printf("%d",rj); return 0; } 71 && ! Zadatak 10.2. Obuhvatanje tačaka Težina: S Dat je niz tačaka u 2D prostoru zadatih preko koordinata x i y i broj R. Napisati program koji određuje koordinate centra kružnice sa radijusom R takve da pokriva što veći broj tačaka. Ulaz: Prvi red ulaza će sadržati cijeli broj R, zatim slijedi broj n tačaka u nizu (maksimalno 100), nakon čega slijedi niz od n parova cijelih brojeva koji predstavljaju x i y koordinate tačaka. Izlaz: Dva cijela broja koji predstavljaju koordinate centra kružnice sa radijusom R. Primjer ulaza: 3 5 45 76 33 52 44 Primjer izlaza: 54 Pojašnjenje: Ovdje ćemo primijeniti metod iscrpne pretrage (brute force) tako što ćemo definisati pravougaonik koji obuhvata sve tačke u nizu i zatim isprobati sve koordinate unutar tog pravougaonika kao potencijalni centar kružnice. Matematski se može dokazati da niti jedna kružnica sa centrom izvan tog pravougaonika ne može obuhvatati više tačaka od neke od kružnica sa centrom unutar pravougaonika. Slika 4: Pojašnjenje primjera ulaza i izlaza 72 U primjeru ulaza tražen je krug sa radijusom 3 koji obuhvata datih 5 tačaka: (4,5), (7,6), (3,3), (5,2), (4,4). Kao što vidimo sa slike 4 tačno rješenje je (5,4) što je program i ispisao. Programski kod (C): #include <stdio.h> #include <math.h> struct tacka { int x; int y; }; struct krug { struct tacka s; float r; }; float udaljenost(struct tacka A, struct tacka B) { return sqrt ( (A.x-B.x)* (A.x-B.x) + (A.y-B.y)* (A.y-B.y) ); } int pripada_krugu(struct tacka t, struct krug k) { float d = udaljenost(t,k.s); return !(d >k.r); } int main() { int r,n,i,x,y,obuhvaceno,max_obuhvaceno; struct tacka niz[100],dolje_lijevo,gore_desno,centar; struct krug k; scanf("%d", &r); scanf("%d", &n); for (i=0; i<n; i++) scanf("%d %d", &niz[i].x, &niz[i].y); /* Najprije odredjujemo pravougaonik koji obuhvata sve tacke u nizu, preko tacaka dolje_lijevo i gore_desno */ dolje_lijevo=gore_desno=niz[0]; for (i=1; i<n; i++) { if (dolje_lijevo.x > niz[i].x) dolje_lijevo.x = niz[i].x; if (dolje_lijevo.y > niz[i].y) dolje_lijevo.y = niz[i].y; if (gore_desno.x < niz[i].x) gore_desno.x = niz[i].x; if (gore_desno.y < niz[i].y) gore_desno.y = niz[i].y; } /* Sada prolazimo kroz sve cjelobrojne vrijednosti u tom pravougaoniku */ max_obuhvaceno=0; for (x=dolje_lijevo.x; x<=gore_desno.x; x++) { 73 for (y=dolje_lijevo.y; y<=gore_desno.y; y++) { /* Provjeravamo koliko tacaka je obuhvaceno kruznicom sa centrom u x,y */ obuhvaceno=0; k.s.x=x; k.s.y=y; k.r=r; for (i=0; i<n; i++) { if (pripada_krugu(niz[i], k)) obuhvaceno++; } /* Da li je taj broj veci od do sada poznatog maksimuma? */ } if (obuhvaceno>max_obuhvaceno) { max_obuhvaceno = obuhvaceno; /* Postavljamo trazeni rezultat na x,y */ centar.x = x; centar.y = y; } } } /* Ispis rezultata */ printf ("%d %d\n", centar.x, centar.y); return 0; 74 11. Rad sa stringovima Zadatak 11.1. Cenzura Težina: S Na školskom forumu postoji spisak cenzurisanih riječi koje se ne smiju pojaviti u porukama koje pišu učenici. Program treba potražiti svaku od riječi na spisku u poruci i, ako se riječ javlja u tekstu poruke, zamijeniti je odgovarajućim brojem zvjezdica. Ulaz: Na ulazu se najprije nalazi jedan cijeli broj n (maksimalno 100) koji predstavlja broj riječi koje se cenzurišu. Zatim slijede cenzurisane riječi u zasebnim redovima (dužina riječi je maksimalno 20 znakova, riječi ne sadrže razmake). Na kraju se nalazi tekst koji treba cenzurisati (maksimalne dužine 1000 znakova, može sadržavati razmake). Tekst se završava znakom za novi red. Izlaz: Dati tekst pri čemu je svaka od cenzurisanih riječi zamijenjena odgovarajućim brojem zvjezdica. Primjer ulaza: 3 cenzura nema direktor U ovom tekstu nema cenzure. Mi ni ne znamo sta je cenzura. Ako ne vjerujete, pitajte direktora. Primjer izlaza: U ovom tekstu **** cenzure. Mi ni ne znamo sta je ******. Ako ne vjerujete, pitajte ********a. Zadatak 11.2. Pravilan jezik Težina: S-N U dalekoj zemlji Prdimahovini govori se prdimahovinski jezik koji ima vrlo jednostavna leksička pravila: - svaka riječ sastoji se od jednog ili više slogova - svaki slog sastoji se od jednog suglasnika i jednog samoglasnika - izuzetno, riječ može počinjati samoglasnikom nakon kojeg slijede regularni slogovi - takođe postoje veznici a i o u. Na osnovu ovih pravila napisati primitivan spellchecker za prdimahovinski jezik. Spellchecker treba ispisati na izlazu sve nepravilne riječi i to onim redoslijedom kojim se javljaju u ulaznom tekstu. Ulaz: Tekst koji treba provjeriti. Tekst je maksimalne dužine 1000 znakova i može sadržavati razmake, takođe riječi u tekstu mogu uključivati i velika i mala slova. Tekst se završava znakom za novi red. Sve znakove u tekstu koji nisu slova treba posmatrati kao graničnike između riječi: npr. Ababa4pipi je ispravan tekst jer sadrži 75 riječi ababa i pipi razdvojene cifrom 4 koja nije slovo pa je prema tome graničnik izmeđju riječi. Izlaz: Spisak nepravilnih riječi u tekstu, pri čemu je svaka riječ ispisana u zasebnom redu. Primjer ulaza: Abababa cecece i dodod bababa. Ba abab cecece dodo. Gugugu-bebe dododo. Aaba dede: agge dudu bibibi. Primjer izlaza: dodod abab Aaba agge Programski kod (C): #include <stdio.h> #include <stdlib.h> #include <string.h> /* Standardna funkcija za unos stringa. vel = velicina stringa*/ void unesi(char* string, int vel) { int i=0; do { *string++ = getchar(); i++; if (i==vel) break; } while (*(string-1) != '\n'); *(string-1) = '\0'; } char ulaz[1001]; /* sluzi za ispisivanje ulaznog stringa od indeksa i do indeksa j */ void ispisi(int i,int j) { while(i<j-1) { printf("%c",ulaz[i]); i++; } printf("\n"); } /* ispituje da li je znak c slovo */ int slovo(char c) 76 { return ((c>='a' && c<='z') || (c>='A' && c<='Z')); } /* ispituje da li je znak c samoglasnik */ int samoglasnik(char c) { return (c=='a' || c=='A' || c=='e' || c=='E' || c=='i' || c=='I' || c=='o' || c=='O' || c=='u' || c=='U'); } /* ispituje da li je znak c veznik definisan u zadatku */ int veznik(char c) { return (samoglasnik(c) && c!='e' && c!='E'); } /* funkcija analizira rijec, ispisuje je ukoliko nije pravilna, vraca kao rezultat indeks od kojeg pocinje sljedeca rijec */ int analiziraj_rijec(int i) { int j=i,k=i; /* petlja koja odredjuje do kojeg indeksa ulaza je trenutna rijec */ while(slovo(ulaz[j++])); /* ukoliko je rijec duga jedan znak, ispituje da li je taj znak veznik */ if(!slovo(ulaz[i+1])) { if(!veznik(ulaz[i])) { printf("%c\n",ulaz[i]); return j; } } /* ukoliko je prvi znak samoglasnik, preskacemo ga */ if(samoglasnik(ulaz[i])) { k++; } /* preostali broj znakova mora biti paran da bi rijec bila korektna */ if((j-k-1)%2!=0) { ispisi(i,j); 77 return j; } /* ispituje da li ostatak rijeci ima pravilnu strukturu */ for(;k<j-1;k+=2) { if(samoglasnik(ulaz[k]) || !samoglasnik(ulaz[k+1])) { ispisi(i,j); return j; } } return j; } int main() { int i=0; int len_ulaz; unesi(ulaz,1000); len_ulaz = strlen(ulaz); i=0; while(i<len_ulaz) { if(slovo(ulaz[i])) i = analiziraj_rijec(i); else i++; } return 0; } Zadatak 11.3. Prijemni ispit Težina: S Nakon prijemnog ispita na nekom fakultetu napravljena su dva spiska: kandidati koji su primljeni na fakultet i kandidati koji nisu primljeni. Ali neposredno prije objavljivanja spiska otkriveno je da je napravljena greška! Naime neki kandidati su se javili na oba spiska. Napišite program koji će pomoći osoblju fakulteta da popravi ovu grešku tako što će ispisati imena svih takvih kandidata kako bi se provjerilo da li su isti primljeni ili ne. Redoslijed ispisanih imena treba biti isti kao u prvom spisku. Ulaz: Cijeli broj koji označava broj primljenih kandidata. Spisak primljenih kandidata u obliku "Ime Prezime", pri čemu je svaki kandidat u zasebnom redu. Cijeli broj koji označava broj odbijenih kandidata. Spisak odbijenih kandidata u obliku "Ime Prezime", pri čemu je svaki kandidat u zasebnom redu. U riječima Ime i Prezime ne mogu se javiti razmaci. Maksimalna dužina imena i prezimena je po 20 78 znakova. Izlaz: Imena i prezimena kandidata koji se nalaze na oba spiska u obliku "Ime Prezime", pri čemu je svaki kandidat u zasebnom redu. Zadatak 11.4. Spellchecker Težina: S-N Ovaj zadatak je bio na Kantonalnom takmičenju iz informatike 2007. godine Vjerovatno ste upoznati sa tzv. spell checker programima. Svaki od tih programa sadrži fajl sa određenim brojem riječi koje zajedno čine rječnik. Služi se tim skupom riječi, program analizira dati tekst i u njemu pronalazi riječi koje se ne nalaze u njegovom rječniku. Na taj način moguće je pronaći riječi koje nisu pravilno spelovane. Vaš zadatak je da napravite program koji će: a) koristeći dati rječnik analizirati dati tekst u određenom jeziku i nepoznate riječi ispisati na ekran. Nakon toga treba ispisati i broj nepoznatih riječi. b) za nepoznate riječi pokušati utvrditi ispravnu riječ i ponuditi korisniku ispravku teksta zamjenom pogrešno napisane riječi sa odabranom riječi. Lista mogućih ispravnih riječi je isti korijen riječi u odnosu na nepoznatu riječ. Korijen riječi je dužina riječi minus jedan znak. Ulaz: Na ulazu se najprije nalazi cijeli broj N (maksimalno 1000) koji predstavlja broj riječi u rječniku. Zatim su date riječi, po jedna u svakom redu (unutar riječi se ne nalazi razmak). Nakon toga je dat tekst koji treba prekontrolisati u formi niza riječi zaključno sa novim redom (tekst može sadržavati znak razmak). Izlaz: Na izlazu treba najprije ispisati broj riječi koje nisu prepoznate u rječniku, a zatim u svakom redu ispisati nepoznatu riječ, znak razmak, a zatim eventualnu zamjensku riječ koja je pronađena u rječniku (ili ako nije pronađena riječ ne treba ispisati ništa). Ako u rječniku postoji više riječi koje zadovoljavaju kriterij (sva slova jednaka osim zadnjeg), treba kao zamjensku ponuditi onu koja je prva navedena u rječniku. 79 12. Veliki broj Zadaci na informatičkoj olimpijadi često zahtijevaju rad sa brojevima toliko velikim da se oni ne mogu pohraniti u tipove podataka koje podržavaju programski jezici C, C++ i Pascal (barem ne precizno). Stoga se često zahtijeva da razvijete posebnu programsku klasu ili biblioteku za rad sa ovakvim brojevima, pri čemu je broj predstavljen nizom (cifara). Matematičke operacije nad ovakvim brojevima su takođe posebna problematika. Zadatak 12.1. Zbir i proizvod dva velika broja Težina: N Ovaj zadatak je bio na Kantonalnom takmičenju iz informatike 2010. godine Na ulazu se nalaze se dva cijela broja dužine maksimalno 100 cifara. Drugim riječima, ovi brojevi mogu biti dosta veći od najvećeg broja koji se može držati u cjelobrojnom tipu podataka nekog programskog jezika. Svaki broj se nalazi u zasebnom redu. Napisati program koji izračunava i ispisuje sumu i proizvod ova dva broja. Suma i proizvod također trebaju biti u zasebnim redovima. Ulaz: Dvije linije koje predstavljaju dva proizvoljno velika cijela broja. Izlaz: Dvije linije koje predstavljaju sumu i proizvod ova dva broja. Primjer ulaza: 10000000000000000000 20000000000000000000 Primjer izlaza: 30000000000000000000 200000000000000000000000000000000000000 Programski kod (C): #include <stdio.h> /* Sljedece funkcije rade tacno samo za pozitivne brojeve! */ /* Pomocna funkcija za ispis velikog broja na ekran */ void ispisiNiz(int niz[], int vel) { int i; for (i=0; i<vel; i++) printf("%d", niz[i]); printf("\n"); } /* Funkcija prima dva niza br1 i br2, njihove velicine velbr1 i velbr2, te treci niz rez u koji treba upisati rezultat. Vraca broj elemenata u nizu rez. 80 Pri tome br1 uvijek mora biti veci od br2! */ int sumaBrojeva(int br1[], int velbr1, int br2[], int velbr2, int rez[]) { /* Racunanje zbira */ int velrez = velbr1+1; int razlika_velicina = velbr1 - velbr2; int prenos = 0; int i; for (i = velbr1-1; i>=0; i--) { rez[i+1] = br1[i] + prenos; if (i>=razlika_velicina) rez[i+1] += br2[i-razlika_velicina]; prenos = rez[i+1]/10; rez[i+1] = rez[i+1]%10; } if (prenos==1) rezultat[0]=1; else { /* Brisemo vodecu nulu iz niza (iako ne smeta) */ for (i=0; i<velbr1; i++) rez[i]=rez[i+1]; velrez--; } return velrez; } /* Funkcija prima niz, velicinu niza vel i neki broj, racuna proizvod niza brojem i upisuje u niz rez. Funkcija vraca velicinu niza rez. */ int proizvodNizaBrojem(int niz[], int vel, int broj, int rez[]) { int velrez = vel+1; int prenos = 0; int i; for (i=vel; i>0; rez[i] = prenos = rez[i] = } } i--) { v[i-1]*broj + prenos; rez[i]/10; rez[i]%10; if (prenos>0) rez[0]=prenos; else { /* Brisemo vodecu nulu iz niza (iako ne smeta) */ for (i=0; i<vel; i++) rez[i]=rez[i+1]; velrez--; } return velrez; /* Funkcija prima dva niza br1 i br2, njihove velicine velbr1 i velbr2, te treci niz rez u koji treba upisati rezultat. Vraca broj elemenata u nizu rez. */ int proizvodBrojeva(int br1[], int velbr1, int br2[], int velbr2, int rez[]) { int medjurezultati[velbr2][velbr1+velbr2]; 81 int i,j; /* Izracunavamo medjurezultate */ for (i=0; i<velbr2; i++) { /* Mnozimo br1 i-tim clanom niza br2 i smjestamo u niz medjurezultata */ int duzina = proizvodVektoraBrojem(br1, velbr1, br2[i], medjurezultati[i]); /* Pomjeramo medjurezultat za i mjesta udesno */ for (j=duzina-1; j>=0; j--) medjurezultati[i][j+i] = medjurezultati[i][j]; /* Upraznjeni prostor na pocetku popunjavamo nulama */ for (j=0; j<i; j++) medjurezultat[i][j] = 0; } /* Nuliramo niz rez */ for (i=0; i<velbr2; i++) rez[i] = 0; /* Sumiramo medjurezultate u niz rez */ int novaduzina = velbr2; for (int i=0; i<velbr2; i++) { /* Sabiramo i-ti medjurezultat sa nizom rez i smjestamo u tmpniz */ int tmpniz[novaduzina+1]; novaduzina = sumaBrojeva(rez, novaduzina, medjurezultati[i], velbr2, tmpniz); /* Vracamo iz niza tmpniz u niz rez */ for (int j=0; j<novaduzina; j++) rez[j] = tmpniz[j]; } } return novaduzina; int main() { /* Nizovi cifara */ int br1[100], br2[100], suma[200], proizvod[200]; /* Brojevi cifara u nizovima */ int velbr1, velbr2, velsuma, velproizvod; int i; /* Brojeve moramo citati znak po znak */ char znak; /* Petlja za citanje prvog broja */ velbr1=0; znak = getchar(); while (znak != '\n') { br1[velbr1] = znak - '0'; velbr1++; 82 } znak = getchar(); /* Petlja za citanje drugog broja */ velbr2=0; znak = getchar(); while (znak != '\n') { br2[velbr2] = znak - '0'; velbr2++; znak = getchar(); } /* Funkcije za sumu i prozivod */ velsuma = sumaBrojeva(br1, velbr1, br2, velbr2, suma); velproizvod = proizvodBrojeva(br1, velbr1, br2, velbr2, proizvod); /* Ispis sume i proizvoda */ ispisiNiz(suma, velsuma); ispisiNiz(proizvod, velproizvod); } return 0; 83 13. Grafovi i stabla Zadatak 13.1. Presjedanje Težina: N Junak ove priče N.N. često mora da putuje. Kao najudobniji vid transporta koristi isključivo avionski. Obzirom da je upoznat sa podatkom da se većina nesreća dešava prilikom uzlijetanja i slijetanja on svoju rutu uvijek bira isključivo prema broju presjedanja koja mora da napravi. Njemu nije bitno koliko dugo će putovati, spreman je duže biti u avionu da bi izbjegao dodatna slijetanja i uzlijetanja. Ni cijena nije bitna, troškove u svakom slučaju snosi firma. Obzirom da sve putničke agencije imaju softver koji nalazi najkraće ili najjefitnije rute, N.N. je prisiljen da uvijek svoju rutu bira ručno. Pomozite mu i napišite program koji će mu pomoći u izboru rute. Ulazni podaci U ulaznoj datoteci “presjedanje.in” na prvoj liniji nalaze se dva cijela broja N (0 < N ≤ 10.000) i M (0 < M ≤ 100.000). Broj N je ukupan broj gradova, dok je broj M ukupan broj letova koji postoje između ovih gradova. Gradovi su numerisani brojevima od 0 do N-1. U svakoj od narednih M linija nalaze se po dva cijela broja ai i bi (0 < ai, bi < N) međusobno razdvojena razmakom. Značenje je da postoje letovi između gradova ai i bi i to u oba smjera. Na posljednjoj liniji ulazne datoteke nalaze se dva cijela broja x i y (0 < x, y < N) međusobno razdvojena razmakom. N.N. želi da putuje iz grada x u grad y. Izlazni podaci U izlaznu dateteku “presjedanje.out” trebate na prvoj i jedinoj liniji trebate ispisati jedan cio broj koji predstavlja dužinu rute sa najmanjim brojem presjedanja između od grada x do grada y. Ukoliko ne postoji tražena ruta, ispišite tekst “Trazena ruta ne postoji.”. Primjer 1 presjedanje.in 5 0 1 2 3 0 1 0 6 1 2 3 4 2 3 4 presjedanje.out 3 Primjer 2 presjedanje.in 4 0 2 0 2 1 3 2 presjedanje.out Trazena ruta ne postoji. 84 Rješenje Najprirodniji način rješavanja ovog i sličnih problema je primjenom grafova, odnosno algoritama za grafove. Neformalno, pod grafom smatramo geometrijsku strukturu koja se sastoji od skupine objekata koji se nazivaju čvorovi grafa i koji su međusobno povezani vezama koje se nazivaju grane grafa. U ovom zadatku čvorovi grafa bi bili gradovi, a grane grafa bi bili letovi između gradova. Postoje usmjereni i neusmjereni grafovi. Kod usmjerenih grafova moguće je da postoji grana od čvora a do čvora b, a da istovremeno ne postoji grana od čvora b do čvora a. Postoje grafovi kod kojih se uz svaku granu veže i određeni broj koji se naziva težina te grane. Obzirom da nama nije od interesa dužina niti cijena određenih letova koji bi se mogli modelirati kao težine određenih grana, mi ćemo koristiti tzv. netežinski graf. Jedan od načina predstavljanja grafova u memoriji je lista susjedstva. Najprije sve čvorove grafa numerišemo brojevima od 0 do N-1, gdje je N ukupan broj čvorova u grafu. U ovom zadatku ovakva numeracije je osigurana samom postavkom. Sada se uz svaki čvor x veže niz cijelih brojeva između 0 i N-1. Svaki element tog niza yi označava da postoji grana između čvora x i čvora y i. Na primjer, sljedeći skup globalnih varijabli u potpunosti opisuje graf koji ćemo koristiti u rješenju ovog zadatka. const int max_n = 10000; int lista_susjedstva[max_n][max_n]; int lista_susjedstva_duzina[max_n] = {0}; int n; max_n je maksimalni mogući broj čvorova (dato postavkom zadatka). Koristimo nizove i matrice čije su dimenzije unaprijed određene kako bismo izbjegli dinamičku alokaciju memorije. n je broj čvorova koji stvarno postoje u grafu (n ≤ max_n). lista_susjedstva je matrica dimnezija max_n×max_n od koje se ustvari koristi najviše n×n elemenata. Svaki od nizova lista_susjedstva[i] (0 ≤ i < n) predstavlja niz čvorova koji su povezani sa čvorom i. Dužina ovih nizova upisana je u nizu lista_susjedstva_duzina, tako da je lista_susjedstva_duzina[i] dužina niza lista_susjedstva[i]. U najgorem slučaju jedan čvor može imati granu do svih ostalih čvorovima i do samog sobe (iako grana do samog sebe u ovom zadatku nema nekog smisla). Prema tome, nijedan od ovih nizova ne može biti duži od n elemenata. Iz ovoga je jasno zašto su maksimalne dimenzije matrice lista_susjedstva n×n odnosno max_n×max_n. Dodavanje grane u graf je jednostavna operacija i sastoji se od dodavanja elemenata u listu susjedstva. Obzirom da je postavka zadatka takva da nameće korištenje neusmjerenog grafa (letovi su uvijek u oba smjera), to je prilikom dodavanja grane između čvorova a i b neophodno dati i granu između čvorova b i a. void dodaj_granu(int a, int b) { lista_susjedstva[a][lista_susjedstva_duzina[a]] = b; ++lista_susjedstva_duzina[a]; lista_susjedstva[b][lista_susjedstva_duzina[b]] = a; ++lista_susjedstva_duzina[b]; } Među osnovne algoritme teorije grafova spadaju algoritmi kretanja kroz graf. Postoje dva bazna algoritma za kretanje kroz graf: pretraga grafa po širini (breadth-first search, BFS) i pretraga grafa po dubini (depthfirst search, DFS). Kod BFS pretrage, krećemo od proizvoljnog čvora grafa, koji označimo sa 0. Sve njegove susjede označimo sa 1. U sljedećoj iteraciji, krećemo od čvorova koji su označeni sa 1, njihove susjede koji nisu bili ranije označeni označimo sa 2. Ovaj postupak nastavljamo dalje tako da u i-toj iteraciji sve susjede čvorova označenih sa i koji nisu bili ranije označeni označimo sa i+1. Postupak se prekida kada dalje obilježavanje više nije moguće. Primijetimo da oznake dodijeljene pojedinim čvorovima ujedno 85 predstavljaju najkraće udaljenosti pojedinih čvorova od startnog čvora, tako da je ovaj postupak pogodan u slučaju da treba naći najkraći put od početnog čvora do nekog zadanog čvora (za netežinski graf). Slijedi jedna implementacija BFS pretrage u programskom jeziku C++. Napomenimo da postoje značajno efikasnije implementacije ali one uključuju upotrebu strukture podataka koja se naziva red (engl. queue). int najkraca_ruta(int pocetak, int kraj) { //Niz u kojem su upisane oznake čvorova za BFS pretragu int oznaka[max_n]; //Inicijalno svi čvorovi imaju oznaku -1, tj. nisu označeni for (int i = 0; i < n; ++i) oznaka[i] = -1; //Čvor od kojeg se počinje BFS pretraga ima oznaku 0 oznaka[pocetak] = 0; //Varijabla za oznaku koja se trenutno obrađuje int trenutna_oznaka = 0; /*Varijabla koja signalizira da li je posjećen barem jedan novi čvor u *prethodnoj iteraciji. Inicijalno je postavljena na true da bi se osigurao *prvi ulazak u while petlju*/ bool posjecen_barem_jedan_cvor = true; while (posjecen_barem_jedan_cvor) { posjecen_barem_jedan_cvor = false; /*Nalazimo čvorove čija je trenutna oznaka ona koja se obrađuje u *ovoj iteraciji*/ for (int trenutni_cvor = 0; trenutni_cvor < n; ++trenutni_cvor) if (oznaka[trenutni_cvor] == trenutna_oznaka) /*Sve prethodno neposjećene čvorove iz liste susjedstva ovog *čvora označavamo sa sljedećom oznakom*/ for (int j = 0; j < lista_susjedstva_duzina[trenutni_cvor]; ++j) { int susjedni_cvor = lista_susjedstva[trenutni_cvor][j]; if (oznaka[susjedni_cvor] == -1) { oznaka[susjedni_cvor] = trenutna_oznaka + 1; posjecen_barem_jedan_cvor = true; } } //U narednoj iteraciji treba obraditi sljedeću oznaku ++trenutna_oznaka; } /*Kako je kod netežinskih grafova oznaka u BFS pretrazi ustvari najkrača *udaljenost od početnog čvora vračamo oznaku čvora kraj. Ako je njegova *oznaka -1, onda čvor nije bio nikako posjećen, pa ne postoji ruta od *čvora pocetak do čvora kraj*/ return oznaka[kraj]; } Implementacija ostatka programa koja slijedi sastoji se od učitavanja podataka iz ulazne datoteke u specificiranom formatu, poziva napisane BFS pretrage i ispisa dobijenih rezultata u nevedenom formatu. int main() { FILE* ulazna_datoteka = fopen("presjedanje.in", "r"); FILE* izlazna_datoteka = fopen("presjedanje.out", "w"); 86 } //Učitavanje podataka iz ulazne datoteke int m; fscanf(ulazna_datoteka, "%d %d", &n, &m); for (int i = 0; i < m; ++i) { int a, b; fscanf(ulazna_datoteka, "%d %d", &a, &b); dodaj_granu(a, b); } int x, y; fscanf(ulazna_datoteka, "%d %d", &x, &y); //Najkrača ruta se određuje primjenom BFS pretrage int udaljenost = najkraca_ruta(x, y); //Ispis rezultata u izlaznu datoteku if (udaljenost == -1) fprintf(izlazna_datoteka, "Trazena ruta ne postoji.\n"); else fprintf(izlazna_datoteka, "%d\n", udaljenost); fclose(ulazna_datoteka); fclose(izlazna_datoteka); return 0; Zadatak 13.2. BIHAMK Težina: N Uslijed obimnih sniježnih padavina BIHAMK-ov pozivni centar je bio zatrpan pozivima. Najčešće pitanje je bilo da li su prohodni putevi između dva grada. Vaš zadatak je da napišete program koji će pomoći operaterima u pozivnom centru da brzo i jednostavno odgovore na takve upite. Ulazni podaci U ulaznoj datoteci “bihamk.in” na prvoj liniji nalazi se dva cijela broja N (0 < N ≤ 1.000) i M (0 < M ≤ 100.000). Broj N je ukupan broj gradova, dok je broj M ukupan puteva koje BIHAMK ima u svojoj bazi. U svakoj od narednih M linija nalaze se po dva naziva gradova ai i bi međusobno razdvojena razmakom između kojih postoji put. Možete smatrati da se nazivi gradova sastoje isključivo od malih i velikih slova i znakova “_” koji se koriste onda kada bi se inače koristio razmak u nazivu grada – npr. Banja_Luka. Naziv niti jednog grada neće biti duži od 30 karaktera. Slijedi najviše 10.000 linija. Svaka linija je u jednom od sljedeća tri formata: • “N Prvi_grad Drugi_grad” - Označava da put između gradova Prvi_grad i Drugi_grad više nije prohodan uslijed sniježnih padavina; • “P Prvi_grad Drugi_grad” - Označava da je put između gradova Prvi_grad i Drugi_grad očišćen i opet prohodan; • “? Prvi_grad Drugi_grad” - Predstavlja upit da li je put između gradova Prvi_grad i Drugi_grad prohodan; • “X” - Naredba za prekid rada programa. Ove podatke trebate obrađivati sekvencijalno, odnosno odgovor na upit da li je put između dva grada prohodan treba dati samo na osnovu podataka koji su u datoteci nalaze prije samog upita. Izlazni podaci: U izlaznu dateteku “bihamk.out” trebate ispisati po jednu liniju za svaki postavljeni upit iz ulazne 87 datoteke, a koja predstavlja odgovor na odgovarajući upit iz ulazne datoteke. Ukoliko je put prohodan, treba ispisati tekst u obliku “Put izmedju gradova Prvi_grad i Drugi_grad je prohodan.”. Analogno ukoliko put nije prohodan, treba ispisati tekst u obliku “Put izmedju gradova Prvi_grad i Drugi_grad nije prohodan.”. Primjer: bihamk.in 6 6 Sarajevo Kiseljak Sarajevo Visoko Visoko Kakanj Kakanj Zenica Kiseljak Zenica Zenica Travnik ? Sarajevo Travnik N Visoko Kakanj ? Sarajevo Kakanj N Kiseljak Zenica ? Sarajevo Travnik P Visoko Kakanj ? Travnik Sarajevo X bihamk.out Put Put Put Put izmedju izmedju izmedju izmedju gradova gradova gradova gradova Sarajevo i Travnik je prohodan. Sarajevo i Kakanj je prohodan. Sarajevo i Travnik nije prohodan. Travnik i Sarajevo je prohodan. Rješenje Prirodan način modeliranja ovog problema je korištenjem grafova. Jedan od načina predstavljanja grafova u memoriji je matrica susjedstva. Najprije sve čvorove grafa numerišimo brojevima od 0 do N-1, gdje je N ukupan broj čvorova u grafu. Čvorovi u ovom zadatku označeni su nizovima karaktera (stringovima). Slijedi implementacija funkcije koja osigurava jednoznačno preslikavanje naziva gradova na brojeve od 0 do N-1. int numerisi_grad(char naziv_grada[]) { /*Statičke varijable (čuvaju vrijednost između poziva funckije) kojima *memorišemo preslikavanje*/ static char numeracija_gradova[max_n][31]; static char numerisano_gradova = 0; //Prvo pretražimo listu već numerisanih gradova. int i; for (i = 0; i < numerisano_gradova; ++i) if (strcmp(naziv_grada, numeracija_gradova[i]) == 0) /*Ukoliko grad sa traženim nazivom postoji varijabla i će imati *vrijednost indeksa niza gdje je smješten naziv numerisanog niza*/ break; /*Ukoliko grad sa traženim nazivom ne postoji, varijabla i će imati *vrijednost ukupnog broja do tada memorisanih gradova (nakon što se završe *sve iteracije gornje for petlje*/ if (i == numerisano_gradova) { //Ukoliko grad prije nije zapamćen, uradimo to sada 88 strcpy(numeracija_gradova[i], naziv_grada); ++numerisano_gradova; } /*Indeks niza gdje je memorisan naziv grada predstavlja jedinstveni cijeli *broj na koji se taj naziv preslikava*/ return i; } Matrica susjedstva A je matrica koja se sastoji od jedinica i nula dimenzije N×N. Ako je element aij matrice A jedinica, tada postoji grana od čvora i do čvora j. Suprotno, ako je element aij matrice A nula, tada ne postoji grana od čvora i do čvora j. Skup sljedećih globalnih varijabli je dovoljan za potpuni opis korištenog grafa. const int max_n = 1000; int n; bool matrica_susjedstva[max_n][max_n]; Prednost predstavljanja grafa matricom susjedstva u odnosu na listu susjedstva ogleda se uglavnom u efikasnom uklanjanju već postojećih grana i efikasnom provjerom da li postoji grana između dva čvora. Ova prva osobina daje matrici susjedstva prednost za ovaj zadatak zbog potrebe da se određene grane grafa brišu (kada odgovarajući put postane neprohodan). Sa druge strane, iz liste susjedstva može se efikasnije dobiti lista svih čvorova sa kojim određeni čvor dijeli granu, što listu susjedstva čini efikasnijim izborom za (između ostalog) algoritme kretanja kroz graf. Ovo listi susjedstva također daje prednost za ovaj zadatak zbog potrebe korištenja nekog od algoritama za kretanja kroz graf da bi se provjerilo da moguće putovati između dva grada. Optimalan izbor strukture za predstavljanje grafa u memoriji bi uključivao kombinaciju liste susjedstva sa dodatnom oznakom da li je određeni put između dva grada prohodan. Ipak, zbog jednostavnosti i za potrebe ilustracije korištenja matrice susjedstva u ovom rješenju je korištena matrice susjedstva kao način predstavljanja grafova u memoriji. Dodavanje grane u graf i brisanje grane iz grafa su sada vrlo jednostavne operacije. Slijede njihove implementacije. Napomenimo da opet koristimo neusmjereni graf. //Funkcija za dodavanje grane u neusmjereni graf void dodaj_granu(int a, int b) { matrica_susjedstva[a][b] = true; matrica_susjedstva[b][a] = true; } //Funkcija za brisanje grane void obrisi_granu(int a, int { matrica_susjedstva[a][b] matrica_susjedstva[b][a] } iz neusmjerenog grafa b) = false; = false; Algoritmi kretanja kroz graf se mogu koristiti da bi se provjerilo da li postoji način da se dođe od čvora a do čvora b. Kod DFS pretrage krećemo od proizvoljnog čvora grafa, koji označavamo sa 0. Od tog čvora se krećemo dalje ka proizvoljnom čvoru, koji označavamo oznakom 1. U svakom koraku, trudimo se da nastavljamo dalje prema bilo kojem neoznačenom čvoru, pri čemu svaki čvor u koji smo došli obilježavamo sa oznakom koja je za jedinicu veća od oznake čvora iz kojeg smo krenuli. Postupak nastavljamo sve dok ne 89 dođemo do čvora iz kojeg dalje ne vodi niti jedna grana ka nekom neoznačenom čvoru. U tom slučaju, vraćamo se nazad duž puta kojim smo došli do prvog čvora iz kojeg postoji grana koja vodi ka nekom neoznačenom čvoru. Tada nastavljamo duž te grane, dok ponovo ne dođemo do čvora iz kojeg nije moguće dalje kretanje prema nekom neoznačenom čvoru. Postupak se ponavlja dok ne iscrpimo svaku dalju mogućnost kretanja. Najkrača implementacija DFS pretrage je rekurzivna. U ovom zadatku oznaka čvorova kao rezultat DFS pretrage nije od posebnog interesa pa ćemo samo koristi jedan globalni niz koji označava da li je čvor posjećen ili nije. bool dfs_posjecen_cvor[max_n]; Rekurzivna implementacija DFS pretrage bazirana je na činjenici da se za vrijeme DFS pretrage uvijek pokušava posjetiti neki novi čvor koji nije prethodno posjećen (a to ustvari znači pokrenuti novu DFS pretragu u čvoru koji želimo posjetiti), dok povratak nazad nastupa tek onda kada više nemamo izbora, odnosno kada nema novih neposjećenih čvorova do kojih vodi neka grana iz trenutnog čvora. Povratak nazad osiguran je otpetljavanjem rekurzije. void dfs(int cvor) { dfs_posjecen_cvor[cvor] = true; for (int i = 0; i < n; ++i) if (matrica_susjedstva[cvor][i] && !dfs_posjecen_cvor[i]) dfs(i); } Sada se provjera da li se kretanjem kroz graf može doći od jednog do drugog čvora ustvari svodi na pokretanje DFS pretrage u jednom čvoru i provjeri da li je prilikom te pretrage posjećen drugi čvor. bool povezani_cvorovi(int prvi_cvor, int drugi_cvor) { for (int i = 0; i < n; ++i) dfs_posjecen_cvor[i] = false; dfs(prvi_cvor); return dfs_posjecen_cvor[drugi_cvor]; } Implementacija ostatka programa koja slijedi sastoji se od učitavanja podataka iz ulazne datoteke u specificiranom formatu i nihovog tumačenja, korištenja objašnjenih funkcija i ispisa dobijenih rezultata u navedenom formatu. int main() { FILE* ulazna_datoteka = fopen("bihamk.in", "r"); FILE* izlazna_datoteka = fopen("bihamk.out", "w") ; int m; fscanf(ulazna_datoteka, "%d %d", &n, &m); for (int i = 0; i < m; ++i) { char a[31], b[31]; fscanf(ulazna_datoteka, "%s %s", a, b); dodaj_granu(numerisi_grad(a), numerisi_grad(b)); } while (true) 90 { char komanda[2], a[31], b[31]; fscanf(ulazna_datoteka, "%s", komanda); if (komanda[0] == 'X') break; fscanf(ulazna_datoteka, "%s %s", a, b); if (komanda[0] == 'P') dodaj_granu(numerisi_grad(a), numerisi_grad(b)); else if (komanda[0] == 'N') obrisi_granu(numerisi_grad(a), numerisi_grad(b)); else if (povezani_cvorovi(numerisi_grad(a), numerisi_grad(b))) fprintf(izlazna_datoteka, "Put izmedju gradova %s i %s je prohodan.\n", a, b); else fprintf(izlazna_datoteka, "Put izmedju gradova %s i %s nije prohodan.\n", a, b); } fclose(ulazna_datoteka); fclose(izlazna_datoteka); return 0; } Zadatak 13.3. Nomenklatura Težina: N Biološka klasifikacija je način na koji biolozi grupiraju i kategoriziraju izumrle i živuće vrste i organizme. Moderna klasifikacija vuče korijene iz sistema Carolusa Linnaeusa, koji je grupisao vrste prema njihovim zajedničkim fizičkim osobinama. Nakon Linnaeusa, ove skupine su dorađene kako bi se dovele u bližu vezu s Darwinovim načelom o zajedničkom pretku. Tako se, na primjer, čovjek klasificira na sljedeći način: Vrsta razreda Razred klasifikacije klasifikacije Carstvo Animalia Pleme Chordata Klasa Mammalia Red Primates Porodica Hominidae Potporodica Hominini Rod Homo Vrsta Sapiens Binomna nomeklatura je naziv za sistem imenovanja vrsti živih bića u kojem se svakoj vrsti daje naziv koji se sastoji od dva dijela (imena roda i imena vrste). Riječi ne moraju nužno biti latinskog porijekla ali se daju imajući u vidu gramatička pravila latinskog jezika. Tako je, na primjer, naziv za čovjeka Homo sapiens. 91 Konvencijom je utvrđeno da je prvo slovo imena roda veliko slovo, dok je prvo slovo imena vrste malo slovo. Vaš zadatak je da napravite jednostavni program za klasifikaciju živih bića. Stablo klasifikacije je moguće jednostavno definisati skupom uređenih parova riječi na način da je prva riječ viši razred klasifikacije a druga riječ niži razred klasifikacije koji pripada prethodnom. Tako, na primjer, djelomično definisano stablo klasifikacije neophodno za klasifikaciju čovjeka bi bilo određeno uređenim parovima riječi (Animalia, Chordata), (Chordata, Mammalia), (Mammalia, Primates), (Primates, Hominidae), (Hominidae, Hominini), (Hominini, Homo), (Homo, Sapiens). Ulazni podaci Na prvoj liniji ulazne datotetke “nomenklatura.in” nalazi se cio broj N (0 < N ≤ 100.000). U narednih N linija ove datoteke nalaze se po dvije riječi odvojene razmakom. Sve riječi imaju veliko početno slovo i ostala mala slova i ne sastoje se od više od 30 slova. Svaki od ovih redova opisuje jednu vezu u stablu klasifikacije na način kako je to objašnjeno ranije. Možete smatrati da će se redovi u daoteteci pojavljivati u onom redoslijedu koji garantuje da će se viši razred klasifikacije uvijek pojaviti prije nižeg. U onom slučaju kada se određeni razred klasifikacije pojavi prvi put, možete smatrati da je u pitanju carstvo, odnosno da je taj razred klasifikacije novi korijen stabla klasifikacije. Moguće je da bude više od jednog korjena. U sljedećoj liniji ulazne datoteke nalazi se se broj M (0 < M < 10.000). U narednih M linija ove datoteke nalaze se vrste za koje je potrebno izvršiti klasifikaciju. Prva riječ je ime roda, druga riječ je ime vrste, dok su preostale riječi naziv vrste na našem jeziku. Naziv na našem jeziku (koji se može sastojati od malih i velikih slova i razmaka) neće biti duži od 30 karaktera ukupno. Prva i treća riječ imaju početno veliko slovo dok su sva ostala slova mala. Izlazni podaci U svaku od M linija izlazne datoteke “nomenklatura.out” treba ispisati po jednu od traženih klasifikacija u onom redoslijedu u kojem su navedene u ulaznoj datoteci. Prvo treba ispisati naziv na našem jeziku, zatim jednu dvotačku iza koje slijedi jedan razmak, a zatim treba pobrojati nazive svakog razreda klasifikacije kojem pripada ta vrsta počevši od najvišeg redom do najnižeg međusobno razdvojene razmacima. Ukoliko navedena vrsta nije u stablu klasifikacije, iza dvotačke i razmaka treba ispisati tekst “klasifikacija ne postoji”. Primjer nomenklatura.in 10 Plantae Angiosperms Angiosperms Eudicots Eudicots Rosids Rosids Rosales Rosales Rosaceae Rosaceae Malus Malus Domestica Rosaceae Pyrus Pyrus Communis Pyrus Ussuriensis 4 Malus domestica Jabuka Pyrus communis Evropska kruska Pyrus ussuriensis Sibirska kruska Malus sieversii Divlja jabuka nomenklatura.out Jabuka: Plantae Angiosperms Eudicots Rosids Rosales Rosaceae Malus Domestica 92 Evropska kruska: Plantae Angiosperms Eudicots Rosids Rosales Rosaceae Pyrus Communis Sibirska kruska: Plantae Angiosperms Eudicots Rosids Rosales Rosaceae Pyrus Ussuriensis Divlja jabuka: klasifikacija ne postoji Rješenje Stablo je struktura podataka koja modelira hijerarhijsku strukturu stabla ili drveta. U osnovi to je specijalan slučaj usmjerenog grafa kod kojeg ne postoje ciklusi 2 i svaki čvor ima tačno jednog roditelja 3 osim jednog čvora koji nema roditelja i naziva se korjenom stabla. Iz ovoga se nameće način za jednostavnu implementaciju drveta. Koristimo strukturu podataka koja sadrži informacije koje čvor po prirodi problema treba da sadrži, informaciju o tome koji je njegov roditelj ili informaciju da nema roditelja, odnosno da je korijen drveta. Sve čvorove smještamo u niz, tako da je informacija o roditelju ustvari indeks čvora koji je roditelj ili -1 ukoliko čvor nema roditelja. U ovom zadatku svaki čvor stabla ja ustvari jedan razred klasifikacije, pa je pogodno koristiti sljedeću strukturu za predstavljanje jednog čvora u memoriji struct Cvor { int roditelj; char naziv[30]; }; Napomenimo da postoje implementacije stabla koje se baziraju na upotrebi pokazivača i dinamičke alokacije memorije. Ove implementacije su efikasnije sa aspekta iskorištenja memorije onda kada unaprijed nije poznata veličina stabla. Operacije nad stablom koje će nam biti potrebne za rješenje zadatka su umetanje novog čvora u stablo ispod određenog roditelja, pretraga čvorova stabla po nazivu i kretanje po stablu od trenutnog čvora do korjena stabla. Algoritam koji rješava postavljeni zadatak koristeći ove operacije nad stablom je sljedeći: 1. Učitaj viši i niži razred klasifikacije sa trenutne linije iz datoteke. 2. Pronađi viši razred u stablu po imenu. 3. Ako razred nije pronađen, dodaj novi korijen stabla sa nazivom višeg razreda. 4. Dodaj novi čvor u stablo sa nazivom nižeg razreda i čvorom gdje je viši razred pronađen kao roditeljom. 5. Vrati se nazad na korak 1 sve dok se ne unese N linija iz datoteke. 6. Učitaj ime roda, ime vrste i naziv na našem jeziku sa trenutne linije iz datoteke. 7. Pronađi čvor čiji je naziv jednak nazivu vrste i čiji roditelj ima naziv koji je jednak nazivu roda. 8. Krečući se uz stablo, tako da se svaki put prelazi na roditelja trenutnog čvora sve do korjena stabla ispisati nazive svih čvorova koji se na ovaj način posjete, ali u obrnuton redoslijedu od redoslijeda posjećivanja. Opišimo još kako realizovati navedene operacije nad stablom. Niz u kojem su smješteni čvorovi kao i varijabla koja čuva podatak o tome koliko je trenutno čvorova u stablu su globalne varijeble zbog jednostavnosti implementacije. Obzirom da u datoteci može biti najviše 100.000 linija to znači da broj 2 U grafu ne postoji ciklus ako polazeći od proizvoljnog čvora slijedeći bilo koju ivicu proizvoljan broj puta, nije se moguće vratiti ponovo u isti čvor. 3 Neka postoji usmjerena ivica između čvora a i čvora b. Tada se čvor a naziva roditeljom čvora b, a čvor b se naziva djetetom čvora a. 93 različitih čvorova ne može biti veći od 200.000 (u slučaju kada su sve riječi različite). int trenutno_cvorova = 0; Cvor cvorovi[200000]; Operacija dodavanja čvora u stablo je jednostavna. Na kraj niza se doda novi čvor i poveća se broj čvorova koji su trenutno u stablu. Operacija pretrage čvorova u stablu također nije komplikovana. Jednostavno se prolaskom kroz niz traži onaj čvor čiji je naziv jednak traženom nazivu i vrača se indeks mjesta u nizu gdje se on nalazi ili -1 ukoliko takav čvor nije pronađen. Slijede implementacije ovih funkcija. //Funckija za dodavanje čvora u stablo sa određenim nazivom ispod roditelja void dodaj_cvor(int roditelj, char naziv[]) { cvorovi[trenutno_cvorova].roditelj = roditelj; /*Stringove (odnosno nizove karaktera) treba dodjeljivati koristeći *funckiju strcpy a ne operatorom '='*/ strcpy(cvorovi[trenutno_cvorova].naziv, naziv); ++trenutno_cvorova; } //Funckija koja vraća indeks u nizu čvora sa određenim nazivom int pronadji_cvor(char naziv[]) { for (int i = 0; i < trenutno_cvorova; ++i) /*Stringove (odnosno nizove karaktera) treba porediti koristeći *funckiju strcmp a ne operatorom '=='.*/ if (strcmp(cvorovi[i].naziv, naziv) == 0) return i; //Ukoliko čvor nije pronađen vračamo -1 return -1; } Operacije kretanja kroz stablo je nešto kompleksnija. Njena najednostavnija realizacija je rekurzivna. Slijedi konkretna implementacija sa ispisom tražene klasifikacije. //Funckija koja ispisuje klasifikaciju za čvor sa određenim indeksom void ispisi_klasifikaciju_id(int id) { if (cvorovi[id].roditelj != -1) ispisi_klasifikaciju_id(cvorovi[id].roditelj); fprintf(izlazna_datoteka, " %s", cvorovi[id].naziv); } /*Funckija koja ispisuje klasifikaciju za čvor sa određenim nazivom i *određenim nazivom svog roditelja*/ void ispisi_klasifikaciju(char naziv_rod[], char naziv_vrsta[]) { //Prvo vršimo sekvencijalnu pretragu niza tražeći čvor sa navedenim uslovima bool pronadjen = false; for (int i = 0; i < trenutno_cvorova && !pronadjen; ++i) if (cvorovi[i].roditelj != -1 && strcmp(cvorovi[i].naziv, naziv_vrsta) == 0 && strcmp(cvorovi[cvorovi[i].roditelj].naziv, naziv_rod) == 0) { //Ako je čvor pronađen, vršimo ispis klasifikacije ispisi_klasifikaciju_id(i); pronadjen = true; 94 } } //Ako je čvor nije pronađen, ispišemo odgovarajuću poruku if (!pronadjen) fprintf(izlazna_datoteka, " klasifikacija nije pronadjena."); fprintf(izlazna_datoteka, "\n"); Ova funkcija poziva samu sebe u onim slučajevima kada postoji roditelj, a zatim ispisuje naziv trenutnog čvora. Polazeći od čvora koji se nalazi na određenom mjestu u nizu funkcija će se rekurzivno pozvati nad njegovim roditeljom. Taj novi poziv funkcije će uzrokovati novi rekurzivni poziv, i tako dalje sve do trenutka kada trenutni čvor postane korijen stabla odnosno kada više ne postoji roditelj čvora. Tada će se ispisati naziv krojena stabla, a zatim uslijed otpetljavanja rekurzije ispisat će se i nazivi ostalih čvorova koji su posjećeni i to u suprotnom redoslijedu od onog u kojem su posjećeni. Da je bilo traženo da se postigne ispis u redoslijedu koji odgovara redoslijedu posjete čvorova, to bi se postiglo tako što bi se ispis vršio prije (eventualnog) rekurzivnog poziva funckije. Slijedi i implementacija ostatka programa koji vrši operacije nad ulaznom i izlaznom datotekom i implementira ranije opisani algoritam. int main() { FILE* ulazna_datoteka = fopen("nomenklatura.in", "r"); izlazna_datoteka = fopen("nomenklatura.out", "w"); int n; fscanf(ulazna_datoteka, "%d", &n); for (int i = 0; i < n; ++i) { char naziv_roditelj[30], naziv_dijete[30]; fscanf(ulazna_datoteka, "%s %s", naziv_roditelj, naziv_dijete); int roditelj = pronadji_cvor(naziv_roditelj); if (roditelj == -1) { dodaj_cvor(-1, naziv_roditelj); roditelj = trenutno_cvorova - 1; } dodaj_cvor(roditelj, naziv_dijete); } int k; fscanf(ulazna_datoteka, "%d", &k); for (int i = 0; i < k; ++i) { char naziv_rod[30], naziv_vrsta[30], naziv[31]; fscanf(ulazna_datoteka, "%s %s ", naziv_rod, naziv_vrsta, naziv); /*Naziv vrste u ulaznoj datoteci je sa početnim malim slovom pa ga *konvertujemo u odgovarajuće veliko slovo jer su čvorovi u stablu *klasifikacije svi sa početnim velikim slovom*/ naziv_vrsta[0] = toupper(naziv_vrsta[0]); fgets(naziv, 31, ulazna_datoteka); /*fgets će unijeti i karakter '\n' koji je oznaka kraja reda a nije *dio naziva na našem jeziku*/ naziv[strlen(naziv)-1] = '\0'; fprintf(izlazna_datoteka, "%s:", naziv); ispisi_klasifikaciju(naziv_rod, naziv_vrsta); } fclose(ulazna_datoteka); fclose(izlazna_datoteka); return 0; 95 } Zadatak 13.4. Farma Težina: I Napomena: Odlukom komisije, na kantonalnom takmičenju iz informatike 2012. godine neće biti uvrštena oblast "najkraći put". Ipak, radi kompletnosti ove zbirke, dajemo i jedan zadatak iz ove oblasti koji je bio dat na kantonalnom takmičenju 2007. godine. Farmer posjeduje livadu na kojoj su posađene kruške. Farma je dimenzija NxN (1 < N < 10) i na svakom polju te farme je posađeno stablo kruške. Farmer je zasadio 5 različitih sorti krušaka, tako da svaka daje različit broj plodova dnevno. Prva sorta daje 1 plod dnevno, druga 2 ploda dnevno, treća daje 3 ploda dnevno, četvrta 4 ploda dnevno i peta sorta daje 5 plodova dnevno. Zadatak je da napravite put farmera iz polja najgornje lijeve kruške do polja najdonje desne kruške pri kojem će farmer sakupiti najveći broj plodova krušaka. Farmer se može kretati samo u smjeru juga (dolje) i istoka (desno). Ulaz: Prva linija ulaza sadrži dimenziju polja N. Ostale linije predstavljaju redove tog polja koji sadrže sortu krušaka (1-5). Izlaz: U prvoj liniji treba da sadrži maksimalan broj skupljenih krušaka, a u drugoj put kojim je farmer išao da bi odabrao taj broj krušaka. Ukoliko farmer treba da ide desno oznaka je R a ukoliko treba da ide prema dolje oznaka je D. 96 Literatura 1. Haris Šupić, Algoritmi i strukture podataka, Elektrotehnički fakultet Univerziteta u Sarajevu, Sarajevo 2010. 2. Željko Jurić, Diskretna matematika za studente tehničkih nauka, Elektrotehnički fakultet Univerziteta u Sarajevu, Sarajevo 2011. 3. Željko Jurić, Tehnike programiranja, skripta. 97
© Copyright 2024 Paperzz