400 likes | 571 Views
Grafi in algoritmi na njih. Janez Brank. Definicija. G = ( V , E ) je graf V je množica točk (vertices) ali vozlišč (nodes) E je množica povezav (edges) Povezave so lahko: neusmerjene ( u : v ) neusmerjen graf
E N D
Grafi in algoritmi na njih Janez Brank
Definicija • G = (V, E) je graf • V je množica točk (vertices) ali vozlišč (nodes) • E je množica povezav (edges) • Povezave so lahko: • neusmerjene (u:v) neusmerjen graf • usmerjene (u, v) usmerjen graf (directed graph, digraph) • u = začetno krajišče, v = končno krajišče • povezava (u, v) oz. (u:v) je incidenčna na u in v • Povezave imajo včasih še kakšno dodatno lastnost(dolžina, kapaciteta, ipd.) • Ponavadi točke predstavljajo neke stvari,povezave pa neko relacijo nad temi stvarmi • Mnoge zanimive probleme lahko rešimo tako,da v problemu “opazimo” nek graf in prevedemo našproblem na nek problem na grafu • Obstaja veliko algoritmov za razne probleme na grafih
h a i Še nekaj definicij b j k g c d • Če obstajajo povezave (u0, u1), (u1, u2), …, (uk–1, uk), je (u0, u1, …, uk) sprehod (dolžine k). • Če so u0, u1, …, uk same različne točke, je to pot. • Če je še uk = u0, je to obhod (če so u1, …, uk različne, je to cikel). • Graf, v katerem ni nobenega cikla, je acikličen.Usmerjen acikličen graf = DAG (directed acyclic graph). • Nekateri namesto “sprehod” rečejo “pot”, namesto “pot” rečejo “preprosta pot”,namesto “obhod” rečejo “cikel”,namesto “cikel” rečejo “preprost cikel”. • Če obstaja pot od u0 do uk, je ukdosegljiva iz u0. u0 in uk sta (šibko) povezani.Če obstaja hkrati še pot od uk do u0, sta u0 in ukkrepko povezani. • Če obstaja povezava (u, v), je upredhodnikv-ja, v je nasledniku-ja, u in v sta soseda. • Vhodna stopnja (in-degree) točke je število predhodnikov,izhodna stopnja (out-degree) je število naslednikov,stopnja je število vseh sosedov. • Povezava (u, u) je zanka (loop). Ponavadi predpostavimo, da graf nima zank. f e
Še nekaj definicij • Graf na n točkah (torej |V| = n) ima lahko največ toliko povezav: • n(n – 1), če je usmerjen in ne dovolimo zank • n2, če je usmerjen in dovolimo zanke • n(n – 1)/2, če je neusmerjen in ne dovolimo zank • n(n + 1)/2, če je neusmerjen in dovolimo zanke. • Če je število povezav bliže O(n) kot O(n2), pravimo, da je graf redek (sparse), sicer je gost (dense). • Mnogi zanimivi grafi so precej redki: • V državi je 1000 krajev, a iz vsakega kraja vodi le 5 cest. • V Sloveniji je 2 milijona ljudi, a vsakdo ima le nekaj 10 ali 100 znancev.
Predstavitev grafa v računalniku • Kako predstaviti graf v pomnilniku našega računalnika, da bo lahko naš program delal z njim? • Dva glavna načina: • Matrika sosednosti • Seznami sosedov • Pri vsakem je še več različic • Kaj izberemo, je zelo odvisno od tega, • s kakšnimi grafi bomo delali in • kakšne operacije hočemo izvajati na teh grafih.
Matrika sosednosti • Naj bo V = {u0, u1, …, un–1}. • Graf lahko predstavimo z matriko (dvodimenzionalno tabelo) velikosti n×n. • a[i][j] = true, če v grafu obstaja povezava (ui , uj),sicer a[i][j] = false. • Prednosti: • Preprosta implementacija • V času O(1) preverimo, ali neka povezava obstaja (ali pa jo dodamo/pobrišemo) • Slabosti: • Vedno požre O(|V|2) pomnilnika, tudi če je graf redek • Za pregled vseh predhodnikov/naslednikov neke točke porabimo vedno O(|V|) časa [zanka po enem stolpcu/vrstici tabele a] • Za pregled vseh povezav porabimo vedno O(|V|2) časa
Matrika sosednosti – različice • Nekaj različic in potencialnih izboljšav: • Namesto tabele intov ali boolov lahko tabelo zbijemo skupaj tako, da bo vsak par (i, j) zasedel le en bit. • Poraba pomnilnika se zmanjša za nek konstanten faktor,poraba časa pa se poveča za nek konstanten faktor. • Če je graf neusmerjen, je tabela simetrična.Torej, če smo dovolj obupani, lahko prihranimo 50% pomnilnika, vendar pridobimo bolj zapleteno indeksiranje: • Če ima vsaka povezava neko dolžino ali kapaciteto ali kaj podobnega, lahko v tabeli a hranimo tudi to
b a a b c b c d c d e c d e a a b c a e d c e a a a a b b c a e i o u c a e i o u d d c c e e Seznami sosedov • Za vsako točko imamo seznam (linked list) naslednikov in seznam predhodnikov. • Oz. en sam seznam sosedov, če je graf neusmerjen. • Prednosti: • Porabimo le O(|V|+|E|) pomnilnika. • Za pregled vseh sosedov neke točke u porabimo le O(deg(u)) časa. • Za pregled vseh povezav porabimo le O(|V|+|E|) časa. • Slabosti: • Preverjanje, ali je neka povezava (u, v) prisotna, lahko porabi O(deg(u)) časa = O(|V|) v najslabšem primeru (pregled celega seznama sosedov za eno od krajišč). • Enako tudi dodajanje/brisanje povezave (ker jo moramo najprej najti).
Seznami sosedov – različice • Različice in izboljšave: • Vsaka povezava (u, v) se pravzaprav pojavi dvakrat: v je naslednik u-ja in u je predhodnik v-ja. • Pogosto je koristno, če ta dva zapisa kažeta drug na drugega. • Lahko pa imamo celo samo en zapis, ki je hkrati vključen v dva različna seznama. • To je vse lepo in prav, le pri implementaciji moramo biti previdni, da se ne zmotimo pri prevezovanju kazalcev. • Povezave lahko shranimo tudi v hash tabelo, v kateri je ključ par (u, v). • Tako bomo lahko v O(1) preverili, ali povezava obstaja ali ne.[če to seveda potrebujemo] • Zapisi hash tabele lahko kažejo na zapise iz seznamov sosedov (in obratno) ali pa celo kar tiste zapise zdaj povežemo še v verige, ki jih zahteva hash tabela. • Če hočemo v O(1) zbrisati neko povezavo iz vseh verig, je koristno, če so verige dvojno povezane (doubly linked lists).
b a a, b a, c e, c c, d c d e succ a Seznami sosedov – primer b c d e pred a b c d • Konkreten primer:typedef struct edge {int u, v; // krajiščiint dolzina, kapaciteta, itd.; // če potrebujemo kaj od tegastruct edge *prev_u_succ, *next_u_succ; // veriga u-jevih naslednikovstruct edge *prev_v_pred, *next_v_pred; // veriga v-jevih predhodnikovstruct edge *prev_in_hash, *next_in_hash; // veriga povezav, ki se v hash tabeli // preslikajov isto hash kodo kot naša povezava (u, v)};int n, hash_modulo;struct edge **pred, **succ, **hash; • Če vnaprej poznamo neko razumno zgornjo mejo za število povezav, lahko vse zapise edge hranimo v neki tabeli in namesto s kazalci nanje v bodoče delamo z indeksi v to tabelo. e hash 0 1 2 3 hash_code(u, v) = (n· u + v) % hash_modulohash_modulo = 4
b a c d e a b c d e 2 0 1 0 1 izhodna stopnja a b c d e prvi naslednik 0 2 2 3 3 0 1 2 3 nasledniki b c d c Seznami sosedov – bolj kompaktno • Življenje je preprostejše, če nam ni treba podpirati vseh možnih operacij (= mnoge ACM naloge): • Včasih potrebujemo le sezname naslednikov ali pa le sezname predhodnikov. • Včasih nam ne bo treba za poljubne pare točk (u, v) preverjati,ali povezava obstaja ali ne • Včasih grafa sploh ne bomo spreminjali(ali pa bomo le brisali povezave) in ga bomo le na začetku prebrali iz neke datoteke. • Takrat lahko vse sezname naslednikov zbijemo skupaj v eno samo tabelo (nasledniki v spodnjem primeru).
Seznami sosedov – bolj kompaktno • int n, m; // n = število točk, m = število povezavfscanf("%d %d", &n, &m); // preberemo število točk in povezavint *edgeFrom = new int[m], *edgeTo = new int[m]; // alokacija tabelint *inDeg = new int[n], *outDeg = new int[n];for (int u = 0; u < n; u++) inDeg[u] = outDeg[u] = 0; • // Preberemo povezave, izračunamo stopnje vseh točk.for (int i = 0; i < m; i++) {int u, v; fscanf("%d %d", &u, &v); edgeFrom[i] = u; edgeTo[i] = v; inDeg[v]++; outDeg[u]++; } • // Izračunamo, kje se v tabeli succ začnejo nasledniki posamezne točke u (namreč pri firstSucc[u]).int *firstSucc = new int[n], *succ = new int[m];firstSucc[0] = 0;for (int u = 1; u < n; u++) firstSucc[u] = firstSucc[u – 1] + outDeg[u – 1]; • // Vpišemo naslednike vsake točke na pravo mesto v tabelo succ. for (int u = 0; u < n; u++) outDeg[u] = 0;for (int i = 0; i < m; i++) {int u = edgeFrom[i], v = edgeTo[i]; succ[firstSucc[u] + outDeg[u]] = v; outDeg[u]++; }delete[] edgeFrom; delete[] edgeTo;
Implicitna predstavitev grafa • Včasih ima graf tako regularno zgradbo, da ga sploh ni treba predstaviti eksplicitno: • Če imamo neko pravilo, ki nam za poljubni dve točki pove, ali obstaja med njima povezava ali ne • Če imamo nek postopek, ki nam za poljubno točko našteje vse njene predhodnice/naslednice/sosede • To je pogost primer pri grafih, ki predstavljajo “prostor stanj” nekega sistema: • Vsaka točka je eno od stanj sistema • Povezava od u do v pomeni, da se lahko stanje sistema v enem koraku spremeni iz u v v(npr. zaradi nekega dogodka, dejanja, premika ipd.)
Algoritmi na grafih • Nekaj problemov, ki pogosto pridejo prav: • Topološko urejanje: iščemo vrstni red točk, da bo začetno krajišče vsake povezave v vrstnem redu pred končnim • Iskanje v širino: sistematično pregledati vse, kar je dosegljivo iz neke začetne točke • Iskanje v globino: kot iskanje v širino, a v drugačnem vrstnem redu • Iskanje najkrajših poti: vsaka povezava ima dolžino, iščemo najkrajše poti med točkami • Iskanje (šibko) povezanih komponent: skupine točk, za katere je vsaka točka skupine dosegljiva iz vsake druge (v neusmerjenem grafu) • Še drugi zanimivi problemi (za katere danes ne bo časa): • Minimalno vpeto drevo: iščemo množico povezav, tako da bo vsota njihovih dolžin čim manjša in da bo vsaka točka incidenčna na vsaj eno od njih • Barvanje grafa: vsaki točki hočemo pripisati neko barvo, tako da sosednji točki nimata nikoli iste barve in da porabimo čim manj barv • Maksimalni pretok po grafu: vsaka povezava ima kapaciteto, hočemo čim večji pretok od izvora do ponora, tovor se ne sme nikjer izgubljati ali kopičiti • Iskanje krepko povezanih komponent: skupine točk, za katere je vsaka točka skupine dosegljiva iz vsake druge (v usmerjenem grafu) • Tranzitivna redukcija, minimalni ekvivalentni podgraf:hočemo obdržati čim manj povezav, ne da bi bila prizadeta dosegljivost • Izomorfizem grafov: hočemo preslikati en graf v drugega, pri tem pa spoštovati sosednost: u je soseda vf(u) je soseda f(v)
d a e g f b h c a b c e f d g h Topološko urejanje • Dan je usmerjen graf • Iščemo “topološki vrstni red”:to je tak vrstni red točk, v katerem za vsako povezavo (u, v) E velja, da je u v našem vrstnem redu pred v • Če točke narišemo v tem vrstnem redu od leve proti desni, kažejo vse povezave v desno • Če obstaja cikel (u0, u1, …, uk–1, u0),bi moral biti u0 pred u1, ta pred u2, …, ta pred uk–1, ta pred u0 • Torej bi moral biti u0 pred samim sabo • Torej grafa s ciklom ne moremo topološko urediti • S topološkim urejanjem lahko torej preverjamo, če ima graf kak cikel • Lahko tudi iščemo najkrajše poti po njem (tudi če imajo povezave različno dolžino)
Topološko urejanje • Ideja: • Prva točka v topološkem vrstnem redu mora imeti vhodno stopnjo 0. • Vzemimo torej poljubno tako točko, jo postavimo na začetek topološkega vrstnega reda;odslej nas njene povezave ne bodo več motile, zato v mislih to točko in njene povezave pobrišimo iz grafa. • Graf ima zdaj eno točko manj, ponovimo isti postopek, itd. • V praksi: • Povezav ni treba zares brisati – dovolj je, če si zapomnimo, kolikšno vhodno stopnjo bi imela neka točka, če bi povezave res brisali. • V neki vrsti Q bomo hranili točke, za katere že vemo, da imajo vhodno stopnjo 0, nismo pa jih še (v mislih) pobrisali iz grafa. • Postopek:for eachv V: inDeg[v] := 0; for each (u, v) V: inDeg[v] := inDeg[v] + 1;Q := prazna vrsta;for eachv V: ifinDeg[v] = 0 then Enqueue(Q, v); whileQ ni prazna:u := Dequeue(Q); printu;for vsako u-jevo naslednico v:inDeg[v] := inDeg[v] – 1; ifinDeg[v] = 0 then Enqueue(Q, v); • Za vrsto Q lahko uporabimo kar tabelo ter dva indeksa head, tail • Na koncu iz te tabele kar odčitamo enega od možnih topoloških vrstnih redov • Če je v grafu kak cikel, se bo postopek končal, še preden bo dodal v vrsto vse točke
d a d d a e a g e g e g f b f h b f b h c h c 0 1 2 3 4 5 6 7 d c a e a head = 0, tail = 1 d e a g a e b head = 1, tail = 2 e g a e b f g f b head = 2, tail = 4 h f b a e b f g c h h head = 3, tail = 6 c d a e b f g c h d c a head = 4, tail = 7 d e a g a e b f g c h d head = 5, tail = 7 e g a e b f g c h d f b head = 6, tail = 7 h f b a e b f g c h d d d h head = tail = 7 a a c e e a e b f g c h d c g g head = 8, tail = 7 f f b b h h c c Primer topološkega urejanja Enqueue a, e Dequeue aEnqueue b Dequeue eEnqueue f, g Dequeue bEnqueue c, h Dequeue fEnqueue d Dequeue g Dequeue c Dequeue h Dequeue d
Primer uporabe topološkega urejanja • acm.timus.ru, #1337 • Imamo n uradnikov. Uradnik i dela le en dan v tednu – na dan ai. Preden obiščemo uradnika i, moramo obiskati vse uradnike iz množice Pi.Teden ima L dni. Danes je dan k. Radi bi čim prej obiskali vse uradnike iz množice M. Koliko dni bo trajalo? • Rešitev: definirajmo usmerjen graf: V = {1, …, n},E = {(u, v) : u Pv} • Če zdaj pregledujemo uradnike v topološkem vrstnem redu, lahko za vsakega uradnika u brez težav določimo najzgodnejši možni datum obiska, ker takrat že vemo najzgodnejši datum obiska za vse, ki jih moramo obiskati pred njim (Pu) • Na koncu vrnemo najkasnejši dan po vseh uradnikih iz M
Iskanje v širino (breadth-first search, BFS) • Je način, kako sistematično pregledati vse točke, ki so dosegljive iz neke začetne točke s. • Postopek:Q := prazna vrsta;for eachv V: seen[v] := false;Q.Enqueue(s); seen[s] := true;whileQ ni prazna:u := Q.Dequeue(s); printu;for vsako u-jevo naslednico v:if notseen[v]:Q.Enqueue(v); seen[v] := true; • Najprej izpiše s, nato vse s-jeve naslednice, nato vse točke, ki so dosegljive iz s v dveh korakih, ne pa v enem,nato vse točke, ki so dosegljive iz s v treh korakih, ne pa v dveh ali manj,itd. • Vrsto lahko implementiramo z linked listo, še lažje pa je s tabelo(dolga mora biti največ |V| elementov, za glavo in rep vrste vodimo dva indeksa).
0 1 2 3 4 5 6 7 8 9 10 11 12 13 e head = tail = 0 a b b c e d head = 1, tail = 2 c b c e head = 2, tail = 2 k g e b c e g d head = 3, tail = 4 f b c k e g d h l m head = 4, tail = 8 i l b c k e g d h l m f head = 5, tail = 9 h b c k e g d h l m f i head = 6, tail = 10 n m b c k e g d h l m f i n j head = 7, tail = 11 b c k e g d h l m f i n head = 8, tail = 11 b c k e g d h l m f i n head = 9, tail = 11 b c k e g d h l m f i n head = 10, tail = 11 b c k e g d h l m f i n head = tail = 11 b c k e g d h l m f i n head = 12, tail = 11 0 1 2 3 4 oddaljenost od s Primer iskanja v širino Enqueue e • Lahko bi v neki tabeli tudi hranili dolžino najkrajše poti od s do vsake točke • Lahko bi si tudi zapomnili, od kod smo v neko točko prišli • Na koncu bi iz tega rekonstruirali potek najkrajših poti od s do vseh ostalih točk • Npr. v i smo prišli iz h, v h iz g, v g iz c, v c iz e • Časovna zahtevnost: O(|V|+|E|) Dequeue eEnqueue b, c Dequeue b Dequeue cEnqueue g, d Dequeue gEnqueue h, k, l, m Dequeue dEnqueue f Dequeue hEnqueue i Dequeue kEnqueue n Dequeue l Dequeue m Dequeue f Dequeue i Dequeue n
Primer uporabe iskanja v širino • acm.uva.es, #310: • Dani so nizi s, t, u, sestavljeni le iz črk a in b. • Naj bo f(w) niz, ki ga dobimo tako, da v w-ju vsak a zamenjamo z nizom t, vsak b pa z nizom u. • Vprašanje: ali je dani niz x podniz kakšnega od nizov s, f(s), f(f(s)), f(f(f(s))), …? x je dolg največ 15 znakov. • Rešitev: definirajmo usmerjen graf:V = {vsi nizi iz črk a in b, dolgi največ toliko kot x}E = {(u, v) : v je podniz niza f(u)} • Naloga se prevede na vprašanje, ali je x v tem grafu dosegljiv iz s, kar lahko preverimo z iskanjem v širino.
Še en primer uporabe iskanja v širino • acm.uva.es, #321 • Imamo hišo z n 10 sobami. • Znani so pari sob, za katere sta sobi v paru neposredno povezane z vrati. • V nekaterih sobah so tudi stikala, i-to stikalo je v sobi si in prižiga/ugaša luč v sobi ti. • 1 korak = da stopiš iz ene sobe v drugo (skozi vrata) ali pa prižgeš/ugasneš luč z enim od stikal v trenutni sobi • V sobo lahko stopiš le, če v njej gori luč • Znano je, v kateri sobi si na začetku in kakšno je stanje vseh luči. Znano je tudi, v katero sobo bi rad prišel in kakšno naj bo takrat stanje vseh luči.Dosezi to v čim manj korakih. • Rešitev: V = {1, …, n}{0,1}n – vsaka točka grafa predstavlja eno možno stanje sistema (naš položaj in stanje vseh stikal) • Povezava od enega stanja do drugega obstaja, če lahko mi pridemo iz enega v drugo stanje z enim korakom. • Ostane le še iskanje najkrajše poti, npr. z iskanjem v širino.
Povezane komponente • V neusmerjenem grafu: • Točki u in v sta povezani ntk. obstaja med njima neka pot. • V usmerjenem grafu: • Točki u in v sta krepko povezani ntk. obstaja neka pot od u do vin še neka pot od v do u (ob upoštevanju smeri povezav). • Točki u in v sta šibko povezani ntk. obstaja neka pot med u in v, če zanemarimo smer povezav (torej če se delamo, da je graf neusmerjen). • (Krepko, šibko) povezana komponenta: • Je množica točk, ki so vse med sabo paroma (krepko, šibko) povezane • in v katero ne moremo dodati nobene nove točke, brez da bi prejšnji pogoj prenehal veljati. • Ogledali si bomo algoritem za povezane komponente v neusmerjenem grafu • Lahko ga uporabimo tudi za šibko povezane komponente v usmerjenem grafu • Če hočemo učinkovit algoritem za krepko povezane komponente, je stvar malo bolj zapletena
Povezane komponente • Postopek je preprost: • Začnemo pri poljubni točki in z iskanjem v širino (ali pa v globino, saj je vseeno) obiščemo vse, kar je dosegljivo iz nje. • To je ena povezana komponenta. • Zdaj začnemo pri poljubni točki, ki ni iz te komponente, in na enak način dobimo drugo povezano komponento, itd. • Postopek:ŠtKomponent := 0;for eachvV: komponenta[v] := –1;for eachs V:ifkomponenta[s] = –1:Q := prazna vrsta; Enqueue(Q, s); komponenta[s] := ŠtKomponent;whileQ ni prazna:u := Dequeue(Q);for vsako u-jevo naslednico v:ifkomponenta[v] = –1: Enqueue(Q, v); komponenta[v] := ŠtKomponent;ŠtKomponent := ŠtKomponent + 1; • Časovna zahtevnost: O(|V| + |E|)
Primer naloge s povezanimi komponentami • acm.uva.es, #10583 • Imamo n ljudi. Za nekatere pare ljudi vemo, da sta človeka iste vere. • Ne vemo pa točno, katere vere je kdo – še tega ne vemo, koliko različnih ver sploh je. Ugotovi največje možno število različnih ver. • Rešitev: definirajmo neusmerjen graf V = {1, …, n}, E = {(u:v) : za u in v vemo, da sta iste vere}. • Vsi ljudje iz neke povezane komponente tega grafa morajo biti iste vere. Različnih ver je torej največ toliko, kolikor je povezanih komponent v tem grafu.
Iskanje v globino (depth-first search, DFS) • Podobno kot iskanje v širino, le da obiskuje točke v drugačnem vrstnem redu. • Če ima u dva naslednika, v in w, bomo šli najprej v v in nato obiskali vse točke, dosegljive iz njega, preden se bomo lotili w-ja. • Postopek:inicializacija:for eachv V: barva[v] := bela;algoritem DFS(u):barva[u] := siva; print u;for vsako u-jevo naslednico v:ifbarva[v] = bela: DFS(v);barva[u] := črna;glavni klic: DFS(s); • Namesto rekurzije imamo lahko tudiiteracijo, točke pa odlagamo na sklad(pri vsaki si tudi zapomnimo, do katerenaslednice smo pri njej že prišli) • Vedno velja (za vsako točko u): • če je u bela, je še nismo izpisali; • če je u siva, smo jo že izpisali; • če je u črna, smo izpisali že njo in vse, kar je dosegljivo iz nje.
a a a a b b a a b b a a b b d d b b d d d d d d c c c c k k c c c c k k g g e e k k g g k k e e g g e e g g e e f f f f f f f f i i l l i i l l i i l l i i l l h h h h a h h h h b n n m m n n d n n j j m m n n m m j j m m a j j a a j j c a b b b k b a a a a a a a d g b b b b b a a e d d e a b b a d b a b b a d d d d d c b b d d f c c b d c k d d k k a d c g c c c c c k d e g g c c b e e g c i k k k k k e l c c k k g g g g g d c k g d e e e e e f g g c k k e e f f g k e f g g k h e e g f h l e f f f f f g c e f f i f l i i k n f f l l i i k j g l f m e f i i i i i j l l l l l i i h l l i h h m n l i i h f l l i l i h h h h h n l h h n n m h n m m h h j m i h j j n n n n n l h j n n m m m m m n m m j j j j j n n m j j n m m h j n m j j m j j n m j Primer iskanja v globino • e, b, c, g, h, i, m, l, k, n, d, f • Dobili smo tudi “DFS drevo” • Za vsako povezavo (u, v) E velja eno od naslednjega: • ena od u in v je prednica druge v DFS drevesu • [le pri usmerjenih grafih]v (in celo njeno poddrevo) je bila obiskana prej kot u • Z drugimi besedami: če povezava (u, v) kaže “naprej” po DFS vrstnem redu, je v potomka u(ne more biti iz nekega drugega poddrevesa)
Iskanje v globino • Časovna zahtevnost je spet O(|V|+|E|),enako kot pri iskanju v širino • Koristno za odkrivanje ciklov: • Če pri pregledovanju u-jevih naslednic opazimo neko v, ki je že siva, pomeni, da je v grafu cikel (ki ga lahko kar odčitamo s sklada) • Če v grafu obstaja kak cikel, dosegljiv iz s, se nam bo gornji primer med preiskovanjem gotovo zgodil • Iskanje v globino se uporablja tudi pri: • odkrivanju krepko povezanih komponent • topološkem urejanju[če uredimo točke po tem, kdaj je DFS končal s tisto točko in njenim poddrevesom, in na koncu ta vrstni red obrnemo] • odkrivanju artikulacijskih točk (točk, pri katerih nam graf razpade na več nepovezanih delov, če tisto točko pobrišemo)
c a a b b e d c d f e f g n g j i j h h k k n l i l m m Artikulacijske točke • Dan je neusmerjen, povezan graf • Če v grafu pobrišemo neko točko u, se lahko zgodi, da razpade na več povezanih komponent • Takšne u se imenujejo “artikulacijske točke” • Preprost algoritem za odkrivanje artikulacijskih točk: • Za vsako u V: delajmo se, da smo u pobrisali iz grafa, in preverimo, če je še vedno povezan • O(|V|(|V| + |E|)) • Učinkovitejši algoritem: izvedimo DFS, oglejmo si DFS drevo • Koren je artikulacijska točka ntk. ima več kot enega otroka • Za poljubno drugo točko v: • Poglejmo vsakega od njenih otrok, w, v DFS drevesu • Če ni nobene povezave med kakšno točko iz w-jevega poddrevesa in kakšno točko u v zunaj w-jevega poddrevesa, potem je v artikulacijska točka • Če pa taka povezava obstaja, je u ena od prednic v-ja • Zato je mogoče takšne povezave učinkovito odkrivati, če točke oštevilčimo v takem vrstnem redu,v kakšnem jih je DFS obiskal – prednike potem prepoznamo po manjših številkah • To se da implementirati v O(|V|+|E|)
Iskanje najkrajših poti • Recimo, da ima vsaka povezava (u, v) Eneko dolžinod(u, v) • Če (u, v) E, si mislimo d(u, v) = • Dolžina poti (u0, u1, …, uk) je Si=1..kd(ui–1, ui) • Problem najkrajših poti: • Za dano točko s nas zanimajo najkrajše poti od s do vseh ostalih točk [single-source shortest paths] • Ali pa: za vsako točko s nas zanimajo najkrajše poti od s do vseh ostalih točk [all-pairs shortest paths] • Nekaj tega smo že videli: • Če so vse povezave enako dolge (d(u, v) = 1 za vsako (u, v) E), lahko uporabimo iskanje v širino • Če je graf acikličen, lahko uporabimo topološko urejanje • Videli pa bomo še nekaj splošnejših postopkov
u0 p Iskanje najkrajših poti uk–1 r (?) uk • Če je p = (u0, u1, …, uk–1,uk) najkrajša pot od u0 do uk, je (u0, u1, …, uk–1) najkrajša pot od u0 do uk–1 • Res: če bi obstajala od u0 do uk–1 neka krajša pot r, bi jo lahko podaljšali s korakom (uk–1, uk) in tako dobili neko pot od u0 do uk, ki bi bila krajša od p (protislovje). • Torej je vsaka najkrajša pot podaljšek neke druge najkrajše poti. • Najkrajše poti od s do vseh drugih točk torej tvorijo “drevo najkrajših poti”. • Vse, kar moramo storiti, je, da za vsako u najdemo njeno predhodnico na najkrajši poti od s do u.
Iskanje najkrajših poti s topološkim urejanjem • Recimo, da oštevilčimo točke v topološkem vrstnem redu: u0, u1, …, un • Postopek:fori := 0 ton – 1: // zdaj za u0, …, ui–1 že poznamo najkrajše poti (v tabeli d)p[ui] := nil;ifui = sthend[ui] := 0 elsed[ui] := ;for vsako ui-jevo predhodnico uj: // gotovo je j < i (zaradi topološkega vrstnega reda)ifd[uj] + d(uj, ui) < d[ui]:d[ui] := d[uj] + d(uj, ui); p[ui] := uj; • Invarianta je, da na začetku i-te iteracije glavne zanke za vsak j < i že poznamo dolžino najkrajše poti od s do uj; ta dolžina je d[uj], predhodnica točke uj na tej poti pa je točka p[uj] • V praksi ni treba najprej izvesti topološkega urejanja in nato zgornje zanke,ampak lahko počnemo oboje hkrati(med topološkim urejanjem iščemo še najkrajše poti)
Dijkstrov algoritem • Graf si mislimo razdeljen na tri dele, črnega, sivega in belega. • Invarianta: • Če je učrna, je d[u] dolžina najkrajše poti od s do u, p[u] pa je predhodnica u-ja na tej poti.Poleg tega so črne tudi vse točke v, za katere je najkrajša pot od s do v krajša kot d[u]. • Sive točke so vse tiste, ki niso črne, pač pa se da do njih priti v enem koraku iz kakšne črne točke.Za vsako sivo u je d[u] dolžina najkrajše take poti od s do u, ki gre ves čas po črnih točkah, le zadnji korak stopi iz črne v sivo; p[u] pa je predhodnica u-ja na tej poti. • Ostale točke so bele. • Postopek:for eachv V: barva[v] := bela; d[v] := ; p[v] := nil;barva[s] := siva; d[v] := 0;while obstaja še kaj sivih točk:u := med vsemi sivimi točkami tista z najmanjšo d[u];barva[u] := črna; (*)for vsako u-jevo naslednico v:če je v siva ali bela: če je d[u] + d(u, v) < d[v]:d[v] := d[u] + d(u, v); p[v] := u; barva[v] := siva; (*) • Za večjo učinkovitost je dobro hraniti vse sive točke v prioritetni vrsti (npr. v kopici – heap). • Če tega nimamo, bo moral biti dober tudi navaden seznam ali tabela • Pogoj za to, da se invarianta ohrani, ko v (*) spreminjamo barve točk, je, da so dolžine vseh povezav ≥ 0.Drugače lahko vrne Dijkstrov algoritem napačne rezultate. • Časovna zahtevnost: O((|V|+|E|) log |V|) s kopico, O(|V|2+|E|) = O(|V|2) brez nje s u v
Posplošitev problema najkrajših poti • Za pot p = (u0, u1, …, uk) definirajmo neko ceno J(p). • Za dano točko s iščemo najcenejše poti od s do vseh ostalih točk. • Če velja naslednje: • J(u0, …, uk–1, uk) = f(J(u0, …, uk–1), uk–1, uk) za neko funkcijo f(t, u, v). ki jo poznamo • J(u0, …, uk–1, uk) ≥ J(u0, …, uk–1) • če je t < t', je f(t, u, v) f(t', u, v) [To nam zagotavlja, da če je p najcenejša pot od s do vin je v-jeva predhodnica na p neka točka u, potem je preostanek te poti najcenejša pot od s do u.] • …lahko iščemo najboljše poti z Dijkstro. • Za običajne najkrajše poti je f(t, u, v) = t + d(u, v).
Primer z iskanjem najkrajših poti • acm.uva.es, #10621 • Človeka A in B hodita po karirasti mreži 30 30, v vsakem koraku se vsakdo premakne iz trenutnega polja v eno od štirih sosednjih polj. • Za vsakega je podan začetni in zahtevani končni položaj na mreži (zA, zB, kA, kB). • Predlagaj jima tako pot, da: • Bosta obe poti enako dolgi (enako število korakov) • Če je d(t) razdalja med človekoma po t korakih, naj bo minimum d(t) po vseh t čim večji. • Rešitev:V = {(xA, yA, xB, yB) : vse koordinate med 1 in 30}E = {vsaka točka ima 44 sosede, ki ustrezajo možnim premikom A-ja in B-ja} • Če je u = (xA, yA, xB, yB), definirajmo j(u) := ((xA –xB)2 + (yA – yB)2) • Definirajmo J(u0, …, uk–1, uk) = –min{j(u1), j(u2), …, j(uk)}.Naš problem se prevede na iskanje najcenejše poti od dane začetne do dane ciljne točke. • Hitro se vidi, da J ustreza pogojem s prejšnje folije • Računamo jo s pomočjoJ(u0, …, uk–1, uk) = –min{j(u1), j(u2), …, j(uk)} = –min{min{j(u1), j(u2), …, j(uk–1)}, j(uk)} = max{–min{j(u1), j(u2), …, j(uk–1)}, –j(uk)} = max{J(u0, …, uk–1), –j(uk)},torej uporabimo f(t, u, v) = max{t, –j(v)}
Bellman-Fordov algoritem • Ideja: glejmo najkrajše poti, sestavljene iz največ k korakov • Pri k = 0 je stvar trivialna – možna je le pot od s do s • Drugače pa je najkrajša pot od s do v s k koraki bodisi: • Dolga manj kot k korakov • Dolga natanko k korakov in je zato podaljšek najkrajše poti od s do neke u s k – 1 koraki • Postopek:for eachv V: d[v] := ; p[v] := nil;d[s] := 0;fork := 1 to |V|: // Invarianta: d[v] je dolžina najkrajše poti od s do v z največ k – 1 koraki. // Izračunajmo najkrajše poti od s do vseh točk z največ k koraki.for eachv V: d'[v] := d[v]; p'[v] := p[v];for each (u, v) E:ifd[u] + d(u, v) < d'[v]:d'[v] := d[u] + d(u, v); p'[v] := u;for eachv V: d[v] := d'[v]; p[v] := p'[v]; • Časovna zahtevnost: O(|V||E|)
s 8 5 –3 1 –4 20 v Bellman-Fordov algoritem • Če na koncu neke iteracije glavne zanke opazimo, da sta tabeli d in d' povsem enaki, • Pomeni, da s k koraki ni mogoče dobiti nobene krajše poti kot s k – 1 koraki. • Torej se tudi v nadaljnjih iteracijah ne bo nič spremenilo • Lahko se kar takoj ustavimo. • Če se nam to ne zgodi niti v zadnji iteraciji (ko je k = |V|): • Pomeni, da je neka pot od s do neke v z |V| koraki krajša kot katerakoli pot od s do te v z manj kot |V| koraki • Toda pot z |V| koraki gotovo vsebuje nek cikel • Če ta cikel pobrišemo, ima pot manj kot |V| korakov, torej je daljša • Nekaj smo pobrisali, pot pa je daljša?! • Da, ker imamo negativni cikel.Večkrat ko gremo po njem, krajša bo pot. Najkrajša pot torej sploh ne obstaja. • Bellman-Ford nam torej omogoča tudi opaziti, če je iz s dosegljiv kakšen negativni cikel.
Najkrajše poti med vsemi pari točk • Lahko poženemo kakšnega od dosedanjih algoritmov |V|-krat, vsakič z drugo s • Lahko prilagodimo Bellman-Forda: • Naj bo dk[u, v] dolžina najkrajše poti od u do v z največ k koraki, pk[u, v] pa v-jeva predhodnica na tej poti. • Za d1[u, v] vemo, da je d1[u, v] = 0 pri u = v, d1[u, v] = d(u, v) sicer.p1[u, v] = u. • Za večje k ga lahko računamo takole:algoritem Tralala(r, t):for eachu V, for eachv V:dr+t[u, v] := ; pr+t[u, v] := nil;for eachw V:ifdr[u, w] + dt[w, v] < ds+t[u, v]:dr+t[u, v] := dr[u, w] + dt[w, v]; pr+t[u, v] := pt[w, v]; • Z algoritmom Tralala lahko iz dr in dt v času O(|V|3) dobimo dr+t.Na začetku poznamo d1, iz tega lahko izračunamo d2, d4, d8, d16, d32, …Nas pa zanima d|V|, torej moramo le še pogledati, katere potence števila 2 moramo sešteti, da pride vsota |V|. • Algoritem Tralala bomo poklicali največ O(log |V|)-krat. Skupaj torej O(|V|3 log |V|).
Floyd-Warshallov algoritem • Za najkrajše poti med vsemi pari točk • Oštevilčimo točke: V = {u0, u1, …, un–1} • Naj bo dk[i, j] dolžina najkrajše poti od ui do uj, ki med njima ne obiskuje točkuk, uk+1, … • Trivialni podproblemi: d0[i, j] = d(ui, uj) • Za k > 0 pa preverimo dve možnosti: • Taka pot mogoče obišče uk–1 (prej in potem pa je to pot, ki ne obiskuje uk–1, uk, …) • Ali pa je ne obišče • Postopek:for i := 0 ton – 1, forj := 0 ton – 1: d0[i, j] := d(ui, uj);fork := 0 ton – 1:for i := 0 ton – 1, forj := 0 ton – 1:dk[i, j] := min{dk–1[i, j], dk–1[i, k] + dk–1[k, j]}; // zdaj lahko dk–1 že zavržemo, ker ga ne bomo več potrebovali • Če se kdaj zgodi, da je dk[i, i] < 0, imamo negativni cikel. • Časovna zahtevnost je le O(|V|3).
Pregled časovne zahtevnostialgoritmov za najkrajše poti • Najkrajše poti Najkrajše poti Algoritem od ene do ostalih med vsemi pari točkiskanje v širino O(V+E) O(V2 +VE) topološko urejanje O(V+E) O(V2 +VE) Dijkstra brez kopice O(V2+E) O(V3 +VE)Dijkstra s kopico O(E log V) O(VE log V)Bellman-Ford O(VE) O(V2E)Bellman-Ford za APSP O(V3 log V)Floyd-Warshall O(V3)[Spomnimo se: E = O(V) za redke grafe, E = O(V2) za goste.]