58131 Tietorakenteet ja algoritmit (syksy 2015) Harjoitus 10, malliratkaisut 1. Järjestetään verkko topologisesti luentomateriaalissa esitetyllä tavalla. d c a b e f Pino: d c a b e f Pino: d c a b e f Pino: b d c a b e f Pino: b d c a b e f Pino: b 1 d c a b e f Pino: b d c a b e f Pino: b, f d c a b e f Pino: b, f, d d c a b e f Pino: b, f, d, c d c a b e f Pino: b, f, d, c 2 d c a b e f Pino: b, f, d, c, e d c a b e f Pino: b, f, d, c, e, a Täten siis tuloksena oleva verkon topologinen järjestys on a,e,c,d,f,b. 2. (a) Olkoon G = (V, E) DAG. Merkitään |V | = n. Väite: G:ssä on solmu, johon ei tule yhtään kaarta. Todistus: Tehdään vastaoletus, että G:n jokaiseen solmuun tulee kaari. Johdamme tästä ristiriidan. Olkoon u nyt mielivaltainen verkon G solmu. Koska vastaoletuksen nojalla jokaiseen verkon solmuun tulee kaari, löydämme solmun v, josta on kaari u:hun. Voimme toistaa tätä menettelyä vastaoletuksen nojalla mielivaltaisen monta kertaa. Saamme siis rakennettua n + 1 solmua pitkän polun (u1 , . . . , un+1 ) seuraamalla kaaria takaperin. Koska verkossa on n solmua, jokin solmu esiintyy kahdesti valitussa polussa. Toisin sanoen, ui = uj , missä 1 ≤ i < j ≤ n + 1. Siis (ui , . . . , uj ) on sykli. Tämä on ristiriidassa sen kanssa, että G on DAG. 2 (b) Selvästi |V | rekursiivista kutsua tuottaa verkon topologisen järjestyksen, joten ainoaksi kysymykseksi jää, miten tehokkaasti löydämme DAGista solmun, johon ei tule kaarta. Voimme tehdä sen esimerkiksi seuraavasti. Käydään ensin läpi kaikki verkon solmut ja kaaret, tallentaen jokaiseen solmuun tiedon siitä, kuinka monta kaarta solmuun tulee. Tallennamme joukkoon S kaikki solmut, joihin ei tule yhtään kaarta. Joka kierroksella valitsemme poistettavan solmun u suoraan joukosta S, ja käymme läpi kaikki u:n naapurit, vähentäen niiden laskureista yhden. Jos jonkin solmun laskuri menee näin nollaan, lisäämme sen solmun joukkoon S. Koska käsiteltävä verkko on DAG, joukossa S on aina ainakin yksi alkio. Alussa tehtävä läpikäynti vie O(|V | + |E|) aikaa. Toisaalta jokaista kaarta kohti vähennetään laskuria ja jokainen solmu käy joukossa S, yhteensä aikaa O(|V | + |E|). Siis kokonaisaika on O(|V | + |E|). Myös luennoilla esitetty menetelmä vie aikaa O(|V | + |E|). 3. Etsitään vahvasti yhtenäiset komponentit verkosta: 3 a b c e f d Ensin suoritetaan verkon syvyyssuuntainen läpikäynti, ja lisätään solmu pinoon silloin, kun sen käsittely on ohi. Pino sisältää seuraavat alkiot seuraavassa järjestyksessä: e, f, d, b, a, c, eli c on pinon huipulla. Seuraavaksi muodostetaan verkon transpoosi. a b c e f d Tästä verkosta löydämme vahvasti yhtenäiset komponentit purkamalla pinon alkio kerrallaan, ja aloittamalla syvyyshaun aina jokaisesta sellaisesta pinosta poistetusta alkiosta, jossa ei olla vielä vierailtu pinon purkamisen aikana. Jokainen näin muodostunut syvyyssuuntainen puu on vahvasti yhtenäinen komponentti. Täten vahvasti yhtenäiset komponentit ovat (syvyyshaun alkusolmu värjätty tummemmalla): a b c e f d Piirtämällä komponenttiverkon (komponentit: A = {a, b, d}, B = {c}, C = {e, f }) näemme helposti, että saamme verkosta vahvasti yhtenäisen lisäämällä yhden kaaren, joka alkaa jostain komponentin C alkiosta ja päättyy komponentin B alkioon. A B C 4. Pienin mahdollinen suuntaamaton yhtenäinen verkko on puu. Jos puusta poistetaan kaari, verkko jakautuu kahdeksi komponentiksi. Näin ollen voidaan poistaa joku kaa4 ri, vain siinä tapauksessa, että verkko ei ole puu, eli jossain puussa on sykli ja voidaan poistaa syklistä mielivaltainen kaari. Ongelma ratkeaa siis suoraan muokkaamalla monisteen sivun 487 algoritmia. Koska kyseessä on suuntaamaton yhtenäinen verkko, saadaan verkko käytyä läpi aloittamalla syvyyssuuntainen läpikäynti mielivaltaisesta solmusta lähtien. Heti kun huomataan takautuva kaari, nimenomaan sen voi poistaa verkosta. Algoritmille annetaan solmu v ∈ V , josta aloitetaan läpikäynti. Koska kyseessä on suuntaamaton verkko, ongelmana on kuitenkin se, että kun edetään syvyysuuntaisessa etsinnässä kaarta (u, v) pitkin, niin u on v:n harmaa vierussolmu, joka ei kuitenkaan käy sykliksi, koska sahaamalla edestakaisin yhtä kaarta ei saada aikaan sykliä suuntaamattomassa verkossa. Siksi on mukaan liitetty tieto siitä, mikä on kyseisen solmun isäsolmu syvyyssuuntaispuussa. Algoritmi Search-edge palauttaa false, mikäli ei löydy sykliä, eli mikäli verkko on puu. Delete-edge(G,v) 1 for jokaiselle solmulle u ∈ V 2 color[u] = white 3 parent[v]= NIL 4 if Search-edge(G,v) == false 5 print(’G is a tree’) Search-edge(G,u) 6 color[u] = gray 7 for jokaiselle solmulle v ∈ vierus[u] // kaikille u:n vierussolmuille v 8 if (color[v] == gray) and (v 6= parent) // löytyi sykli, voidaan lopettaa 9 print(’edge (’u’,’v’)’) 10 return true 11 else // solmua v ei vielä löydetty, eli on valkoinen 12 parent[v] = u 13 if Search-edge(G,v) == true 14 return true 15 color[u] = black 16 return false Aikavaativuudelle triviaali yläraja on sama kuin sivun 487 algoritmilla, eli O(|V | + |E|). Tarkempi analyysi kuitenkin paljastaa, että oikeastaan tämä on O(|V |). Nimittäin, algoritmin suoritus loppuu heti, kun sykli huomataan. Näin ollen on siihen mennessä tarkasteltu korkeintaan O(|V |) solmua ja kaarta. Vastaavasti, jos ei löydy sykliä, eli verkko on puu, kaarten lukumäärälle pätee |E| = |V | − 1, eli myös tässä tapauksessa tarkastellaan korkeintaan O(|V |) solmua ja kaarta. 5. Yksinkertaisin tapa tutkia onko verkko haavoittuva on kokeilla poistaa jokainen solmu vuorollaan, ja sen jälkeen tarkastaa verkon läpikäynnillä onko se yhtenäinen. Menetelmä vie aikaa O(|V |2 + |V ||E|). Algoritmi on esitetty ohessa vielä pseudokoodilla. Algoritmissa käytetään aputaulukkoa K merkitsemään onko solmussa jo vierailtu. 5 haavoittuva(G) for jokaiselle solmulle v ∈ V K[v] = true for jokaiselle solmulle x ∈ V−{v} K[x] = false // Nyt vain v on poistettu (merkitty True) Valitaan solmu y ∈ V−{v} lkm = DFS(y) if lkm < |V| − 1 // Kaikissa solmuissa ei vierailtu läpikäynnissä return true return false DFS(v) if K[v] == true return 0 K[v] = true // Merkitään solmu vierailluksi lkm = 1 // Solmuja vierailtu tämän läpikäynnin aikana for jokaiselle v:n naapurille x lkm = lkm + DFS(x) return lkm Ongelman ratkaisuun on olemassa myös yksittäiseen syvyyssuuntaiseen läpikäyntiin pohjautuva tapa, joka vie ajan O(|V | + |E|). Solmu v on haavoittuva (eli sen poisto rikkoo yhtenäisyyden), jos on olemassa kaksi solmua, siten että kaikki polut näiden välillä menevät v:n kautta. Algoritmi tarkastelee syvyyssuuntaisen läpikäynnin muodostamaa puuta. Jos juurella on vähintään kaksi lasta, se on haavoittuva. Muiden solmujen kun juurisolmun osalta voidaan todistaa (Aho, Hopcroft, Ullman: The Design and Analysis of Computer Algorithms, Addison-Wesley, 1974, s. 182-183), että v on haavoittuva, jos ja vain jos jollekin v:n lapselle s pätee, ettei ole takautuvaa kaarta mistään s:n jälkeläisestä (sis. s itse) v:n aitoon edeltäjään. Huomautus: Koska syyvyyssuuntaispuun lehdillä ei ole jälkeläisiä, ne eivät voi olla haavoittuvia. Ohessa on esitetty algoritmi pseudokoodilla. Algoritmi käyttää apunaan aputaulukkoja K, S ja A. Taulukkoon K merkitään onko solmussa jo vierailtu. Taulukkoon S merkitään kunkin solmun syvyystaso läpikäynnin muodostamassa puussa; juurella on syyvyystaso 1. Algoritmin syvyyssuuntaisessa läpikäynnissä pidetään lisäksi kirjaa siitä, tuleeko vastaan takautuvia kaaria. Tätä varten käytetään taulukkoa A, jossa on kyseisen solmun jälkeläisten ja heidän takautuvien kaarien kohdesolmujen pienin syvyystaso. Solmulla v on siis väitteen mukainen takautuva kaari, jos v:n lapsen A-arvo on pienempi kuin v:n syvyystaso. Toisin sanoen, v on haavoittuva jos sen lapselle x pätee A[x] ≥ S[v] (ja v ei ole juuri). haavoittuva2(G) valitaan joku solmu v ∈ V // Syvyyssuuntaispuun juuri K[v] = true S[v] = 1 A[v] = 1 for solmun v naapureille x 6 vieraile(x,2) // Käsitellään vielä juuri erikseen, jos ei ole löydetty haavoittuvia solmuja if v:llä on kaksi tai enemmän lapsia syvyysuuntaispuussa return true else return false vieraile(v,s) K[v] = true // Merkitään solmu vierailluksi S[v] = s A[v] = s for solmun v naapureille x if K[x] == true if x ei ole v:n isäsolmu // On takautuva kaari (v,x) A[v] = min(A[v], S[x]) else vieraile(x,s+1) if (A[x] ≥ S[v]) and v ei ole juuri return true // Solmu v on haavoittuva A[v] = min(A[v], A[x]) 6. Lähestymme ongelmaa muodostamalla ensin verkon G transpoosiverkon GT ja etsimme verkon GT (jokin) alkusolmu. Edellisistä harjoituksista tiedämme, että verkon transpoosiverkon voi muodostaa ajassa O(|V | + |E|). Alkusolmu on solmu, josta on polut kaikkiin verkon solmuihin. Siispä syvyyssuuntaisessa läpikäynnissä kaikki alkusolmun jälkeen löytyvät solmut päätyvät alkusolmun jälkeläisiksi syvyyssuuntaisessa puussa. Näin ollen tiedetään, että alkusolmu voi sijaita vain syvyyssuuntaisen metsän viimeiseksi löydetyssä puussa. Toisaalta, jos syvyyssuuntaisessa puussa jokin solmu u on alkusolmu, ovat myös kaikki sen edeltäjät alkusolmuja, koska niistä on polut verkon kaikkiin muihin solmuihin ainakin solmun u kautta. Yhdistämällä edelliset voidaan päätellä, että joko minkä tahansa syyvyyssuuntaisen metsän viimeiseksi löytyneen puun juuri on alkusolmu tai verkossa ei ole alkusolmuja lainkaan. Solmu voidaan vahvistaa alkusolmuksi aloittamalla syvyyssuuntainen läpikäynti siitä. Jos kaikki solmut päätyvät samaan puuhun, tarkoittaa se sitä, että solmusta on polut verkon kaikkiin muihin solmuihin ja se on näin ollen alkusolmu. Seuraava alkusolmua etsivä algoritmi käyttää hyväkseen luentojen DFS-visit-algoritmia. Se suorittaa ensin syvyyssuuntaisen läpikäynnin alkaen sattumanvaraisesta solmusta. Viimeisenä löytynyt juuri otetaan talteen ja siitä aloitetaan uusi syvyyssuuntainen läpikäynti. Jälkimmäisessä vaiheessa riittää käydä vain niissä solmuissa, jotka ovat saavutettavissa epäillystä alkusolmusta. Mikäli DFS-visit ei käynyt verkon kaikissa solmuissa, ei valittu solmu ollut alkusolmu, eikä verkossa siis ole alkusolmuja. Tässä algoritmin syötteenä oleva verkko G0 on siis alkuperäisen verkon transpoosiverkko GT . StartVertex(G0 ) 1 for jokaiselle u ∈ V 0 2 color[u] = white 7 3 5 6 7 8 10 11 12 13 14 15 16 17 time = 0 root = NIL for jokaiselle u ∈ V 0 if color[u] == white DFS-visit(G0 , u) root = u for jokaiselle u ∈ V 0 color[u] = white DFS-visit(G0 ,root) for jokaiselle u ∈ V 0 if color[u] 6= black return NIL return root Algoritmin aikavaativuus saadaan helposti, kun tiedetään, että syvyyssuuntaisen läpikäynnin aikavaativuus on O(|V | + |E|). Riveillä 1-10 suoritetaan oleellisesti ensimmäinen syvyyssuuntainen läpikäynti ja riveillä 11-13 jälkimmäinen (hieman typistetyssä muodossa). Näiden lisäksi algoritmissa on vakioaikaisia käskyjä ja riveillä 14-16 viimeinen silmukka, jonka aikavaativuus on O(|V |). Siispä algoritmin kokonaisaikavaativuus on O(|V | + |E|). 7. Tehtävä voidaan ratkaista seuraavalla tavalla. Poistetaan alkuperäisestä verkosta t:stä lähtevät kaaret. Muodostetaan näin saadun verkon vahvasti yhtenäiset komponentit (katso luennot) C1 , C2 , . . . , Ck , ja nimetään näistä Ct :ksi se komponentti, joka sisältää ainostaan solmun t. (Komponenttiin ei voi kuulua muita solmuja, koska t:stä ei ole enää lähteviä kaaria muutetussa verkossa.) Komponenttiverkko on verkko, jota saadaan kun kutakin komponentti vastaa solmu ja jos on kaari jostain komponentin A solmusta komponentin B solmuun, niin on kaari vastaavasta solmusta A solmuun B. Muodostamme nyt komponenttiverkon transpoosin (eli kaaret menevät vastakkaiseen suuntaan) ja asetamme kaaripainoksi montako alkuperäistä solmua (eli pääsiäismunaa) komponentti, johon tullaan, sisältää. Muodostettu verkko on nyt syklitön, joten syvyyssuuntaisella läpikäynnillä voi hakea pisin (eli painavin) polku Ct :stä alkaen (ks. luennot: pisin polku DAG:ssa s. 494–498). Jos tällainen pisin polku loppuu komponentissa Cs , niin pelin aloitussolmuksi kelpaa mikä tahansa solmu joukosta Cs . Aikavaativuutta ei kysytty, mutta vahvasti yhtenäiset komponentit löytyvät ajassa O(|V | + |E|). Komponenttiverkon muodostaminen toimii samoin ajassa O(|V | + |E|) (ks. luennot s. 500-508) ja syvyyssuuntainen läpikäynti tehdään (useimmiten pienemmässä) komponettiverkossa, joten koko aikavaativuus on O(|V | + |E|). Tilavaativuutta dominoi uusi komponenttiverkko, joka pahimmillaan on alkuperäisen verkon kokoinen, eli O(|V | + |E|). 8
© Copyright 2024