ratkaisut

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