Tietojenkäsittelytiede 30 Heinäkuu 2010 sivut 33–55 c kirjoittaja(t) Toimittaja: Jorma Tarhio Kuinka kiltti mutta tarpeeton pikku algoritmi sai tärkeän tehtävän Antti Valmari Tampereen teknillinen yliopisto Ohjelmistotekniikan laitos [email protected] Tiivistelmä Jos taulukon lokeroista yli puolet sisältää saman arvon, sanotaan sitä enemmistöarvoksi. Enemmistöarvon löytämiseen on olemassa monessa suhteessa esimerkillinen algoritmi: erittäin pieni, elegantti, nopea ja muistin käytöltään säästeliäs. Sen ainoa, mutta sitäkin merkittävämpi, puute on, että sille on melkein mahdoton keksiä järkevää käyttöä. Melkein aina joko tarvitaan tieto eniten esiintyvästä arvosta siinäkin tapauksessa, että se ei ole enemmistönä, ja sitä algoritmi ei tuota; tai eri arvoja on vain kaksi, jolloin tehtävä ratkeaa helpommalla laskemalla jomman kumman arvon esiintymiskerrat. Tästä syystä kirjoittaja on pitkään pitänyt algoritmia mielenkiintoisena mutta käytännössä hyödyttömänä akateemisena kuriositeettina. Yllättäen kirjoittajalle tuli vastaan tarve, johon algoritmi on juuri sopiva. Tämä kirjoitus alkaa algoritmin esittelyllä. Sitten esitellään joukko toistensa varaan rakentuvia algoritmeja, joista viimeinen käyttää enemmistöarvon löytämisalgoritmia apunaan. Matkan varrella kohdataan useita kiehtovia algoritmisia ideoita. Kerrotaan myös, miksi enemmistöarvon löytämisalgoritmi on erityisen sopiva tehtäväänsä viimeisessä algoritmissa. 1 Johdanto on vaikeustasoltaan vaihtelevia osia. Osa Tämän kirjoituksen päätavoitteena ei ole vaikeimmista asioista on ladottu lauseiden esitellä uusia tieteellisiä tuloksia, vaikka todistuksiksi, joten lukijan on helppo hysitäkin tässä kirjoituksessa tehdään. Al- pätä niiden yli niin halutessaan. Todistukgoritmien tutkimus on tuottanut monia sia ei kuitenkaan kannata hypätä yli ruyllättäviä ja joskus suorastaan nerokkai- tiininomaisesti, sillä osa kiehtovista ajata tuloksia, joiden oivaltaminen tuottaa tuksista on esitetty vain niissä. O- ja Θmielihyvää. Tämän kirjoituksen käsittele- merkintöjen tuntemisesta on apua, mutta mien asioiden tutkiminen tuotti kirjoitta- välttämätöntä se ei liene. jalle tällaista mielihyvää tavallista enemTieteellisten kirjoitusten tyyliin kuumän. Kirjoituksen päätavoitteena on välit- luu, että lukija pidetään koko ajan tietoitää tätä mielihyvää lukijoille. sena siitä, mihin tähdätään. SalapoliisikerKirjoitus on tarkoitettu laajalle luki- tomukseen sovellettuna se tarkoittaisi, etjakunnalle ohjelmoimaan juuri oppineis- tä syyllinen paljastetaan heti alussa ja lopta algoritmeja hyvin tunteviin. Siksi siinä puosa kirjasta perustelee, miksi juuri hän OHJE KIRJAPAINOLLE: B5-arkin vasen- ja yläreuna kohdistetaan A4-arkin vasempaan ja yläreunaan. Näin pitäisi marginaaliksi tulla taitteen puolella noin 33 mm ja muualla noin 22 mm. Kuinka kiltti mutta tarpeeton pikku algoritmi sai tärkeän tehtävän 34 on syyllinen. Tämän kirjoituksen tavoitteeseen sellainen sopii huonosti. Siksi tässä kirjoituksessa enimmäkseen annetaan vain vihjeitä siitä, minne ollaan menossa. Tavallisen tasoista ennakkotietoa kaipaava voi aloittaa lukemalla luvun 10, jossa kirjoituksen pääasiat on koottu yhteen. Tässä kirjoituksessa on myös tavallista tieteellistä kirjoitusta enemmän pohdittu, miksi ensimmäisenä mieleen tuleva tapa tehdä puheena oleva asia ei ole hyvä. Jollei tiedä, että suoraviivainen tapa tehdä jokin asia ei toimi, niin yllättävä tapa saattaa tuntua omituiselta kikkailulta. Jos on tietoinen asiaan liittyvistä ongelmista, niin yllättävän tavan nerokkuus on helpompi huomata. Aloittakaamme. 2 Enemmistöarvon löytämisalgoritmi Enemmistöarvon löytämisalgoritmi ratkaisee seuraavan tehtävän. On annettu arvot A[1], A[2], . . . , A[n], missä n ≥ 1. Jos jokin arvo esiintyy aineistossa yli n/2 kertaa, niin tehtävänä on ilmoittaa tämä arvo. Jos mikään arvo ei esiinny aineistossa yli n/2 kertaa, niin algoritmi saa palauttaa minkä tahansa arvon, joka esiintyy aineistossa. Jos enemmistöarvo on olemassa, on se yksikäsitteinen. Algoritmin palauttamasta arvosta ei voi välittömästi nähdä, onko se enemmistöarvo vai onko niin, että millään arvolla ei ole yksinkertaista enemmistöä. Tämän voi kuitenkin selvittää helposti ja nopeasti laskemalla, montako kertaa palautettu arvo esiintyy aineistossa. Tällaisen algoritmin suunnitteleminen on hauska tehtävä niille, jotka pitävät pikkunäppärien algoritmien suunnittelemisesta eivätkä tunne ratkaisua ennalta. Jos kuulut heihin, älä lue kohta tulevien kolmen tähden ohi ennen kuin olet yrit1 J:n tänyt! Paras ratkaisu on niin nopea, että aineistoa ei ehdi laittaa suuruusjärjestykseen siinä ajassa. Tehtävään ei kannata suhtautua pelkkänä akateemisena kuriositeettina. Marraskuussa 2009 julkaistiin uutispalstalla comp.theory otsikolla ”Math/CompSci Interview Question — Thoughts?” viesti, jossa ohjelmistotuotannon alan työpaikkaa isosta monikansallisesta yrityksestä hakenut kertoi saaneensa työhönottohaastattelussa ratkaistavakseen seuraavan tehtävän (suomennos minun): On annettu 1 000 000 32-bittistä kokonaislukua sisältävä taulukko. Yksi kokonaislukuarvo x esiintyy siinä ainakin 500 001 kertaa. Laadi algoritmi x:n löytämiseksi. Tehokkuudesta saa lisäpisteitä. Viestistä virinneessä keskustelussa esitettiin muutama erilainen ratkaisu ja pohdittiin, mihin haastattelija pyrki kysymyksellään. Netti ei olisi netti, ellei siellä olisi myös sivua, johon on koottu vastauksia työpaikkahaastatteluissa esiintyneisiin kysymyksiin. Enemmistöarvon löytäminen löytyy sieltäkin [7]. * * * Parhaan tunnetun enemmistöarvon löytämisalgoritmin keksivät Robert S. Boyer ja J Strother Moore1 1980. He saivat kirjoituksensa julkaistua vasta 1991 [4], mutta tieto heidän keksinnöstään levisi sitä ennen ja löytyi oppikirjasta jo 1986 [2]. Algoritmi on esitetty kuvassa 1. Se käy aineiston kerran läpi ylläpitäen arvausta ja laskuria. Ensimmäiseksi arvaukseksi otetaan aineiston ensimmäinen arvo. Jos vastaan tuleva arvo on sama kuin arvaus, niin laskuria kasvatetaan yhdellä. Muussa tapauksessa laskuria vähennetään yhdellä, paitsi sen ollessa nolla sitä ei vähennetä, perässä ei ole pistettä, koska J ei ole lyhenne vaan ensimmäinen etunimi kokonaisuudessaan. Valmari 35 laskuri := 0 for i := 1 to n do if laskuri = 0 then arvaus := A[i]; laskuri := 1 else if A[i] = arvaus then laskuri := laskuri + 1 else laskuri := laskuri − 1 Kuva 1: Enemmistöarvon löytämisalgoritmi. vaan vastaan tuleva arvo otetaan uudeksi arvaukseksi ja laskuri kasvatetaan ykköseen. Algoritmin toiminnan oikeellisuus ei ole aivan itsestään selvä, joten se ansaitsee tulla todistetuksi. Lause 1 Olkoon a muuttujan arvaus arvo kuvan 1 algoritmin lopetettua. Joko a esiintyy taulukossa A yli n/2 kertaa tai mikään arvo ei esiinny taulukossa A yli n/2 kertaa. Jos n ≥ 1, niin a esiintyy taulukossa A. Todistus. Koska laskuria ei vähennetä sen ollessa nolla, on laskurin arvo aina nolla tai positiivinen. Tarkastellaan for-silmukan jokaisen kierroksen alussa funktiota f (x), joka määritellään seuraavasti: laskuri, jos x = arvaus f (x) = −laskuri, muutoin. Ensimmäisellä kerralla arvaus on määrittelemätön, mutta se ei haittaa, koska silloin laskuri = 0 ja funktion määritelmän molemmat vaihtoehdot tuottavat jokaisella x saman tuloksen 0. Jos suoritetaan ensimmäinen thenhaara, niin f (A[i]) muuttuu arvosta 0 arvoon 1 ja muilla x f (x) muuttuu arvosta 0 arvoon −1. Keskimmäisen haaran tapauksessa f (A[i]) kasvaa yhdellä ja muut f (x) vähenevät yhdellä. Myös viimeisessä haarassa f (A[i]) kasvaa yhdellä, koska silloin A[i] 6= arvaus ja f (A[i]) = −laskuri. Lisäksi f (arvaus) vähenee ja muut f (x) kasvavat yhdellä. Yhteenvetona saadaan, että jos A[i] = x, niin f (x) kasvaa yhdellä riippumatta siitä, mikä if-lauseen kolmesta haarasta suoritetaan; ja jos A[i] 6= x, niin f (x):n arvo voi kasvaa yhdellä tai vähentyä yhdellä. Jos x on enemmistöarvo, niin kasvukertoja ovat ainakin ne yli n/2 kertaa, jolloin A[i] = x. Kasvukertoja on siis enemmän kuin vähenemiskertoja. Näin ollen, ja koska f (x) aloittaa nollasta, on algoritmin lopussa f (x) > 0. Jos silloin olisi f (x) = −laskuri, niin olisi laskuri < 0. Mutta näin ei voi olla, koska aikaisemmin todettiin, että aina laskuri ≥ 0. Jäljelle jää vain f :n määritelmän vaihtoehto f (x) = laskuri ja x = arvaus. Olemme osoittaneet, että jos x on enemmistöarvo, niin algoritmin lopussa arvaus = x. Lisäksi algoritmilta vaadittiin, että jos enemmistöarvoa ei ole ja n ≥ 1, niin lopussa arvaus on jokin aineistossa esiintyvä arvo. Tämä on helppo nähdä todeksi kuvasta 1. 2 Enemmistöarvon löytämisalgoritmi ei tee muuta kuin selaa aineiston läpi täsmälleen kerran ja jokaisen arvon kohdalla se tekee kolme yksinkertaista toimintoa. Apumuistia se tarvitsee kaksi kokonaislukumuuttujaa vastauksen esittämiseen käytettävän muuttujan lisäksi. Se on siis melkein niin yksinkertainen, nopea ja muistipihi kuin taulukkoa käsittelevä algoritmi voi ylipäätään olla. Silti se ratkaisee haastavan tehtävän. Näissä suhteissa se on algoritmisuunnittelijan unelma. Enemmistöarvon löytämisalgoritmista ei välittömästi näe, että se tuottaa tulokseksi sen mitä pitää. Tämä ei ole pelkäs- 36 Kuinka kiltti mutta tarpeeton pikku algoritmi sai tärkeän tehtävän Kuva 2: Noppapeli, jossa on pitkä turvallinen sekä lyhyt vaarallinen reitti. tään myönteinen asia, mutta toisaalta se on merkki siitä, että algoritmi ei ole itsestään selvä. Koska se on yksinkertainen ja tehokas mutta ei itsestään selvä, saavat jotkut tutkijat, kuten minä, siitä älyllistä mielihyvää ja kutsuvat sitä elegantiksi. Näin ollen sitä on käytetty esimerkkinä oppikirjoissa [2, kohta 4.3.3], [11, luku 18]. Se on eräs ensimmäisiä koneellisesti oikeaksi todistettuja algoritmeja, sillä sen keksijät olivat myös erään kuuluisan varhaisen automaattisen teoreemantodistimen laatijoita ja todistivat sillä algoritminsa Fortran-kielisen toteutuksen oikeaksi vuonna 1980 [4]. Algoritmia on käytetty esimerkkinä jopa Helsingin yliopistossa tehdyssä filosofian väitöskirjassa [12]. Ja, kuten edellä todettiin, enemmistöarvon löytämistehtävällä on testattu työnhakijoita. Kun näiden kehujen jälkeen aletaan miettiä, mihin enemmistöarvon löytämisalgoritmia voitaisiin käytännössä hyödyntää, niin tunnelma lässähtää kuin pannukakku. Tehtävän englanninkielinen nimi ”majority vote/voting” luo mielikuvan vaaleista. Algoritmin tuottama informaatio ei kuitenkaan riitä todellisissa vaaleissa, vaan tarvitaan myös kunkin ehdokkaan äänimäärä, ja ehdokkaat on asetettava niiden mukaan järjestykseen. Nämä on helppo laskea muilla keinoin, jos ehdokkaita on ennalta tunnettu pienehkö joukko. Niinhän todellisissa vaaleissa on. Laskenta muuttuu millään lailla vai- keaksi vasta kun aineisto on poimittu suuresta perusjoukosta. Silloinkaan ei yleensä riitä löytää enemmistöarvoa, vaan halutaan tietää eniten ääniä saanut arvo siinäkin tapauksessa, että se ei yksinään muodosta enemmistöä. Toisaalta, jos aineisto järjestetään suuruusjärjestykseen, niin on helppo selvittää kunkin arvon esiintymiskertojen määrä laskemalla monestiko se toistuu peräkkäin. Vaikka järjestäminen on hitaampaa kuin enemmistöarvon löytämisalgoritmin suoritus, on se riittävän nopeaa useimpiin käytännön tarpeisiin. Näistä syistä enemmistöarvon löytämisalgoritmille on jokseenkin mahdoton löytää hyödyllistä käyttöä. Niin ainakin uskoin 21. elokuuta 2009 asti. 3 Markovin ketjut Noppapelissä syntyy silloin tällöin kuvan 2 kaltainen tilanne. Nykyisestä paikasta maaliin on kaksi vaihtoehtoista reittiä: pitkä mutta turvallinen sekä lyhyt, jossa on vaarallinen paikka, johon pelaaja ei halua osua. Voidakseen tehdä viisaan valinnan pelaaja haluaa tietää, millä todennäköisyydellä hän osuu vaaralliseen paikkaan, jos hän valitsee lyhyen reitin. Jos pelaaja on esimerkiksi neljän askeleen päässä vaarallisesta paikasta, niin seuraavalla heitolla hän pääsee sen ohi todennäköisyydellä 13 (silmäluvut 5 ja 6). Todennäköisyydellä 16 hän osuu siihen seuraavaksi (silmäluku 4). Muutoin hän jää yhden, kahden tai kol- Valmari 37 1/3 6 1/3 5 1/3 1/3 4 1/3 1/3 1/3 1/3 3 1/3 1/3 1/3 2 1/3 1/3 1/3 1 2/3 1/3 V 1 T 1 1/3 Kuva 3: Yksinkertaistettua noppapelin tilannetta vastaava Markovin ketju. men askeleen päähän siitä, missä kunkin vaihtoehdon todennäköisyys on 61 . Olkoon vaaralliseen paikkaan n askelta, ja tarkoittakoon pn todennäköisyyttä osua siihen ennemmin tai myöhemmin. Kun n < 0, on vaarallinen paikka ohitettu, joten pn = 0. Kun n = 0, ollaan vaarallisessa paikassa, joten p0 = 1. Kun n > 0, niin pn lasketaan summana niistä kuudesta tapauksesta, jotka vastaavat seuraavan nopanheiton silmälukuja 1, 2, . . . , 6: pn = 1 6 ∑ pn−i . 6 i=1 Nämä säännöt voi esittää kuvan 3 kaltaisella piirroksella. Oikein piirrettynä siitä tulisi kamalan sotkuinen, joten sitä on yksinkertaistettu kuvittelemalla, että arpanopassa olisi vain kolme tahkoa silmäluvuiltaan 1, 2 ja 3. Piirrosta on yksinkertaistettu myös esittämällä negatiiviset askelmäärät (eli n < 0) yhdellä ympyrällä, johon on kirjoitettu ”T” tarkoittamaan turvallista paikkaa. Vaarallinen paikka on merkitty kirjaimella ”V”, ja ympyröihin kirjoitetut numerot tarkoittavat etäisyyttä vaaralliseen paikkaan. Kaarten varrelle kirjoitetut luvut ilmaisevat siirtymien todennäköisyydet. Jos todennäköisyys on nolla, niin vastaava kaari jätetään piirtämättä. Meillä on siis järjestelmä, jossa on tiloja (kuvan ympyrät) sekä tunnetulla todennäköisyydellä tapahtuvia siirtymiä tilasta toiseen (kuvan nuolet). Tällaista järjestelmää sanotaan (diskreetin ajan) Markovin ketjuksi venäläisen matemaatikon Andrei Markovin mukaan, joka eli 1856–1922 [3, s. 873]. On olemassa myös jatkuvan ajan Markovin ketjuja. Ne ovat hankalampia selittää, mutta onneksi tässä kirjoituksessa tarvitaan niistä vain kahta tietoa: aivan kohta käsiteltävä samanveroisten tilojen yhdistäminen koskee niitäkin, ja niiden siirtymiin merkityt luvut voivat saada myös negatiivisia arvoja. Markovin ketjuista voi ratkaista numeerisesti monenlaisia asioita koskien tutkittavaa järjestelmää. Isojen Markovin ketjujen käsittely on valitettavasti työlästä. Tästä syystä on kehitetty keinoja pienentää Markovin ketjuja tunnistamalla tietyssä mielessä samanveroisia tiloja ja sulauttamalla ne yhteen. Kuvan 3 tila T on niiden tilojen yhteensulautuma, jotka vastaavat etäisyyden negatiivisia arvoja. Kuva 4 havainnollistaa tilojen yhdistämistä yleisemmin. Sen Markovin ketjussa on kolmenlaisia tiloja. Ne on merkitty kirjaimilla ”A”, ”B” ja ”C”. Ajatuksena on, että tilat on merkitty eri kirjaimilla jos ja vain jos niillä on jokin välittömästi merkityksellinen ero. Esimerkiksi noppapelissä vaarallinen tila on välittömästi erilainen kuin muut tilat: pelätty uhka on toteutunut, kun muissa tiloissa on ainakin toivoa, että se vältetään. Tilojen tällaista luokittelua kutsutaan alkujaoksi. Kuvan 4 vasemmanpuoleisessa piirroksessa on huomattu, että kummastakin harmaalla viivalla ympyröidystä tilasta päästään seuraavaksi B-merkittyyn tilaan todennäköisyydellä 0,9 ja C-merkittyyn tilaan todennäköisyydellä 0,1, ja muualle kummastakaan ei pääse. Niillä on siis sa- Kuinka kiltti mutta tarpeeton pikku algoritmi sai tärkeän tehtävän 38 A 0,5 0,5 A A 0,2 0,1 0,9 A 0,4 0,4 0,2 0,8 A 1 0,9 B 0,1 A 0,1 A 0,5 1 0,9 A 0,5 A C 0,2 0,1 0,9 A 0,4 0,4 0,2 0,8 A 0,9 0,1 1 B 1 0,9 A 0,1 C Kuva 4: Tilojen yhdistäminen Markovin ketjussa. mat mahdolliset tulevaisuudet samoilla todennäköisyyksillä. Niillä ei myöskään ole välitöntä eroa, koska ne on merkitty samalla kirjaimella ”A”. Ne ovat siis kaikkien olennaisten asioiden kannalta samanveroiset, joten ne voi yhdistää yhdeksi tilaksi. Kuvan oikeanpuoleisessa piirroksessa on kaksi muuta tilaa todettu samanveroisiksi. Niistä toisesta lähtee kaksi siirtymää todennäköisyydellä 0,4 ja toisesta yksi todennäköisyydellä 0,8, mutta se ei haittaa, koska ne vievät samanveroisiksi jo todettuihin tiloihin. Siksi kummastakin päästään samalla todennäköisyydellä 0,8 samanlaiseen vaihtoehtoisten tulevaisuuksien kokonaisuuteen. Lisäksi kummastakin pääsee toiseensa samalla todennäköisyydellä 0,2. Mitään eroa ei voi havaita heti eikä myöhemmin. Sen sijaan vasemmanpuoleisia tiloja ei voi yhdistää. Ne eroavat toisistaan sen todennäköisyyden suhteen, jolla päästään äsken yhdistettyihin tiloihin. Toiselle niistä tämä todennäköisyys on 0,5 ja toiselle 0,9. Edellä aloitimme alkuperäisellä Markovin ketjulla, tunnistimme samanveroisia tiloja ja yhdistimme ne. Tämä toimintaperiaate joutuu vaikeuksiin oheisen Markovin ketjun tapauksessa. Sen kaikki tilat voi yhdistää, mutta mistä nähdään, että niin on? 0,6 0,7 A 0,3 A 1 0,4 A 1 A Tämän vuoksi tiloja yhdistävät algoritmit toimivat toisella tavalla. Luvussa 5 katsotaan, miten asia on hoidettu eräässä toisessa yhteydessä. Ennen sitä on tutustuttava erääseen tietorakenteeseen. 4 Hienonnettavan osituksen tietorakenne Joukon ositus on sen jako osiksi siten, että jokainen alkio kuuluu tasan yhteen osaan, eikä mikään osa ole tyhjä. Ositus on sopiva käsite esittämään tilannetta, jossa käsiteltävät kohteet on jaettu ryhmiin jonkin yhdistävän tai erottavan tekijän mukaan. Joukon {1, 2, . . . , n} ositus voidaan esittää kahden taulukon avulla: luku ja alku. Samaan osaan kuuluvat luvut ovat peräkkäin taulukossa luku, ja alku[i] ilmaisee kohdan, josta osan i luvut alkavat. Ositus {2, 4}, {6, 1, 5, 8}, {3}, {7, 9} voidaan esittää seuraavilla taulukoilla: luku alku = = [2, 4, 6, 1, 5, 8, 3, 7, 9] [1, 3, 7, 8] Olettakaamme, että osa {6, 1, 5, 8} halutaan halkaista kahdeksi osaksi {1, 5} ja {6, 8}. Tämä onnistuu ryhmittelemällä osan alue taulukossa luku uudelleen ja lisäämällä uusi osan alkukohta: luku alku = = [2, 4, 1, 5, 6, 8, 3, 7, 9] [1, 3, 5, 7, 8] Tämän toteuttamisessa tehokkaasti on ongelma. Uuden osan alkukohta lisättiin keskelle taulukkoa alku, jolloin sen loppuosan sisältö [7, 8] jouduttiin siirtämään askeleen verran oikealle. Jos loppuosa on Valmari 39 iso, menee siirtämiseen paljon aikaa. Oli- on osa, joka ilmoittaa kullekin luvulle sen si nopeampaa lisätä uuden osan alkukohta osan numeron, johon luku kuuluu. taulukon alku loppuun seuraavasti: Kuva 5 näyttää toimintojen toteutukset. Toiminnossa merkitse oleva testi estää luku = [2, 4, 1, 5, 6, 8, 3, 7, 9] merkitsemästä uudelleen lukua, joka on alku = [1, 3, 7, 8, 5] jo merkitty. Luku merkitään vaihtamalla Valitettavasti tämä tekee osien loppu- sen paikkaa alkupuolikkaan ja loppupuokohtien löytämisen hankalaksi. Aikaisem- likkaan välisen rajan kohdalla olevan lumin osan ℓ loppukohta voitiin selvittää vun kanssa ja kasvattamalla rajaa yhdellä. katsomalla alku[ℓ + 1], paitsi viimeisen Lukujen uudet paikat täytyy päivittää tauosan tapauksessa, mutta sen loppukoh- lukkoon paikka. Jos siinä osassa, johon ludaksi tiedetään koko taulukon loppukohta. ku i kuuluu, ei aikaisemmin ollut merkitNyt osan 2 loppukohtaa ei voi löytää si- tyjä lukuja, niin merkitse(i) palauttaa sen ten. Tämän ratkaisemiseksi otetaan käyt- osan numeron. Muussa tapauksessa se patöön kolmas taulukko loppu, jonka sisäl- lauttaa nollan. Paluuarvosta tulee olemaan tö ilmaisee osien loppukohdat. Loppukoh- hyötyä jatkossa. Merkitse ei sisällä silmukoita eikä kutdaksi voitaisiin antaa osan viimeisen luvun paikka, mutta myöhemmin esitettä- su muita toimintoja eikä itseään, joten se vien asioiden kannalta on luontevampaa, toimii vakioajassa. että loppukohdaksi annetaan sitä ykkösen Koska merkitse vaihtaa lukujen järjesverran suurempi luku. tystä, on tärkeää, että merkitsemisiä ei tehdä kesken osan sisällön selaamisen. Muuluku = [2, 4, 1, 5, 6, 8, 3, 7, 9] toin selaaminen saattaa tuottaa joitakin lualku = [1, 3, 7, 8, 5] kuja monesti ja jättää toisia tuottamatta. loppu = [3, 5, 8, 10, 7] Tietorakenteen käyttäjien on otettava tämä huomioon. Osan halkaisemiseksi on tavalla tai toiToiminnon halkaise kaksi ensimmäissella ilmaistava, mitkä luvut menevät ensimmäiseen ja mitkä toiseen puolikkaa- tä riviä huolehtii, että toiminto ei tee tyhseen. Riittää kertoa ensimmäiseen puo- jää osaa. Jos osan kaikki luvut on merkitlikkaaseen menevät luvut. Otamme tätä ty, sijoitus keski[ℓ] := alku[ℓ] muuttaa ne varten käyttöön toiminnot merkitse(i) ja merkitsemättömiksi. Halkaise pyyhkii aihalkaise(ℓ), missä i on luku ja ℓ on osan na kaikki vanhat merkinnät, jotta ne einumero. Aikaisemman esimerkin osa 2, vät häiritsisi myöhempiä merkitsemisiä ja joka sisältää {6, 1, 5, 8}, saadaan halkais- halkaisemisia. Kolmas rivi kasvattaa osien määrää ja tua suorittamalla merkitse(1), merkitse(5) ottaa käyttöön uuden osanumeron u. Nelja halkaise(2). Toiminnon merkitse(i) on löydettävä jäs rivi valitsee pienemmän puolikkaan. luku i taulukosta luku. Tätä varten otetaan Numero u annetaan sille, ja suurempi puokäyttöön neljäs taulukko paikka. Jotenkin likas säilyttää vanhan numeron ℓ. Suuon pidettävä kirjaa, mitkä luvut on mer- rin osa jäljellä olevista riveistä päivittää kitty. Tämä voidaan tehdä jakamalla osa osien alku-, keski- ja loppukohdat vastaaalkupuolikkaaseen, joka sisältää merkityt maan uutta ositusta. For-silmukka merkitluvut ja loppupuolikkaaseen, joka sisältää see uuteen osaan kuuluville luvuille, että muut. Niiden välisen rajan ilmaisee uusi ne kuuluvat siihen. taulukko keski. Viimeinen uusi taulukko Halkaise palauttaa joko toisen puolik- 40 Kuinka kiltti mutta tarpeeton pikku algoritmi sai tärkeän tehtävän merkitse(i) ℓ := osa[i]; k := keski[ℓ] p := paikka[i] if p ≥ k then luku[p] := luku[k] paikka[ luku[p] ] := p luku[k] := i paikka[i] := k keski[ℓ] := k + 1 if k = alku[ℓ] then return ℓ return 0 halkaise(ℓ) if keski[ℓ] = loppu[ℓ] then keski[ℓ] := alku[ℓ] if keski[ℓ] = alku[ℓ] then return 0 osia := osia + 1; u := osia if keski[ℓ] − alku[ℓ] ≤ loppu[ℓ] − keski[ℓ] then alku[u] := alku[ℓ]; loppu[u] := keski[ℓ] alku[ℓ] := keski[ℓ] else alku[u] := keski[ℓ]; loppu[u] := loppu[ℓ] loppu[ℓ] := keski[ℓ]; keski[ℓ] := alku[ℓ]; ℓ := u keski[u] := alku[u] for p := alku[u] to loppu[u] − 1 do osa[ luku[p] ] := u return ℓ Kuva 5: Merkitse(i) ja halkaise(ℓ). kaan numeron tai luvun 0 merkiksi siitä, että ei halkaistu, koska toisesta puolikkaasta olisi tullut tyhjä. Palautettu puolikkaan numero on sen puolikkaan numero, johon merkitsemättä jääneet luvut kuuluvat. Tästä paluuarvosta on hyötyä luvun 8 algoritmin toteutuksessa. Käyttäjä tietää molempien puolikkaiden numerot ilman paluuarvoakin, koska ne ovat kutsussa annettu numero ja osia, mutta ilman paluuarvoa hän ei tietäisi, kumpi puolikas sisältää ennen kutsua merkityt luvut. Toiminnon halkaise suoritusaika on for-silmukan vuoksi suoraan verrannollinen pienemmän puolikkaan kokoon. Halkaise olisi ollut yksinkertaisempi toteuttaa siten, että jos tehdään uusi osa, niin se tehdään aina merkityistä luvuista. Silloin suoritusaika olisi suoraan verrannollinen merkittyjen lukujen määrään eikä pienemmän puolikkaan kokoon. Tämä ei ole olennainen ero, koska merkitsemiset ja halkaiseminen vievät joka tapauksessa yhteensä aikaa vähintään verrannollisesti merkittävien lukujen määrään. Sen sijaan olennaista on, että kummallakaan toteutuksella aikaa ei kulu verrannollisesti isomman puolikkaan kokoon siinä tapauk- sessa, että se koostuu merkitsemättömistä luvuista, koska se voi olla olennaisesti suurempi kuin merkittyjen lukujen määrä. Monimutkaisemman toteutuksen valitsemisen syy ei siis ole suoritusajassa. Todellinen syy on, että se mahdollistaa luvussa 5 kerrottavan parannuksen eräisiin algoritmeihin. Sain suuren osan tämän tietorakenteen ideoista Timo Knuutilan kirjoituksesta [10]. 5 Determinististen äärellisten automaattien minimointi Deterministinen äärellinen automaatti on yksi teoreettisen tietojenkäsittelytieteen tärkeimpiä peruskäsitteitä. Se muistuttaa edellä käsiteltyä Markovin ketjua, mutta siinä on muutamia olennaisia eroja. Siirtymiin ei ole liitetty todennäköisyyksiä vaan nimiä, jotka on poimittu joukosta, jota kutsutaan aakkostoksi. ”Deterministinen” tarkoittaa, että samasta tilasta ei voi olla useampia kuin yksi siirtymä samalla nimellä. Tiloista yksi on asetettu erikoisasemaan alkutilana ja nolla tai useampia tiloja on nimetty lopputiloiksi. Kuvassa 6 on esimerkki, jossa alkutila on osoitettu tyh- Valmari jästä alkavalla nuolella ja lopputilat kaksinkertaisilla ympyröillä. Siirtymien nimiä on mielekästä ajatella kirjaimina. Jokainen alkutilasta siirtymiä pitkin johonkin lopputilaan kulkeva reitti määrittelee sanan, joka saadaan luettelemalla siirtymien nimet peräkkäin siinä järjestyksessä kuin ne kohdattiin. Tällä tavalla jokainen deterministinen äärellinen automaatti määrittelee jonkin joukon sanoja. Klassinen deterministisiin äärellisiin automaatteihin liittyvä tehtävä on löytää mahdollisimman pieni automaatti, joka määrittelee täsmälleen saman joukon sanoja kuin annettu automaatti. Sitä kutsutaan deterministisen äärellisen automaatin minimoimiseksi. Sellaiset tilat ja siirtymät, joihin alkutilasta ei pääse siirtymiä pitkin, eivät vaikuta deterministisen äärellisen automaatin määrittelemien sanojen joukkoon. Minimoinnissa ne tulee poistaa. Sama pätee tiloihin ja siirtymiin, joista ei pääse mihinkään lopputilaan, paitsi alkutilaa ei saa poistaa. Poistaminen onnistuu tavallisilla graafihakualgoritmeilla. Poistovaihe on niin ilmeinen, että osa kirjoittajista ei edes mainitse sitä [8, 9]. Varsinainen haaste minimoinnissa on siihen sisältyvä tilojen yhdistämistehtävä. Se on melko samanlainen kuin edellä esitelty Markovin ketjun tilojen yhdistämistehtävä. John E. Hopcroftin tulos vuodelta 1971 [9] on keskeinen tällä alalla.2 Siihen on hiljattain saatu parannuksia, joiden vuoksi asiaa kannattaa selostaa laajemmin kuin kiltin pikku algoritmin tarina välttämättä vaatisi. Hopcroftin minimointialgoritmi perustuu tilojen ryhmittelyyn epätyhjiksi lohkoiksi, joita halkaistaan tietyn säännön mukaan pienemmiksi lohkoiksi kunnes ne lakkaavat pilkkoutumasta. Hän käytti loh- 41 kojen esittämiseen kaksisuuntaisiin linkitettyihin listoihin perustuvaa tietorakennetta. Siihen voidaan yhtä hyvin, ellei paremminkin, käyttää luvussa 4 kuvattua tietorakennetta. Aluksi on korkeintaan kaksi lohkoa: lopputilat ja muut tilat. Alkuperäisiä lohkoja on yksi siinä tapauksessa, että kaikki tilat ovat lopputiloja sekä siinä tapauksessa, että lopputiloja ei ole ollenkaan. Tavoitteena on saavuttaa mahdollisimman vähällä pilkkomisella tilanne, jossa jokainen lohko on yhteensopiva jokaisen aakkosen a suhteen. Yhteensopivuus tarkoittaa, että lohkon tilat ovat keskenään samaa mieltä siitä, lähteekö niistä siirtymää, jonka nimi on a, ja jos lähtee, niin mihin lohkoon kuuluvaan tilaan se vie. Kun yhteensopivuus on saavutettu, saadaan minimoitu automaatti yhdistämällä kunkin lohkon tilat yhdeksi tilaksi. Halkaisemisia halutaan tehdä mahdollisimman vähän, jotta minimoidussa automaatissa olisi mahdollisimman vähän tiloja. Voidaan todistaa, että tällä tavalla saatu automaatti määrittelee saman joukon sanoja kuin alkuperäinen automaatti ja on pienin mahdollinen. Tarkastellaan esimerkkinä kuvan 6 determinististä äärellistä automaattia. Alkuperäiset lohkot ovat {1, 2, 3, 4, 5, 6} ja {7, 8}. Lohko {7, 8} on yhteensopiva sekä aakkosen 0 että aakkosen 1 suhteen. Lohko {1, 2, 3, 4, 5, 6} on yhteensopiva 0:n suhteen, mutta ei 1:n suhteen, koska tilasta 6 ei ole mutta muista on 1-siirtymä lohkoon itseensä. Niinpä {1, 2, 3, 4, 5, 6} on halkaistava osiksi {1, 2, 3, 4, 5} ja {6}. Koska tila 6 muodostaa nyt oman lohkonsa, on {1, 2, 3, 4, 5} yhteensopimaton 1:n suhteen, joten se on halkaistava lohkoiksi {1, 2, 3, 4} ja {5}. Samasta syystä tilat 4, 3 ja 2 on erotettava omiksi lohkoikseen 2 On myös väitetty, että [9] olisi vuodelta 1970. Löysin sen skannattuna netistä ja sen kannessa lukee ”January, 1971”. Kuinka kiltti mutta tarpeeton pikku algoritmi sai tärkeän tehtävän 42 1 1 2 1 3 0 0 0 7 1 4 1 0 1 8 5 0 1 6 0 1 Kuva 6: Deterministinen äärellinen automaatti, jota ei kannata minimoida ”etuperin”. kukin vuorollaan. Tilat 7 ja 8 pysyvät loppuun asti yhdessä. Tuntuisi luonnolliselta halkaista lohko käymällä sen tilat läpi ja katsomalla, pääseekö niistä samalla aakkosella samaan lohkoon. Esimerkissä se tarkoittaa, että käydään läpi tilat 1, 2, 3, 4, 5 ja 6; vähän myöhemmin tilat 1, 2, 3, 4 ja 5; sitten 1, 2, 3 ja 4; ja niin edelleen. Jos ylärivissä on n− 2 tilaa, käydään läpi (n− 2)+. . .+ 2+ 1 = 12 (n2 − 3n + 2) tilaa. Näin toimivan algoritmin ajan kulutuksen kertaluokka aakkoston koolla kaksi ei siis voi olla parempi kuin O(n2 ), missä n on tilojen määrä. Hopcroft huomasi, että jos työ järjestetään toisin, voidaan ajan kulutuksen kertaluokaksi aakkoston koolla kaksi saada O(n log n). Se on selvästi edellistä parempi. Gries selkeytti Hopcroftin algoritmia ja laski, että jos aakkoston koko on α, on sen ajan kulutus O(αn log n) [8]. Algoritmi jäi Griesinkin jäljiltä vaikeatajuiseksi ja Knuutila on selkeyttänyt sitä edelleen [10]. Tässä luvussa esitetään algoritmista muunnos, jonka ajan kulutus on parempi kuin minkään ennen vuotta 2008 esitetyn ja joka on luultavasti yksinkertaisempi kuin mikään koskaan aikaisemmin esitetty. Sen ajan kulutus on O(m log n), missä m on siirtymien määrä. Tämä on parannus, koska m ≤ αn ja usein m on paljon pienempi kuin αn. Griesin algoritmissa valitaan yksi lohko L ja aakkonen a pilkkojaksi. Jokaisen lohkon tilat jaetaan kahteen ryhmään: niihin, joista on a-niminen siirtymä L:ään kuuluvaan tilaan ja niihin, joista ei ole. Jos kumpikin ryhmä on epätyhjä, halkaistaan lohko ryhmien mukaisesti. Sitten valitaan toinen pilkkoja ja sama toistetaan. Tätä jatketaan, kunnes mikään lohko ei enää halkea pienemmiksi osiksi. Pilkkojaa käytettäessä ei käydä läpi halkaistavan lohkon tiloja vaan pilkkojan lohkon tilat. Niihin saapuvat pilkkojan aakkosella nimetyt siirtymät kuljetaan takaperin. Siirtymien alkupäiden tilat merkitään luvun 4 toiminnolla merkitse tai jollakin sen tapaisella. Niiden lohkojen numerot, joiden tiloja merkittiin, kootaan johonkin tietorakenteeseen H. Luvussa 4 todettiin, että merkitse palauttaa lohkon numeron vain kun lohko kohdataan ensimmäisen kerran. Tämän ansiosta on helppo välttää saman numeron lisääminen H:hon moneen kertaan, ja H voidaan toteuttaa hyvin yksinkertaisesti esimerkiksi pinona. Kun merkitsemisvaihe on valmis, halkaistaan ne lohkot, joiden numerot ovat H:ssa, toiminnolla halkaise tai vastaavalla, sikäli kuin ne halkeavat. Luvun 4 halkaise huolehtii, että jos lohkon kaikki tilat merkittiin, niin lohkoa ei halkaista. Tavoitellun nopeushyödyn kannalta on tärkeää, että halkaise ei käy läpi enempää tiloja kuin merkittiin. Luvussa 4 huolehdittiin siitäkin. Edellä olleessa esimerkissä lohkon {1, 2, 3, 4, 5, 6} läpikäynti aakkosella 1 löytää tilat 1, 2, 3, 4 ja 5 mutta ei tilaa 6, joten lohko halkeaa osiksi {1, 2, 3, 4, 5} ja {6}. Sen jälkeen lohkon {6} läpikäynti aakkosella 1 löytää vain tilan 5, joka Valmari näin ollen erotetaan omaksi lohkokseen. Lohkon {5} ja aakkosen 1 käyttö pilkkojana erottaa tilan 4 omaksi lohkokseen ja niin edelleen, kunnes ylärivi on pilkkoutunut yhden tilan lohkoiksi. Ylärivi pilkkoutui erillisiksi tiloiksi käymällä läpi vain 2n − 5 tilaa. 43 Hopcroftilla oli jokaista aakkosta kohti lista niiden lohkojen numeroista, joita täytyy käyttää jatkossa pilkkojina kyseisen aakkosen kanssa. Aina kun lohko halkesi, selvitettiin kaksiosaisella testillä, kumman osan numero lisätään listaan. Myöhemmillä kirjoittajilla on ollut vaihtelevia Äskeisen esimerkin menestys on tietorakenteita ja testejä samaan tarkoikiinni siitä, että isoja lohkoja, kuten tukseen, myös muissa pilkkomissovelluk{1, 2, 3, 4, 5}, ei alun jälkeen tarvinnut sissa kuin deterministisen äärellisen autokäyttää pilkkomiseen ennen kuin ne oli- maatin minimoinnissa [1, 5, 6, 8, 10, 13, vat itse pilkkoutuneet pieniksi. Oliko tämä 14, 15, 16]. Minkäänlaista tietorakennetvain hyvää tuuria? Ei ollut, vaan takana on ta ja siihen liittyviä testejä ei kuitenkaan yleinen periaate. Hopcroft huomasi, että tarvita! Tämä osoitetaan seuraavaksi ja se jos lohkoa L on jo käytetty pilkkomiseen lienee ennen julkaisematon havainto. ja se jakaantuu lohkoiksi L1 ja L2 , niin Kutsuttakoon jatkossa käytettävien jatkossa riittää käyttää toista niistä pilkko- lohkojen, pilkkojien tms. numeroiden miseen. joukkoa työjoukoksi, ja puhukaamme ykIlmiöstä saa käsityksen vertaamalla sinkertaisuuden vuoksi vain lohkoista. Jos lohkojen {1, 2, 3, 4, 5} ja {6} halkaise- haljenneen lohkon numero ei ollut työjoumisvaikutuksia aakkosella 1. Lohko {6} kossa, niin työjoukkoon täytyy lisätä pieaiheuttaa tilan 5 merkitsemisen ja sitä nemmän osan numero. Jos se oli työjoukautta lohkon {1, 2, 3, 4, 5} jakaantumi- kossa, niin molempien osien numeroiden sen osiksi {1, 2, 3, 4} ja {5}. Vastaavas- kuuluu päätyä työjoukkoon. Vanhan nuti {1, 2, 3, 4, 5} aiheuttaa tilojen 1, 2, 3 meron perineen osan numero — eli vanha ja 4 merkitsemisen ja sitä kautta loh- numero — on siellä jo, joten täytyy lisäkon {1, 2, 3, 4, 5} jakaantumisen osiksi tä uuden osan numero. Hopcroft ja muut {1, 2, 3, 4} ja {5}. Lopputulos on sama, tarvitsivat testejä sen selvittämiseksi, lisätäänkö pienemmän osan vai uuden osan vaikka merkityt tilat olivat erit. numero, ja kumpi osa on pienempi. Niin On helppo arvata, että jos jatkossa käynytkin tarvittaisiin, jos luvun 4 halkaise tettäväksi valitaan osista L1 ja L2 pienemantaisi uuden numeron aina merkityistä pi, niin saadaan säästöä. Säästön määrä tiloista koostuvalle osalle. Mutta koska se voi kuitenkin olla yllätys. Jos samaa siirantaa uuden numeron aina pienemmälle tymää käytetään halkaisemisessa monta osalle, sulautuvat eri tapaukset yhdeksi, kertaa, niin jokaisella kerralla sen loppueikä testejä tarvita. pään tila kuuluu lohkoon, jonka koko on enintään puolet koosta edellisellä kerralUudeksi lohkonumeroksi on helpointa la. Tästä seuraa, että samaa siirtymää voi- antaa yhtä suurempi kuin edellinen uusi daan käyttää enintään noin log2 n kertaa. numero. Lohkot saa ottaa pilkkomiskäytKoska siirtymiä on enintään αn kappa- töön työjoukosta missä järjestyksessä taletta, on tästä aiheutuva työmäärä kaiken hansa. Jos ne otetaan vanhin ensin, niin kaikkiaan O(αn log n). Jos ei valittaisi pie- työjoukossa on aina peräkkäiset numenempää osaa, niin siirtymän käyttökerto- rot alkaen ensimmäisestä käyttämättömän jen määrä voisi olla lähes n ja työmäärä lohkon numerosta ja loppuen suurimpaan Θ(αn2 ). lohkon numeroon. Tällaisen joukon tallet- 44 Kuinka kiltti mutta tarpeeton pikku algoritmi sai tärkeän tehtävän tamiseen ei tarvita erityistä tietorakennetta, vaan riittää, että kumpikin numero on kokonaislukumuuttujassa. Vielä on ratkaistava, miten löydetään tehokkaasti ne siirtymät, jotka kulloinkin on kuljettava takaperin. Tyypillinen keino on ollut liittää jokaiseen tilaan ja aakkoseen lista, joka sisältää siihen tilaan päättyvien, sillä aakkosella nimettyjen siirtymien alkupäiden tilat. Nämä listat on helppo muodostaa. Ne vievät Θ(αn) muistia. Tämä on harmillista, sillä siirtymien todellinen määrä m on usein paljon pienempi kuin αn. Vuonna 2008 julkaistiin, että muistin käyttö voidaan pudottaa kertaluokkaan O(m + α) ja ajan käyttö kertaluokkaan O(m log n) ottamalla käyttöön toinen kappale luvun 4 tietorakenteesta [16]. Siihen talletettava ositus muodostuu siirtymistä eikä tiloista. Kutsuttakoon sen osia kimpuiksi erotukseksi lohkoista. Lisäksi jokaisella tilalla on kaikkien siihen saapuvien siirtymien numeroiden lista. Se saa olla missä järjestyksessä tahansa, joten se on helppo toteuttaa. Aluksi kimput muodostetaan siten, että kaikki samannimiset siirtymät kuuluvat samaan kimppuun ja erinimiset eri kimppuihin. Tämä on vaikeampaa kuin voisi luulla, sillä järjestämiseen nimien mukaan menee tavallisilla algoritmeilla O(m log m) aikaa, joka on enemmän kuin luvattu kokonaissuoritusaika O(m log n). Koska tämän eron käytännön merkitys on pieni ja nopeampi osituskeino on varsin eksoottinen, emme esitä sitä tässä. Se löytyy julkaisusta [16]. Algoritmin edetessä kimppuja pilkotaan siten, että siirtymät, joiden loppupään tilat kuuluvat eri lohkoihin, joutuvat eri kimppuihin. Lohkoja pilkotaan siten, että tilat, joista toisesta alkaa ja toisesta ei ala siirtymä pilkkomiseen käytettävässä kimpussa, joutuvat eri lohkoihin. Pilkkomis- järjestyksellä ei ole väliä oikean lopputuloksen kannalta. Yksi lohko voidaan jättää kokonaan käyttämättä pilkkomiseen, sillä jokainen kimppu, jossa on sekä siihen päättyviä että muita siirtymiä, tulee pilkotuksi muiden siirtymiensä loppupäiden lohkojen vaikutuksesta. Algoritmi lopettaa, kun mikään lohko ja kimppu ei pilkkoudu. Kuva 7 esittää tällä tavalla toimivan algoritmin. Lohkotietorakenteen taulukoissa ja toiminnoissa on käytetty etuliitettä l_ ja kimpuille vastaavasti k_. Niinpä l_luku[i] on selattavan tilan ja k_luku[i] siirtymän numero. Muuttuja k sisältää pilkkomiseen käytettävän kimpun ja ℓ lohkon numeron. Koska yhden lohkon saa jättää käyttämättä, aloittaa ℓ arvosta 2. While-silmukoiden ehdoissa esiintyvät l_osia ja k_osia sisältävät lohkojen ja kimppujen kokonaismäärän kunakin ajanhetkenä, joten kumpikin tyypillisesti kasvaa algoritmin suorituksen aikana. Pääsilmukan vartalo pilkkoo ensin yhden kimpun mukaan ja sitten kaikkien sillä hetkellä jäljellä olevien lohkojen mukaan. Näin saadaan vähillä testeillä varmistettua, että jos aluksi on ainakin yksi kimppu, niin, kun algoritmi lopettaa, on pilkottu kaikkien kimppujen ja yhtä vaille kaikkien lohkojen mukaan. Jos aluksi ei ole yhtään kimppua, niin lohkoilla pilkkominen jää pois, mutta silloin lohkoilla ei ole mitä pilkkoa. Julkaisuissa [16, 14] pilkottiin lohkolla aina heti kun se oli haljennut. Siksi niissä tarvittiin erilliset H lohkoille ja kimpuille. Nyt lohkoilla pilkkominen alkaa vasta kun H on tyhjennetty lohkojen numeroista, joten selvitään yhdellä H. Algoritmin tähänastinen esittely keskittyi sen eroihin aikaisempiin verrattuna, joten toiminnan virheettömyys ja nopeus eivät välttämättä tulleet selviksi. Virheettömyys perustuu seuraavaan tulokseen. Valmari 45 // Aloitetaan ensimmäisestä kimpusta ja toisesta lohkosta k := 1; ℓ := 2; H := 0/ // Pääsilmukka while k ≤ k_osia do // Pilko lohkoja vuorossa olevan kimpun mukaan for i := k_alku[k] to k_loppu[k] − 1 do h := l_merkitse( alkup¨aa¨ n_tila[ k_luku[i] ] ) if h > 0 then H := H ∪ {h} for h ∈ H do l_halkaise(h) / k := k + 1 H := 0; // Pilko kimput vastaamaan äsken pilkottuja lohkoja while ℓ ≤ l_osia do for i := l_alku[ℓ] to l_loppu[ℓ] − 1 do for j ∈ tulosiirtym¨at( l_luku[i] ) do h := k_merkitse( j) if h > 0 then H := H ∪ {h} for h ∈ H do k_halkaise(h) / ℓ := ℓ + 1 H := 0; Kuva 7: Deterministisen äärellisen automaatin minimoinnin pilkkomisvaihe. Apulause 2 Seuraavat asiat pätevät aina kuvan 7 algoritmin pääsilmukan ehdon kohdalla: 1. Jos kaksi siirtymää on eri kimpuissa, niin niillä on eri nimet tai ne päättyvät eri lohkoihin. Todistus. Väitteiden 1 ja 4 ”niin”-osat eivät voimaan astuttuaan voi enää kumoutua, koska lohkoja ja kimppuja ei yhdisJos kaksi siirtymää päättyy eri lohkoi- tetä eikä siirtymien nimiä ja kytkentöjä hin, niin ne ovat eri kimpuissa tai aina- muuteta. Kokonaisuudessaan väitteet 1 ja kin toisen lohkon numero on vähintään 4 pätevät, koska ”niin”-osissa on lueteltu kaikki tilanteet, joissa algoritmi alustus ℓ. mukaan lukien voi sijoittaa tiloja eri lohJos kaksi tilaa on eri lohkoissa, niin ne koihin ja siirtymiä eri kimppuihin. Väite olivat eri lohkoissa alussa tai toisesta 2 saatetaan voimaan alustuksessa eikä voi alkaa siirtymä, joka kuuluu kimppuun, kumoutua jälkeenpäin. Väite 5 on ilmeijossa ei ole toisesta alkavaa siirtymää. nen. Vain yhden lohkon numero on alle 2. Alussa eri lohkoissa olleet tilat ovat aiVäite 3 pätee alussa, koska silloin ℓ = 2. na eri lohkoissa. Myöhemmin ”jos”-osa voi astua voimaan Jos tilasta alkaa siirtymä, joka kuuluu vain lohkon haljetessa. Silloin toinen osakimppuun, jossa ei ole toisesta tilas- lohko saa upouuden lohkonumeron, joka 2. Erinimiset siirtymät ovat eri kimpuissa. 3. 4. 5. 6. ta alkavaa siirtymää, niin tilat kuuluvat eri lohkoihin tai kimpun numero on vähintään k tai toisesta tilasta alkaa samanniminen siirtymä, ja se kuuluu kimppuun, jonka numero on vähintään k. 46 Kuinka kiltti mutta tarpeeton pikku algoritmi sai tärkeän tehtävän on suurempi kuin mikään siihenastinen ja siten suurempi kuin ℓ. Siirtymän loppupään tilan lohkon numero voi muuttua algoritmin suorituksen aikana, mutta vain upouudeksi lohkonumeroksi. Kun ℓ kasvaa, on algoritmi juuri huolehtinut, että missään kimpussa ei ole sekä lohkoon ℓ että muualle päättyviä siirtymiä. Väite 6 pätee samankaltaisista syistä kuin väite 3. Se pätee alussa, koska silloin jokaisen kimpun numero on vähintään k. Jos kimpun halkeaminen saattaa ”jos”osan voimaan, niin kimpun jompi kumpi osa saa riittävän suuren numeron. Siirtymän kimpun numero voi kasvaa mutta ei vähetä. k:n kasvaessa kaikki lohkot on juuri halkaistu kimpun k mukaisesti. 2 kimpun ja lohkon numero ei ole vähintään k ja ℓ. Väitteet 2, 3, 5 ja 6 supistuvat silloin siten, että ne tuottavat väitteille 1 ja 4 vastakkaissuuntaiset väitteet. Kuulukoon kaksi tilaa samaan lohkoon ja olkoon toisesta jonkin niminen siirtymä johonkin lohkoon. Väitteen 4 negaation vuoksi toisestakin on samaan kimppuun kuuluva siirtymä. Sillä on väitteen 1 negaation vuoksi sama nimi ja määränpäälohko kuin ensin mainitulla. Pilkkominen on siis saatettu loppuun eli yhteensopivuus on saavutettu. 2 Seuraava lause ilmaisee, että algoritmi saavuttaa aiemmin asetetun yhteensopivuustavoitteen. Tästä voidaan todistaa, että jos algoritmiin lisätään edellä mainittu poistovaihe, niin sen lopputulos on pienin mahdollinen automaatti, joka määrittelee saman joukon sanoja kuin syötteeksi annettu automaatti. Jätämme todistuksen esittämättä, koska se on puhdasta matematiikkaa eikä mielenkiintoinen tämän kirjoituksen kannalta. Lause 4 Kuvan 7 algoritmin suoritusaika on O(m log n), missä n on tilojen ja m siirtymien määrä. Vielä täytyy osoittaa, että algoritmi on niin nopea kuin luvattiin. Todistus. Väitteen 2 vuoksi ja koska automaatti on deterministinen, on kussakin kimpussa korkeintaan n siirtymää. Muuttujien k ja ℓ juoksutus tekee ilmeiseksi, että jos samaa siirtymää tai tilaa käytetään pilkkomiseen monta kertaa, se kuuluu joka kerta kimppuun tai lohkoon, jonka nuLause 3 Kuvan 7 algoritmin lopetettua mero on suurempi kuin edellisellä kerraljokainen lohko on yhteensopiva jokaisen la. Koska suuremman eli uuden numeron aakkosen suhteen, jokainen lohko on osa- saa halkaistun kimpun tai lohkon pienemjoukko jostakin alkuperäisestä lohkosta, ja pi osa, on uudelleen käytettävän siirtymän jokainen lohko on niin suuri, kuin edellä kimppu tai tilan lohko kooltaan korkeinmainitut seikat sallivat. taan puolet edelliskertaisesta. Siksi samaa siirtymää tai tilaa käytetään korkeintaan Todistus. Väitteiden 4 ja 1 ansiosta kaksi noin log n kertaa. 2 tilaa joutuu eri lohkoihin vain, jos ne ovat eri lohkoissa alussa tai toisesta lähtee siirTilojen tulosiirtymien yhteismäärä on tymä johonkin lohkoon siten, että toisesta sama kuin siirtymien määrä. Näistä, luei lähde saman nimistä siirtymää samaan vun 4 tehokkuusanalyyseista sekä oletuklohkoon. Algoritmi ei siis pilko lohkoja sesta, että H, alkup¨aa¨ n_tila ja tulosiirtyenempää kuin on välttämätöntä yhteenso- mien selaus on toteutettu asianmukaisespivuuden saavuttamiseksi. ti seuraa, että algoritmin suoritusaika on Erikoistapausta k_osia = 0 lukuunot- O(m log n). 2 tamatta algoritmin lopettaessa minkään Valmari 47 1 1 2 1 2 3 0 2 0 1 Kuva 8: Paigen ja Tarjanin algoritmin havainnollistus. 6 Paigen ja Tarjanin algoritmi Äärellisen automaatin deterministisyys takaa, että jos tilasta on siirtymä johonkin lohkoon, niin siitä ei ole samannimistä siirtymää johonkin toiseen lohkoon. Edellisen luvun algoritmi ja päättely hyödynsivät tätä. Tilanne monimutkaistuu huomattavasti, jos sallitaan monta samannimistä siirtymää samasta tilasta, jopa siinä tapauksessa, että eri nimiä on kaikkiaan käytössä vain yksi. Yhtäpitävästi voidaan olettaa, että siirtymillä ei ole nimiä, ja samasta tilasta voi lähteä monta siirtymää. Paige ja Tarjan käsittelivät tätä tilannetta tiiviissä ja työläästi luettavassa julkaisussa [13]. Vaikeus on siinä, että jos lohkoa L on käytetty pilkkomiseen ja L halkeaa lohkoiksi L1 ja L2 , niin L1 :n käyttö pilkkomiseen edellä kuvattuun tapaan ei erota niitä tiloja, joista on siirtymä sekä L1 :een että L2 :een niistä, joista on siirtymä pelkästään L1 :een. Tarvitaan keino tehdä tämä erottelu. Sen seurauksena lohko pilkkoontuu kerralla enintään kahden osan sijasta enintään kolmeen osaan. Tämäkin on hallittava jotenkin. Paige ja Tarjan ratkaisivat nämä koostelohkojen ja nerokkaasti käytettyjen laskurien avulla. Koostelohko on tietynlainen epätyhjä joukko lohkoja. Aluksi kaikki lohkot kuuluvat samaan koostelohkoon, ja myöhemmin koostelohkoja syntyy vain jäljempänä kerrottavalla tavalla. Voidaan osoittaa, että koostelohkon lohkojen unionin aiheuttama pilkkomistarve on toteutettu. Jotta tämä pätisi alussa, aivan aluksi ti- lat pitää jakaa niihin, joista lähtee siirtymiä ja niihin, joista ei lähde. Tekemättömästä työstä pidetään kirjaa joukolla, jossa ovat niiden koostelohkojen numerot, joissa on ainakin kaksi lohkoa. Pilkkominen aloitetaan poistamalla jostain tällaisesta koostelohkosta yksi lohko ja julistamalla se sekä pilkkojaksi että itsessään koostelohkoksi. Kuvan 8 alarivissä on koostelohko, jonka vasemmasta reunasta on erotettu kolmetilainen pilkkoja. Pilkkomisen tuloksena jokainen lohko, jonka ainakin yhdestä tilasta on siirtymä alkuperäiseen koostelohkoon, jakaantuu enintään kolmeksi lohkoksi. Vasen lohko koostuu niistä tiloista, joiden kaikki alkuperäiseen koostelohkoon vievät siirtymät vievät pilkkojaan; oikean lohkon tilojen siirtymistä mikään ei vie pilkkojaan; ja keskilohkon tiloista on siirtymiä sekä pilkkojaan että muualle alkuperäiseen koostelohkoon. Kuvan 8 ylärivin lohkon pilkkoutuminen on osoitettu katkoviivoilla. Oikea lohko erottuu muista sillä, että sen tiloja ei kohdata kuljettaessa pilkkojaan päättyvät siirtymät takaperin, mutta vasemman lohkon erottaminen keskilohkosta on vaikeaa. Jokaiseen tilaan ja koostelohkoon liittyy kuvitteellinen päälaskuri, joka kertoo, kuinka monta siirtymää vie kyseessä olevasta tilasta kyseessä olevaan koostelohkoon. Oikeasti toteutetaan ne päälaskurit, joissa oleva arvo on suurempi kuin nolla. Jokaiseen siirtymään liittyy linkki, jonka avulla löydetään se päälaskuri, jonka ti- 48 Kuinka kiltti mutta tarpeeton pikku algoritmi sai tärkeän tehtävän la on siirtymän alkutila ja jonka koostelohkoon siirtymä päättyy. Sanomme sitä siirtymän päälaskuriksi, vaikka se voi olla monen siirtymän yhteinen. Siirtymä itse kuuluu päälaskurinsa laskemiin siirtymiin, joten päälaskurin arvo on aina positiivinen. Kuvassa 8 linkit on esitetty näin: ” ”. Lisäksi jokaiseen tilaan liittyy apulaskuri lyhytaikaista käyttöä varten. Kuvan 8 ylärivin tilojen apulaskurit on esitetty tilojen vieressä. Pilkkomisen aluksi pilkkojaan tulevat siirtymät käydään läpi ja jokaisen niistä alkupään tilan apulaskuria kasvatetaan yhdellä. Näin saadaan selville, kuinka monta siirtymää kustakin tilasta vie pilkkojaan. Sitten pilkkojaan tulevat siirtymät käydään läpi uudelleen. Vertaamalla siirtymän alkupään tilan apulaskurin arvoa siirtymän päälaskurin arvoon saadaan selville, vievätkö kaikki tilasta alkuperäiseen koostelohkoon vievät siirtymät pilkkojaan, vai viekö osa niistä muualle koostelohkoon. Toisin sanoen, saadaan selville, kuuluuko tila vasempaan vai keskilohkoon. Tämän perusteella tilat merkitään yhdellä tai toisella tavalla myöhempää halkaisemista varten. Oikeaan lohkoon kuuluvat ne tilat, joita ei kohdata tässä prosessissa ja jotka jäävät siksi merkitsemättä. Keskilohkon jokaista tilaa varten joudutaan perustamaan uusi päälaskuri, jotta olisi olemassa päälaskuri sekä pilkkojaa että alkuperäisen koostelohkon loppuosaa varten. Pilkkojan päälaskurin arvoksi asetetaan tilan apulaskurin arvo. Sama arvo vähennetään alkuperäisen koostelohkon päälaskurista, joka jatkaa koostelohkon loppuosan päälaskurina. Siirtymän linkki käännetään osoittamaan uutta päälaskuria. Kaikki tämä kannattaa tehdä, kun tila kohdataan ensimmäisen kerran jälkimmäisellä selauksella. Tieto siitä, että tämä on tehty, laitetaan talteen yhdessä uuteen päälaskuriin vievän linkin kanssa. Kun tila kohdataan selauksen edetessä uudelleen, riittää päivittää siirtymän linkki. Apulaskurit täytyy nollata tulevaa käyttöä varten. Tämän voisi tehdä heti kun tilan laji on selvinnyt. Toisaalta, juuri siksi, että apulaskuria ei sen jälkeen tarvita, sen paikalle voidaan tarvittaessa tallettaa uuteen päälaskuriin vievä linkki, kunhan yksi bitti (esimerkiksi etumerkki) uhrataan kertomaan, kumpi tieto muistipaikassa on. Näin säästetään muistia ja suututetaan ohjelmien ylläpidettävyydestä huolta kantavat. Apulaskurit voi myöhemmin nollata tehokkaasti selaamalla pilkkojaan tulevat siirtymät kolmannen kerran. Kun kaikki tämä on tehty, niin halkaistaan ne lohkot, joiden tiloja merkittiin edellä. Tämä tapahtuu muuten kuten luvussa 5, paitsi tiloja on merkitty kahdella tavalla ja lohkoja voidaan joutua halkaisemaan kolmeen osaan. Paigen ja Tarjanin algoritmi voidaan yleistää nimetyille siirtymille edellisen luvun kimppujen avulla [14]. Myös muut edellisen luvun parannukset voidaan ottaa käyttöön. Itse asiassa olen ohjelmoinut ja testannut edellisen luvun uudet ajatukset tässä yhteydessä enkä deterministisillä äärellisillä automaateilla. 7 Derisavin, Hermannsin ja Sandersin algoritmi Pääsemme vihdoin takaisin Markovin ketjuihin. Kirjallisuudessa on huolettomasti väitetty, että soveltamalla Paigen ja Tarjanin algoritmia voidaan Markovin ketjun tilojen yhdistämistehtävä ratkaista O(m log n) ajassa, missä n on yhä tilojen ja m siirtymien määrä. Derisavi, Hermanns ja Sanders huomauttivat vuonna 2003, että asia ei ole niin yksinkertainen [5]. Markovin ketjun tilojen yhdistämisel- Valmari 49 lä ja Paigen ja Tarjanin algoritmilla on paljon yhteistä. Tärkein ero on seuraava. Paigen ja Tarjanin algoritmissa olennaista on, onko tilasta siirtymää pilkkojaan. Markovin ketjun tilojen yhdistämisessä olennaista on, kuinka paljon on tilasta pilkkojaan vievien siirtymien todennäköisyyksien summa. Summien laskemiseksi Derisavi työtovereineen käytti edellisen luvun apulaskurien kaltaista menetelmää. summan mukaan, jolloin samaan ryhmään kuuluvat tilat asettuvat peräkkäin. Derisavi työtovereineen huomautti, että koska tyypilliset järjestämisalgoritmit eivät vie O(k) vaan O(k log k) aikaa, missä k on järjestettävien tilojen määrä, tulee pilkkomisalgoritmin suoritusaikaan ylimääräinen logaritmitermi. Siis suoritusaika on O(m log2 n). Niiden aikaisempien kirjoittajien, jotka väittivät, että O(m log n) riittää, olisi tullut kertoa, miten ylimääräinen Koska alkuperäisen koostelohkon pilklogaritmitermi voidaan välttää. Tätä he eikomisvaikutus on jo tehty, on kaikilla vät olleet tehneet. vasemman, oikean ja keskilohkon tiloilla sama alkuperäiseen koostelohkoon vieDerisavi työtovereineen osoitti myös, vien siirtymien todennäköisyyksien sum- että jos keski- ja vasemman lohkon unioni ma. Vasemman lohkon tiloilla koko tä- järjestetään niin sanottujen splay-puiden mä summa kohdistuu pilkkojaan. Muual- avulla, niin pilkkomisalgoritmin suorile alkuperäiseen koostelohkoon kohdistu- tusaika putoaa tavoiteltuun kertaluokkaan va summa on nolla. Oikean lohkon tiloilla O(m log n). Splay-puut ovat kuitenkin vätilanne on päinvastainen. Keskilohkon ti- hän tunnettu, monimutkainen ja (kuten biloilla osa summasta kohdistuu pilkkojaan nääripuut yleensä) melko paljon muistia ja osa muualle alkuperäiseen koosteloh- kuluttava tietorakenne. Ylimääräinen lokoon. Jakauma voi olla erilainen eri ti- garitmitermi ei ole edes teoriassa iso hiloilla. Esimerkiksi yhdestä tilasta voidaan dastus. Voi olla, että splay-puihin perustusiirtyä todennäköisyydellä 0,1 pilkkojaan va ratkaisu on monimutkaisuutensa vuokja 0,6 muualle koostelohkoon, ja toisel- si käytännön tilanteissa yleensä hitaamla tilalla vastaavat todennäköisyydet voi- pi kuin tavallisen yksinkertaisen järjestävat olla 0,4 ja 0,3. misalgoritmin käyttö. Siksi tämän tulokNäin ollen vasen ja oikea lohko ovat sen käytännön merkitys vaikuttaa pienelyhteensopivia sekä pilkkojan että alkupe- tä. räisen koostelohkon loppuosan suhteen, mutta keskilohko ei välttämättä ole. Keskilohkon tilat täytyy ryhmitellä ja jakaa lohkoiksi sen mukaan, mikä on niistä pilkkojaan (tai vaihtoehtoisesti muualle alkuperäiseen koostelohkoon) vievä summa. Kun tällainen keino on otettu käyttöön, sillä voi myös erottaa vasemman lohkon tilat keskilohkon tiloista. Tämä on etu, sillä, kuten luvussa 6 nähtiin, Paigen ja Tarjanin algoritmin keino tehdä sama asia on niin monimutkainen, että siitä eroon pääsy olisi suotavaa. Julkaisu [5] keskittyy nopeuteen splaypuiden kanssa ja ilman. Algoritmin yleisen oikeellisuuden se kuittaa epämääräisillä viittauksilla Paigen ja Tarjanin algoritmiin sekä lähteisiin, joissa puhutaan Markovin ketjun tilojen yhdistämisestä yleisellä mutta ei julkaisun algoritmin tasolla. Tämä ei riitä, sillä julkaisun algoritmi poikkeaa Paigen ja Tarjanin algoritmista huomattavasti. Se muun muassa ei käytä koostelohkoja. Lukijan on vaikea saada julkaisusta [5] selville, mikä on sen algoritmin oikeelliLuonnollisin ryhmittelykeino on jär- suuden kannalta olennaista. Lisäksi algojestää keski- ja vasemman lohkon tilat ritmissa sellaisena kuin se julkaisussa esi- 50 Kuinka kiltti mutta tarpeeton pikku algoritmi sai tärkeän tehtävän tetään on virhe. Kun lohko halkeaa, niin sen osia pitää lisätä vuoroaan odottavien pilkkojien joukkoon. Julkaisussa sinne lisätään aina kaikki muut osat paitsi suurin. Suurimman osan saa kuitenkin jättää pois vain siinä tapauksessa, että haljennut lohko ei itse ollut vuoroaan odottavien pilkkojien joukossa. Virhe ei kumoa algoritmin perusajatuksia, mutta johtaa siihen, että julkaisun mukaan toteutettu ohjelma laskisi väärin. Lukijalla on oltava huomattavat taustatiedot voidakseen havaita ja korjata virheen. telohkojen määrää, joihin tilasta i on ainakin yksi siirtymä. Käymällä kaikki tilat läpi saadaan ∑ni=1 κ(i) ≤ m. Keskilohkon tilasta on siirtymä sekä pilkkojaan että muualle alkuperäiseen koostelohkoon. Pilkkojasta tulee uusi koostelohko. Tästä seuraa, että joka kerta kun i on keskilohkossa, κ(i) kasvaa yhdellä. Niinpä, jos λ(i) on niiden kertojen määrä, jotka tila i on ollut keskilohkossa, niin λ(i) ≤ κ(i). Kun λ(i) summataan kaikkien tilojen yli saadaan käsiteltyjen keskilohkojen kokojen summa, eli ∑Kj=1 k j = ∑ni=1 λ(i). Yhdistämällä tä8 Eroon splay-puista hänastiset huomiot saadaan ∑Kj=1 k j ≤ m. Koska lohkon koko on enintään tilojen Kesäkuussa 2009 pidin esitelmää julkaimäärä, on ∑Kj=1 k j log k j ≤ ∑Kj=1 k j log n = susta [14], joka esittää Paigen ja Tarjanin algoritmin yleistyksen tilanteeseen, (∑Kj=1 k j ) log n. Edellä saadun tuloksen jossa siirtymillä on vapaasti valittavat ni- kanssa tämä tuottaa ∑Kj=1 k j log k j ≤ met. (Senkin kohdalla kirjallisuudessa on m log n. 2 huolettomasti mutta virheellisesti väitetJos keskilohkot järjestetään O(k log k) ty, että yleistämiseen ei kätkeydy erityiaikaa kuluttavalla algoritmilla, niin lusiä ongelmia.) Yleisössä ollut Giuliana K ku ∑ j=1 k j log k j kuvaa järjestämisten yhFranceschinis kysyi, voiko algoritmiani teensä kuluttamaa aikaa. Apulause sanoo soveltaa Markovin ketjun tilojen yhdistäsiis, että algoritmin tavoiteltu suoritusaika mistehtävään, sillä algoritmini vaikutti häriittää keskilohkojen järjestämiseen tavalnestä yksinkertaisemmalta kuin Derisavin lisella järjestämisalgoritmilla. Näin ollen, ja työtovereiden. jos vasemman lohkon tilat saadaan erotetPaljastui, että julkaisun [14] uusista tutua keskilohkon tiloista ennen järjestämisloksista ei ole apua, mutta Markovin ketjun tilojen yhdistämistehtävä oli muuten tä, niin tavallinen järjestämisalgoritmi riitkiinnostava. Tästä syntyi julkaisu [15]. tää eikä monimutkaisia splay-puita tarviSiinä osoitimme ja korjasimme luvussa 7 ta. Vasemman lohkon tilat saadaan eromainitun virheen, korvasimme splay-puut yksinkertaisemmalla mekanismilla, ja an- tettua Paigen ja Tarjanin algoritmin lasnoimme perusteelliset algoritmikuvaukset kurikeinolla. Ikävä kyllä se on niin monimutkainen, että ajatus tyytymisestä ja todistukset. Splay-puiden korvaamisessa tarvitaan O(m log2 n) suoritusaikaan tuntuu houkuttelevammalta. seuraavaa tulosta. Diskreetin ajan Markovin ketjujen taApulause 5 Jos Markovin ketjun tilojen pauksessa siirtymiin liittyvät luvut ovat osituksen pilkkomisen aikana käsiteltyjen todennäköisyyksiä ja niin ollen positiivikeskilohkojen koot ovat k1 , k2 , . . . , kK , sia. Siitä seuraa, että joko vasen lohko on niin ∑Kj=1 k j log k j ≤ m log n. tyhjä tai se koostuu niistä tiloista, joiden Todistus. Tarkoittakoon κ(i) niiden koos- pilkkojaan vievien siirtymien todennäköi- Valmari syyksien summa on suurin. Summan suurin arvo on helppo selvittää selaamalla vasemman ja keskilohkon unionin tilat läpi, jonka jälkeen sen tuottavat tilat voidaan ottaa erilleen selaamalla tilat uudelleen läpi. Jos vasen lohko on tyhjä, tämä keino erottaa keskilohkon jonkin osan, mutta se ei haittaa. Järjestämisen tarkoitus on ryhmitellä keskilohkon tilat summien mukaan, eikä tätä häiritse, jos yksi ryhmä otetaan etukäteen erilleen. Valitettavasti tämä keino ei toimi jatkuvan ajan Markovin ketjuille, sillä niiden siirtymissä esiintyy myös negatiivisia lukuja. Tarvitaan toinen keino. Äskeinen toteamus yleistäen, ei haittaa, jos keino erottaakin väärän ryhmän, kunhan jäljelle jäävissä ryhmissä on yhteensä tarpeeksi vähän tiloja, ja keino on nopea ja yksinkertainen. On helppo osoittaa, että aika riittää, vaikka luvut k j kaksinkertaistettaisiin. Joko lukija arvasi? 9 Tarvitaanko enemmistöarvon löytämisalgoritmia varmasti? Enemmistöarvon löytämisalgoritmi on ihanteellinen keino valita yksi ryhmä erotettavaksi vasemman ja keskilohkon unionista ennen järjestämistä. Jos vasen lohko on suurempi kuin keskilohko, niin se löytää vasemman lohkon tiloihin liittyvän summan. Päinvastaisessa tapauksessa vasen ja keskilohko ovat yhteensä niin pienet, että aika riittää niiden unionin järjestämiseen O(k log k) aikaa kuluttavalla algoritmilla, joten ei ole väliä, mikä ryhmä erotetaan. Enemmistöarvon löytämisalgoritmi on melkein yhtä nopea ja yksinkertainen kuin suurimman summan etsiminen, mutta toimii silloinkin kun siirtymiin liittyvät luvut voivat olla negatiivisia. Näistä syistä julkaisussa [15] käytetään enemmistöarvon löytämisalgoritmia yhden ryhmän erottamiseen. 51 Enemmistöarvon löytämisalgoritmi on täten saanut mielekkään tehtävän. Sen ansiosta yhdistämisalgoritmin suoritusajaksi saadaan O(m log n) ilman monimutkaisten splay-puiden käyttöä. Kohta tullaan näkemään, että algoritmien käytännön nopeuksia on vaikea verrata kovin tarkasti. Silti on helppo uskoa, että julkaisun [15] algoritmi on yksinkertaisuutensa ja pienemmän muistin kulutuksensa ansiosta käytännössä yleensä tehokkaampi kuin julkaisun [5] algoritmi. Vaikeampi on selvittää, tuoko yhden ryhmän erottaminen enemmistöalkion löytämisalgoritmilla käytännössä hyötyä verrattuna siihen, että koko vasemman ja keskilohkon unioni järjestetään yhdessä. Se pudottaa ajan kulutuksen kertaluokasta O(m log2 n) kertaluokkaan O(m log n). Kuitenkaan O-merkinnän tasolla paras algoritmi ei aina ole käytännössä paras, sillä O-merkintä ilmaisee suoritusajalle ylärajan, ja algoritmi voi olla suurimmassa osassa tapauksia paljon sitä nopeampi. Esimerkiksi Quicksort katsotaan yleensä käytännössä nopeammaksi kuin Heapsort, vaikka Quicksortin ajan kulutus on O(k2 ) ja Heapsortin O(k log k). Enemmistöalkion löytämisalgoritmi on itsessään niin halpa, että se ei missään oloissa kasvata järjestämiseen käytettävää aikaa merkittävästi. Olisi houkuttelevaa päätellä tästä, että sen käytöllä ei koskaan voi hävitä merkittävästi mutta voi voittaa merkittävästi, joten sitä varmasti kannattaa käyttää. Valitettavasti asia ei ole näin yksinkertainen. Yhden osan erottaminen muuttaa järjestystä, jossa uudet lohkot saavat numeronsa. Siten se muuttaa järjestystä, jossa lohkoja käytetään pilkkomiseen. Kuten seuraavaksi perustellaan, sillä voi olla vaikutusta kokonaistyömäärään. Kuinka kiltti mutta tarpeeton pikku algoritmi sai tärkeän tehtävän 52 0,3 sa mahdollista, että uusi tieto muuttaa tilannetta. Splay-puut joutuivat syrjään, koska julkaisussa [15] onnistuttiin osoittamaan, että tavoiteltu aika riittää keskilohOheisen kuvan jomman kumman reu- kon järjestämiseen tavallisella algoritmilnalohkon käyttö pilkkomiseen halkaisee la. Kenties tavoiteltu aika riittää myös vakeskellä olevan lohkon, eikä muita halkea- semman ja keskilohkon unionin järjestämisia tapahdu. Kuvasta 7 näkyy, että loh- miseen tavallisella algoritmilla. Derisavin koa numero 1 ei käytetä pilkkomiseen. Jos ja muiden huomautus ei ollut, että se ei riikeskimmäinen lohko saa alun perin nume- tä, vaan että puuttuu todistus, että se riitron 2, niin sitä käytetään pilkkomiseen en- tää. Jos sellainen todistus löytyy, niin minen kuin se itse halkeaa. Sen molemmat tään ryhmää ei tarvitse erottaa ennen järtilat käydään läpi kertaalleen ja myöhem- jestämistä ja enemmistöarvon löytämisalmin vielä jompi kumpi tila kertaalleen. goritmi muuttuu taas tarpeettomaksi. SeuJos se saa numeron 3, niin sitä käytetään raava apulause kertoo, voiko näin käydä. pilkkomiseen vasta kun se on itse haljennut, joten sen kumpikin tila käydään läpi Apulause 6 Keski- ja vasemman lohvain kerran. Erolla ei ole muuta vaikutus- kon unionien järjestämiset Θ(k log k) aita algoritmin toimintaan. Jälkimmäisessä kaa vievällä algoritmilla vievät hitaimtapauksessa tehdään kaiken kaikkiaan vä- massa tapauksessa yhteensä Θ(m log2 n) aikaa. hemmän työtä. Esimerkki havainnollistaa, että tarkka suoritusaika voi riippua seikoista, jot- Todistus. Kuvassa 9 on esitetty perhe Marka eivät määräydy algoritmista vaan sii- kovin ketjuja, jossa on yksi ketju jokaiseltä, miten se on toteutettu ohjelmana, mis- le luvun h arvolle. Alkujako on osoitettu sä järjestyksessä syöte annetaan ja niin harmailla viivoilla. Jos keskimmäistä alkuperäistä lohkoa edelleen. Sellaisia ei voi ottaa huomioon algoritmeja vertailtaessa. Ohjelmien pa- käytetään ensimmäisenä pilkkojana, se remmuusjärjestystä ei läheskään aina voi halkaisee itsensä oikeanpuoleisinta katselvittää mittaamalla suoritusaikoja, sillä koviivaa pitkin. Katkoviivan vasemmalle h−1 − 1 ja oikealle puolelle yhdellä syöteaineistolla nopeampi ohjel- puolelle jää 2 h−1 sen tilaa, joten jatkossa käytettäväkma saattaa olla hitaampi toisella syöteai- 2 neistolla. Tekemissäni mittauksissa ei il- si pilkkojaksi valitaan vasen puoli. Kun simennyt satunnaisvaihtelua suurempaa no- tä käytetään pilkkojana, se erottaa seuraavan palstan tilat muista. Tätä jatkuu, kunpeuseroa kumpaankaan suuntaan. On siis erittäin vaikeaa selvittää sito- nes lohko on pilkottu kuvan kaikkia katvasti, onko yhden osan erottaminen ennen koviivoja pitkin. järjestämistä käytännössä parempi, yhtä Jokaisessa pilkkomisessa kuvan vahyvä vai huonompi ratkaisu kuin erotta- semmassa reunassa olevat 2h tilaa muomatta jättäminen. Ei kuitenkaan ole mi- dostavat vasemman lohkon, vastaavan tään erityistä syytä olettaa, että se olisi keski- ja oikean lohkon ollessa tyhjiä. Jos huonompi, ja erottamisen halpuus ja sen vasemman ja keskilohkon unioni järjestuoma parannus O-merkinnän tasolla ovat tetään jokaisen pilkkomisen yhteydessä syitä olettaa, että se on parempi. Sen käyt- Θ(k log k) aikaa vievällä algoritmilla, niin tö on siis järkevää. kuvan vasen reuna aiheuttaa h järjestämisTässä vaiheessa on ainakin periaattees- tä joista kukin käsittelee 2h alkiota. Ne 1 0,7 0,3 0,7 1 Valmari 53 ... 2h .. . .. . ... ... 2h−1 .. . h Kuva 9: Esimerkki, jossa sama iso vasen lohko tulee käsiteltäväksi useasti [15]. Jokaisen siirtymän todennäköisyys on 1. vievät yhteensä aikaa lukuun h(2h log 2h ) verrannollisesti. Kuvassa m = n = 2h+1 , joten ajan kulutus on verrannollinen lukuun (log2 2n ) m2 log 2n = Θ(m log2 n). 2 Tavoiteltu suoritusaika ei siis riitä vasemman ja keskilohkon unionien järjestämiseen tavallisella algoritmilla. Kyse ei ole siitä, että puuttuu todistus, että se riittää, vaan siitä, että se todellakaan ei riitä. Vasemman lohkon ottaminen erilleen silloin kun se on suuri on siis välttämätöntä. Enemmistöarvon löytämisalgoritmin käyttö ei ole turhaa. 10 Yhteenveto peen luokitella tiloja tilasta pilkkojana käytettävään lohkoon vievien kaarten painojen summien mukaan. Luokittelua ei voi toteuttaa tavallisella järjestämisalgoritmilla koska, kuten kirjoituksessa todistettiin, tavoiteltu suoritusaika ei riitä siihen. Onneksi järjestettävät tilat ovat peräisin kahdesta lähteestä, joista toinen tuottaa niin vähän tiloja, että aika riittää niiden järjestämiseen, ja toisen tuottamat tilat kuuluvat keskenään samaan luokkaan, joten niitä ei tarvitse järjestää. Ongelmaksi jää erotella eri lähteistä tulevat tilat toisistaan. Tilat voi erotella Paigen ja Tarjanin esittämällä laskurikeinolla. Valitettavasti Tässä kirjoituksessa tarkasteltiin ositukse on monimutkainen. Helpommalla päässen pilkkomiseen perustuvia algoritmeja tään erottelemalla yksi ryhmä tiloja enemkolmen tehtävän ratkaisemiseen. Tehtävät mistöarvon löytämisalgoritmin avulla. Se olivat klassinen äärellisen automaatin mion erittäin yksinkertainen ja tehokas algonimointi; sen muunnos, jossa siirtymillä ei ritmi, joka löytää eniten esiintyvän arvon, ole nimiä, mutta samasta tilasta voi silti lähteä monta siirtymää; sekä Marko- jos se esiintyy useammin kuin muut arvot vin ketjun samanveroisten tilojen yhdistä- yhteensä. Muussa tapauksessa se palautminen. Markovin ketjut ovat hyödyllisiä taa mielivaltaisen aineistoon kuuluvan artodennäköisyyslaskentaan liittyvien teh- von. tävien numeerisessa ratkaisemisessa. TaKyvyttömyys tuottaa informatiivinen voitteena oli ratkaista tehtävät O(m log n) vastaus silloin, kun mikään arvo ei yksiajassa, missä n on tilojen ja m siirtymien nään muodosta enemmistöä, on heikkous, määrä. jonka vuoksi enemmistöarvon löytämisalMarkovin ketjun tapauksessa on tar- goritmilla on niukasti todellista hyöty- 54 Kuinka kiltti mutta tarpeeton pikku algoritmi sai tärkeän tehtävän käyttöä. Markovin ketjujen sovelluksessa 4. Boyer, R.S. & Moore, J S.: MJRTY: A Fast Majority Vote Algorithm. Boyer, R.S. siitä ei kuitenkaan ole haittaa. Jos jälkim(toim.): Automated Reasoning: Essays mäisen lähteen tuottamia tiloja on niin väin Honor of Woody Bledsoe, 105–117, hän, että ne eivät muodosta enemmistöä, Kluwer Academic Publishers, Dordrecht, niin aika riittää kaikkien tilojen järjestäThe Netherlands (1991) miseen, joten ei ole väliä, mikä ryhmä ti5. Derisavi, S., Hermanns, H. & Sanders, loja erotetaan. W.H.: Optimal State-space Lumping in Enemmistöarvon löytämisalgoritmi on Markov Chains. Information Processing siis yksinkertainen ja tehokas keino noLetters 87(6), 309–315 (2003) peuttaa Markovin ketjujen tehtävässä 6. Fernandez, J.-C.: An Implementation of esiintyvää järjestämistä sen verran, että taan Efficient Algorithm for Bisimulation voiteltuun suoritusaikaan O(m log n) pääsEquivalence. Science of Computer Protään. gramming 13, 219–236 (1989/90) Tarkasteltavia algoritmeja käsiteltiin 7. Freshers Interviews: Majority Element. melko laajasti. Muun muassa esiteltiin tiehttp://placementsindia.blogspot.com/ torakenne, jolla pilkottavan osituksen voi 2007/09/majority-element.html esittää tehokkaasti. Matkan varrella kuLuettu 14.5.2010. vailtiin muutamia viime vuosina keksit- 8. Gries, D.: Describing an Algorithm by tyjä parannuksia pilkkomiseen perustuHopcroft. Acta Informatica 2, 97–109 (1973) viin algoritmeihin. Kerrottiin, miten deterministisen äärellisen automaatin mini- 9. Hopcroft, J.: An n log n Algorithm for Minimizing States in a Finite Automaton. moinnin ajan kulutus voidaan pudottaa Technical Report STAN-CS-71-190, Stankertaluokasta O(αn log n) kertaluokkaan ford University (1971) O(m log n), missä α on aakkoston koko. Kerrottiin, miten voidaan päästä eroon tie- 10. Knuutila, T.: Re-describing an Algorithm by Hopcroft. Theoretical Computer torakenteesta, jota aikaisemmin on tarScience 250, 333–363 (2001) vittu pitämään kirjaa pilkkomisesta, joka 11. Morgan, C.: Programming from Specivielä tulee tehdä. Kerrottiin, miten erääsfication, 2nd ed.. Prentice-Hall Intertä toisestakin tietorakenteesta päästään national Series in Computer Science eroon. Kirjoituksessa esitetty nopea de(1994) Myös http://web2.comlab.ox.ac. terminististen äärellisten automaattien miuk/oucl/publications/books/PfS/ (1998) nimointialgoritmi lienee yksinkertaisempi 12. Mäenpää, P.: The Art of Analysis: Logic kuin mikään aikaisemmin esitetty. Viitteet 1. Aho, A.V., Hopcroft, J.E. & Ullman, J.D.: The Design and Analysis of Computer Algorithms. Addison-Wesley, Reading, MA (1974) 2. Backhouse, R.C.: Program Construction and Verification. Prentice-Hall International Series in Computer Science, UK (1986) 3. Boyer, C.: Tieteiden kuningatar, matematiikan historia osa II. Suomentanut Kimmo Pietiläinen. Art House (1994) and History of Problem Solving. Väitöskirja, Helsingin yliopisto, Filosofian laitos (1993) 13. Paige, R. & Tarjan, R.: Three Partition Refinement Algorithms. SIAM Journal of Computing 16(6), 973–989 (1987) 14. Valmari, A.: Bisimilarity Minimization in O(m log n) Time. Franceschinis, G. & Wolf, K. (toim.) Petri Nets 2009, Lecture Notes in Computer Science 5606, 123– 142, Springer, Heidelberg (2009) 15. Valmari, A. & Franceschinis, G.: Simple O(m log n) Time Markov Chain Lumping. Esparza, J. & Majumdar, R. (toim.) Valmari TACAS 2010, Lecture Notes in Computer Science 6015, 38–52, Springer, Heidelberg (2010) 16. Valmari, A. & Lehtinen, P.: Efficient Minimization of DFAs with Partial Transition 55 Functions. Albers, S. & Weil, P. (toim.) STACS 2008, Symposium on Theoretical Aspects of Computer Science, Bordeaux, France, 645–656. http://drops.dagstuhl.de/ volltexte/2008/1328/ (2008)
© Copyright 2024