Tietorakenteet, laskuharjoitus, 6.-9.1 Muista TRAKLA-tehtävien deadline 31.1. 1. Tarkastellaan ensin tehtävää yleisellä tasolla. Jos funktion T vaativuusluokka on O(f), niin funktio T on muotoa T (n) = cf(n) + dg(n), missä g = O(f) ja c sekä d ovat vakiokertoimia. Ehto g = O(f) seuraa siitä, että muulloin T O(f). Tehtävänannon nojalla voidaan jättää funktio g huomioimatta. Itse asiassa tämä rajoitus on oleellinen, sillä ilman sitä olisi mahdotonta saada yksikäsitteisiä ratkaisuja tämän tehtävän kohtiin. Näiden havaintojen nojalla jokaisessa tehtävän kohdassa riittää ratkaista yhtälöstä cf(00) = 1 ms vakiokerroin c ja laskea sen jälkeen arvo lausekkeelle cf(5000), josta nähdään kuinka monta millisekuntia kullakin vaativuudella kuluisi aikaa syötteen käsittelyyn, jonka koko on 5000. (a) O(log n): Käsitellään edellinen päättely vielä tässä erikoistapauksessa, jotta ajatus tulisi selkeämmäksi konkreettisen esimerkin avulla. Algoritmin aikavaativuus on O(log n), joten sen vaativuutta tarkasti kuvaava funktio on muotoa c log n + dg(n), jossa d on vakio ja g(n) on jokin funktio, jolle pätee g(n) = O(log n). Edellä käsitellyn yleisemmän päättelyn merkinnöin f = log. Koska g voitiin jättää huomiotta riittää ratkaista c log 00 = 1. c log 00 = 1 ms c = 1 ms log 00 Löydettiin c:lle arvo. Nyt voidaan laskea kauan suoritus kestäisi kun n = 5000. Huomaa, että tässä vaiheessa ei vielä oltu kiinnitetty logaritmin kantalukua. Käyttämällä logaritmin kannanvaihtosääntöä log a x = log b x log b a osoittautuukin, ettei se ole lainkaan välttämätöntä. Siis olkoon a > 1. c log a 5000 = log a 5000 log a 00 ms = log 00 5000 ms 1, 61 ms Havaittiin, että algoritmi on lähes yhtä nopea 5000:n kokoiselle syötteelle, kuin 00:nkin kokoiselle syötteelle. Tämän laskun perusteella logaritmisen aikavaativuuden algoritmit vaikuttavat hyvin tehokkailta. (b) O(n): Ratkaistaan ensin c tunnetun arvon nojalla. c00 = 1 ms c = 1 ms 00 Nyt voidaan laskea algoritmin kesto 5000:n kokoiselle syötteelle. c5000 = 5000 ms 00 = 5 ms 1
(c) O(n log n): Tämä lasketaan kuten edelliset kohdat. Suoritusaika, kun n = 5000: c5000 log 5000 = (d) O(n ): Ratkaistaan c: Suoritusaika, kun n = 5000: c00 log 00 = 1 ms c = c5000 = 5000 log 5000 ms 00 log 00 1 ms 00 log 00 c00 = 1 ms c = 1 ms 40000 5000000 ms 40000 = 5 log 00 5000 ms 40, 19 ms = 65 ms = 0, 65 s Havaitaan, että neliöllisen aikavaativuuden algoritmeilla suoritus alkaa jo hidastua tuntuvasti. (e) O( n ): Ratkaistaan c: c 00 = 1 ms c = 1 ms 00 Suoritusaika, kun n = 5000: c 5000 = 5000 ms 00 = 4800 ms Tälläisenään luku ei kerro vielä juuri mitään. Yhdessä vuodessa on 365 4 60 60 1000 = 31536000000 ms ja log 31536000000 34, 876, joten algoritmin suoritus vie aikaa noin 4765, 558 10 1434 vuotta. Eräiden arvioiden mukaan maailmankaikkeudessa on atomeja noin 10 80 kappaletta. Samaisen lähteen mukaan maailmankaikkeuden ikä on vaivaiset 13, 7 10 9 vuotta, joten suorittaaksemme näin kauan kestävän algoritmin tarvitsisimme aikaa vähintään 00 kertaa maailmankaikkeuden iän verran. Tämän tarkastelun nojalla eksponentiaalisen aikavaativuuden algoritmi vaikuttaa hyvin pitkälti käyttökelvottomalta erittäin pienestä vakiokertoimesta huolimatta. Käytännössä eksponentiaalisen aikavaativuuden algoritmeja tulisikin välttää viimeiseen asti. Joskus niitä on pakko käyttää, mutta tällöin on varmistuttava, että mahdolliset syötteet ovat tarpeeksi pieniä.. Yksi minuutti sisältää 60 1000 = 60000 ms. Tehtävässä voidaan käyttää hyväksi edellä ratkottuja vakiokertoimia. Jokaisessa kohdassa riittää nyt etsiä suurin n:n arvo, joka toteuttaa epäyhtälön cf(n) 60000 ms. (a) O(log n): log a n ms log a 00 60000 ms log 00 n 60000 n 00 60000 6, 3 10 138061 Huomataan, että logaritmisen aikavaativuuden algoritmille voidaan antaa käytännössä kuinka iso syöte tahansa, jos jaksetaan odotella vähän aikaa.
(b) O(n): n ms 00 60000 ms n 00 60000 = 1, 107 Myös lineaarisen ajan vievä algoritmi sietää vielä hyvin suuria syötteitä. (c) O(n log n): n log a n ms 00 log a 00 60000ms n log a n log a 00 1000000 n log 00 n 1000000 Edellä olevaa epäyhtälöä ei kyetä ratkaisemaan analyyttisesti ainakaan tämän kurssin esitiedoilla. Kokeilemalla havaitaan, että suurin kokonaisluku, jolla epäyhtälö on vielä voimassa on 4170906. (d) O(n ): Käytetään jälleen edellisessä kohdassa ratkaistun vakiokertoimen c arvoa hyväksi. (e) O( n ): n ms 40000 60000 ms n 400000000 n 400000000 48989, 79 n ms 00 60000 ms n 00 60000 log n 00 log 60000 n 00 log 60000 n 00 + log 60000 15, 87 Eksponentiaalisen aikavaativuuden algoritmissa syötteen ei tarvitse olla kovinkaan suuri, jotta suoritusaika hidastuu. Todellisissa tapauksissa vakiokertoimet eivät ole niin pieniä kuin tässä tapauksessa. Esimerkiksi vakiokertoimella c = 1 algoritmille voitaisiin antaa korkeintaan 15:n kokoinen syöte, mikäli haluttaisiin laskennan suoriutuvan alle minuutissa. 3. Muistutetaan tässä, että funktio f kuuluu kertaluokkaan O(g), jos on olemassa vakiot d > 0 ja n 0 siten että kaikilla n > n 0 pätee f(n) dg(n). Tätä merkitään f = O(g). (a) Näytetään, että n + 7n + 3 = O(n 3 ). Tässä on ideana arvioida annettua funktiota sopivalla tavalla ylöspäin. Sopiva tapa määräytyy kulloisenkin tavoitteen mukaan. Tässä tehtävässä halutaan osoittaa funktion vaativuusluokaksi O(n 3 ), joten ylärajaksi pitäisi saada d n 3, jollain d > 0. Olkoon n > 0. n + 7n + 3 n + 7n + 3n n + 7n + 3n = 1n 1n 3 Edellinen laskussa osoitettiin, että n + 7n + 3 1n 3 kun n > 0, siis määritelmän vakioiksi kelpaavat n 0 = 0 ja d = 1. 3
(b) Näytetään, että n 3 + 7n + 3 O(n ). Todistetaan tämä vastaoletuksen avulla. Oletetaan siis, että n 3 + 7n + 3 = O(n ). Olkoon d ja n 0 määritelmän takaamat vakiot, joille pätee vastaväitteen nojalla n 3 + 7n + 3 dn kaikille n > n 0. Arviomalla funktiota alaspäin kun n > n 0 > 0 saadaan: n 3 n 3 n 3 + 7n + 3 dn Jakamalla puolittain luvulla n tästä seuraa, että n d kaikilla n > n 0 0. Tämä ei voi päteä koska lukua n voidaan kasvattaa rajatta. Saatiin aikaiseksi ristiriita, joten vastaoletuksen on oltava väärä. Saatiin siis todistettua n 3 + 7n + 3 O(n ). Edellisessä päättelyssä päästiin tulokseen, josta seuraa ristiriita, mutta näytetään vielä matemaattisen täsmällisyyden nimissä kaksi erilaista tapaa kaivaa ristiriita edellisestä lausekkeesta. Kummassakin on ajatuksena valita n:lle jokin arvo, jolla vastaväitteen mukaan pitäisi päteä n d. Valintaa n:n arvoksi rajoittaa ehto n > n 0. Saavuttaaksemme ristiriidan halutaan myös että n > d. Erääksi ristiriitaan johtavaksi arvoksi kelpaa n = max{n 0, d + 1. Tästä seuraa kaksi vaihtoehtoa, jotka kummatkin on käsiteltävä erikseen. Käsitellään ensin tapaus d > n 0. Tällöin n = d + 1 ja edellä johtamamme epäyhtälö väittää d + 1 d 1 0, joka on ilmiselvä ristiriita. Toisessa tapauksessa n 0 d, jolloin saadaan edellä olevan nojalla Siis pääsimme jälleen ristiriitaan. n 0 + 1 d n 0 n 0 + 1 n 0 1 0 Esitetään vielä toinen sopiva arvo n:lle, jonka avulla päädytään ristiriitaan. Asetetaan n = (n 0 + 1)(d + 1). Tätä hiukan yllättävää valintaa puoltaa havainto, että d voi olla kuinka pieni positiivinen luku tahansa. Lisäämällä d:hen 1 ja kertomalla n 0 +1 tällä luvulla voidaan olla varmoja, että kaikissa mahdollisissa tapauksissa n > n 0. Sijoittamalla tämä arvo edellä johdettuun epäyhtälöön, päästään haluttuun tulokseen. (n 0 + 1)(d + 1) d (n 0 + 1)d + n 0 + 1 d (n 0 + 1)d (n 0 + 1)d + n 0 + 1 d n 0 + 1 1 1 < n 0 + 1 1 1 < 1 Nähtiin, että myös tällä tavoin saavutetaan ristiriitan kohtuullisen yksinkertaisen laskennon jälkeen. (c) Näytetään, että log n O(10 16 ). Käytetään jälleen vastaoletusta, siis oletetaan, että log n = O(10 16 ). On siis olemassa vakiot d 0 ja n 0 s.e. log n d 0 10 16 = d 0 10 16 1. Merkitään d = d 0 10 16. Nyt log a n d n a d, joka on selvästikin ristiriita n:n arvolla (n 0 +1)(a d +1). Siis alkuperäinen väite pitää paikkansa. 4
(d) Osoitetaan, että 10 6 = O(1). Kun valitaan määritelmän d:ksi esimerkiksi 10 7, niin kaikilla n pätee 10 6 d 1 = 10 7 1, joten n 0 :ksi kelpaa mikä tahansa luku. Kuten huomataan n ei vaikuta tähän vaativuusluokkaan kuuluvien funktioiden arvoon millään tavalla. Algoritmin aika- tai tilavaativuutena tämä tarkoittaa sitä, että syötteen koko ei vaikuta algoritmin suoritusaikaan tai sen käyttämään muistiin millään tavalla. Toisin sanoen algoritmin käyttämä aika tai tila on saman kokoinen kaikilla syötteillä. Tälläisiä algoritmeja kutsutaan vakioaikaisiksi tai sanotaan että algoritmi käyttää vakiomäärän muistia. (e) Osoitetaan, että log n = O(log n). Intuiitiivisesti väite on selvä, sillä eksponentti saadaan tuotua logaritmin sisältä logaritmin kertoimeksi. Käsitellään tämä kuitenkin hiukan yksityiskohtaisemmin. Käytetään logaritmeille mielivaltaisia kantalukuja a, b > 1 havainnollistaaksemme miksi aikavaativuusmerkinnöissä ei tarvitse kiinnittää logaritmin kantaa. Logaritmin laskusäännöistä tiedetään, että log a n = log a n = log b n log b a = log b a log b n Tämä arvio antaa määritelmän d:tä vastaavan luvun valitsemiseksi hyvän idean. Asetetaan d = 3 log b a. Nyt voidaan helposti osoittaa näyttää väite todeksi. log b a 3 log b a log b a log b n 3 log b a log b n kaikilla n > 0 log b n log b a 3 log b a log b n kaikilla n > 0 log a n 3 log b a log b n kaikilla n > 0 Tämä riittää osoittamaan väitteen. Korvaamalla laskuista numero k:lla ja 3 k + 1:llä oltaisiin todistettu log n k = O(log n). 4. Tarkastellaan seuraavaa lukujen 1,..., n summan laskevaa algoritmia. Summa(n) 1 summa = 0 for i = 1 to n // Invariantti: summa == i 1 j 3 summa = summa + i Tarkastellaan aluksi algoritmin aikavaativuutta. Rivilla 1 oleva sijoitus suoritetaan ainoastaan kerran koko algoritmin avulla. Tavallinen sijoitus sujuu vakioajassa. 5
Rivillä oleva silmukan tarkistus suoritetaan n + 1 kertaa. Itse tarkistus sujuu vakioajassa. Usein algoritmien analyysissa ajatellaan for-silmukoiden kätkevän taakseen while-silmukan kaltaisen rakenteen, jota seuraava koodin pätkä kuvaa. i = 0 while i < n i = i + 1... Tässä silmukassa while:n tarkistus suoritetaan selvästikin n + 1 kertaa. Esimerkiksi C-kielen tai Javan for-silmukoista on helposti nähtävissä samankaltaisuus while-silmukkaan. Rivi 3 suoritetaan tasan n kertaa. Jälleen itse rivillä suoritettava yhteenlasku kestää ainoastaan vakioajan. Jo tämän tarkastelun nojalla nähdään, että algoritmi vie lineaarisen ajan syötteen koon suhteen, kun ajatellaan koon tarkoittavan tässä yhteydessä argumenttina saatavaa numeroa. Osoitetaan vielä hiukan seikkaperäisemmin miksi algoritmin aikavaativuus on luokkaa O(n). Merkitään rivin i suoritukseen kuluvaa aikaa a i :llä. Kun n > 0 algoritmin viemää aikaa voidaan arvioida seuraavasti: a 1 + (n + 1)a + na 3 = a 1 + a + (a + a 3 )n a 1 n + a n + (a + a 3 )n = (a 1 + a + a 3 )n. Joten algoritmin aikavaativuutta kuvaava funktio on kaikilla n > 0 pienempi tai yhtäsuuri kuin dn, missä d = a 1 + a + a 3. Algoritmin tilavaativuus on vakio, siis luokkaa O(1), sillä olipa n minkä kokoinen tahansa, algoritmi tarvitsee ainoastaan yhden muuttujan. Tarkastellaan vielä algoritmin pseudokoodiesitykseen merkittyä invarianttia. Aloitettaessa silmukan ensimmäinen kierros i = 1. Invariantti väittää, että muuttuja summa arvo on i 1 j = 0 j = 0. Tämä pitää paikkansa, sillä muuttujaan summa on vielä koskematon alustuksen jäljiltä. Jos invariantti oli voimassa kun i = k, niin se pätee myös kun i = k + 1. Tällöin invariantti väittää, että muuttujan summa arvo on i 1 j = k+1 1 j = k j. Koska invariantti oli voimassa kun i = k, niin summan arvo on tämän nojalla seuraavalla kierroksella summa + k = k 1 j + k = k j = i 1 j, siis täsmälleen se mitä invariantti väittää kun i = k+1. Edellisen kaavan k tulee siitä, että kullakin kierroksella summaan lisätään i:n arvo. Saavuttaessa viimeisellä kierroksella i:n arvoa testaavaan silmukkaan pätee, että i = n+1. Edellä osoitettiin, että invariantti pätee myös tällöin, joten muuttujan summa arvoksi saatiin silmukan päätyttyä i 1 j = n j = 1 + +... + n, eli juurikin se mitä algoritmilla haluttiin saavuttaa. 5. Tehtävän oli tutkia algoritmia Evaluate-Polynomial. 6
Evaluate-Polynomial(A[0.. k], x) 1 s = 0 for i = 0 to A.length // Invariantti: s == i 1 A[j] xj 3 z = 1 4 for j = 1 to i 5 z = z x 6 s = s + A[i] z 7 return s Simuloidaan ensin algoritmin toimintaa funktiolla p(x) = x 3 3x 7 kun x =. Algoritmin alkutila: s == 0, i = 0, A[i] = 7 Tilanne ensimmäisen kierroksen jälkeen: s == 7, i = 1, A[i] = 0 Tilanne toisen kierroksen jälkeen: s == 7, i =, A[i] = 3 Tilanne kolmannen kierroksen jälkeen: s == 19, i = 3, A[i] = Tilanne viimeisen kierroksen jälkeen: s == 3, i = 4 Merkitään n = A.length 1, joka on luonnollinen mitta syötteen koolle. Rivi 1 suoritetaan kerran. Rivi suoritetaan n + 1 kertaa. operaatioita. Rivi 3 suoritetaan n kertaa. Rivi 4 suoritetaan jokaisella ulomman silmukan kierroksella i + 1 kertaa. Koko algoritmin aikana tämä rivi suoritetaan kertaa. n+1 0 +... + (n + 1) = i = i=0 (n + 1)(n + ) = O(n ) Rivi 5 suoritetaan yhden ulomman silmukan kierroksella i kertaa. Yhteensä tämä suoritetaan n(n+1) kertaa. Rivi 6 suoritetaan yhteensä n kertaa. Rivi 7 suoritetaan ainoastaan kerran. Koska aritmeettisten operaatioiden ajatellaan olevan vakioaikaisia nähdään ylläolevasta päättelystä, että algoritmin suoritusaika on O(n ). Tämä voidaan edellisen kohdan tapaan perustella seikkaperäisesti laskemalla algoritmin käyttämä aika joillain ajoilla a i. 6. (a) Summakaavan n i=1 i arvoksi tiedetään n(n+1). Puuttuva arvo voidaan selvittää laskemalla taulukon arvot yhteen ja vähentämällä saatu tulos ylläolevan kaavan arvosta. Esitetään ratkaisu vielä pseudokoodilla. 1 Lukua n 1 kutsutaan polynomin asteeksi. 7
Etsi_puuttuva(A[1.. n]) 1 summa = 0 for i = 1 to A.length 3 summa = summa + A[i] 4 n = A.length + 1 5 return (n (n + 1))/ summa (b) Koska tietorakenteen ainoa tarvittava operaatio on osasumma riittää tallettaa lukujen summat taulukkoon S. Jos rakenne alustetaan antamalla sille taulukko A, niin tällöin S[i] = i A[j]. osasumma(a, b) saadaan laskettua vakioaikaisesti erotuksena S[b] S[a 1] ja alustus vie lineaarisen ajan. Ohessa on tietorakenteen Java-luokkana sekä sen käyttöä esittelevä pääohjelma. public class Osasummat { private double[] summat; public Osasummat(double[] luvut) { this.summat = new double[luvut.length]; this.summat[0] = luvut[0]; for(int i = 1; i < luvut.length; i++) { this.summat[i] = this.summat[i-1] + luvut[i]; public double osasumma(int i,int j) { if (j >= this.summat.length) j = this.summat.length-1; if (i > 0) return this.summat[j] - this.summat[i-1]; else return this.summat[j]; class T6b { /* Metodi ottaa taulukkojen koon komentoriviparametrinaan. */ public static void main(string[] args) { if (args.length < 1) System.exit(0); System.out.println("foo"); int koko = Integer.parseInt(args[0]); // Talletetaan tähän taulukkoon luvut 0...n double[] taulu = new double[koko]; // Tähän approksimoidaan funktion x^ integraalin arvoa // välillä [0,1] double[] integraalit = new double[koko]; 8
double vali = (1.0/koko); for(int i = 0; i < koko; i++) { taulu[i] = i; integraalit[i] = vali*(math.pow(i/(double)koko+0.5*vali,)); Osasummat summat = new Osasummat(taulu); Osasummat integraali = new Osasummat(integraalit); System.out.println(summat.osasumma(0,koko - 1)); System.out.println(summat.osasumma(0,koko/)); System.out.println(summat.osasumma(koko/,koko)); System.out.println(integraali.osasumma(0,koko - 1)); System.out.println(integraali.osasumma(0,koko/)); System.out.println(integraali.osasumma(koko/,koko)); 9