2fe29957124cb9c647f99dd21f51e627.ppt
- Количество слайдов: 40
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 Še nekaj definicij • Če obstajajo povezave (u 0, u 1), (u 1, u 2), …, (uk– 1, uk), je (u 0, u 1, …, uk) sprehod (dolžine k). b j k g c d e i f – Če so u 0, u 1, …, uk same različne točke, je to pot. • Če je še uk = u 0, je to obhod (če so u 1, …, 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 u 0 do uk, je uk dosegljiva iz u 0 in uk sta (šibko) povezani. Če obstaja hkrati še pot od uk do u 0, sta u 0 in uk krepko povezani. • Če obstaja povezava (u, v), je u predhodnik v-ja, v je naslednik u-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.
Š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 n 2, č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(n 2), 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 = {u 0, u 1, …, 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 Seznami sosedov c d e a b c d e b d c a b c d e • Za vsako točko imamo seznam (linked list) naslednikov in seznam predhodnikov. c a a e c – 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). a b c d e a a c e i o u
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).
Seznami sosedov – primer a succ b c e d pred hash • Konkreten primer: a b c d e 0 1 2 3 a, b a, c e, c hash_code(u, v) = (n· u + v) % hash_modulo = 4 typedef struct edge { int u, v; // krajišči int dolzina, kapaciteta, itd. ; // če potrebujemo kaj od tega struct edge *prev_u_succ, *next_u_succ; // veriga u-jevih naslednikov struct edge *prev_v_pred, *next_v_pred; // veriga v-jevih predhodnikov struct edge *prev_in_hash, *next_in_hash; // veriga povezav, ki se v hash tabeli // preslikajo v 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. c, d
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). a izhodna stopnja c e a 2 b 0 c 1 d 0 e 1 prvi naslednik b a 0 b 2 c 2 d 3 e 3 nasledniki 0 b 1 c 2 d 3 c d
Seznami sosedov – bolj kompaktno • int n, m; // n = število točk, m = število povezav fscanf("%d %d", &n, &m); // preberemo število točk in povezav int *edge. From = new int[m], *edge. To = new int[m]; // alokacija tabel int *in. Deg = new int[n], *out. Deg = new int[n]; for (int u = 0; u < n; u++) in. Deg[u] = out. Deg[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); edge. From[i] = u; edge. To[i] = v; in. Deg[v]++; out. Deg[u]++; } • // Izračunamo, kje se v tabeli succ začnejo nasledniki posamezne točke u (namreč pri first. Succ[u]). int *first. Succ = new int[n], *succ = new int[m]; first. Succ[0] = 0; for (int u = 1; u < n; u++) first. Succ[u] = first. Succ[u – 1] + out. Deg[u – 1]; • // Vpišemo naslednike vsake točke na pravo mesto v tabelo succ. for (int u = 0; u < n; u++) out. Deg[u] = 0; for (int i = 0; i < m; i++) { int u = edge. From[i], v = edge. To[i]; succ[first. Succ[u] + out. Deg[u]] = v; out. Deg[u]++; } delete[] edge. From; delete[] edge. To;
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 v f(u) je soseda f(v)
d a Topološko urejanje e g f b h • 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 (u 0, u 1, …, uk– 1, u 0), bi moral biti u 0 pred u 1, ta pred u 2, …, ta pred uk– 1, ta pred u 0 • Torej bi moral biti u 0 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) c a b c e f d g h
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 each v V: in. Deg[v] : = 0; for each (u, v) V: in. Deg[v] : = in. Deg[v] + 1; Q : = prazna vrsta; for each v V: if in. Deg[v] = 0 then Enqueue(Q, v); while Q ni prazna: u : = Dequeue(Q); print u; for vsako u-jevo naslednico v: in. Deg[v] : = in. Deg[v] – 1; if in. Deg[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
Primer topološkega urejanja 0 1 2 3 a e head = 0, tail = 1 4 5 6 7 Dequeue a Enqueue b a e b head = 1, tail = 2 a a a a e b f g head = 2, tail = 4 e e e b b b f g c h head = 3, tail = 6 f f f Enqueue a, e g c h d head = 4, tail = 7 g c h d head = 5, tail = 7 g c h d head = 6, tail = 7 g c h d head = 8, tail = 7 Dequeue e Enqueue f, g d a Dequeue b Enqueue c, h e g Dequeue f Enqueue d f b h Dequeue g Dequeue c Dequeue h Dequeue d c
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 each v V: seen[v] : = false; Q. Enqueue(s); seen[s] : = true; while Q ni prazna: u : = Q. Dequeue(s); print u; for vsako u-jevo naslednico v: if not seen[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).
Primer iskanja v širino a b d c 0 1 2 3 e head = tail = 0 4 5 6 7 8 9 10 11 12 13 Dequeue e Enqueue b, c e b c head = 1, tail = 2 e b e e Dequeue c Enqueue g, d c g d head = 3, tail = 4 c b c b c Dequeue g Enqueue h, k, l, m d h k l m head = 4, tail = 8 g g d g d h h Dequeue d Enqueue f k l m f head = 5, tail = 9 Dequeue h Enqueue i k l m f i head = 6, tail = 10 k k Dequeue k Enqueue n l m f i n head = 7, tail = 11 l Dequeue l m f i n head = 8, tail = 11 e b c g d h k l m Dequeue m m f i n head = 9, tail = 11 e e 0 b c 1 g d 2 f i Dequeue b c head = 2, tail = 2 h h k k l l 3 m m Dequeue f f i n head = 10, tail = 11 f f Dequeue i i n head = tail = 11 Dequeue n i n head = 12, tail = 11 4 oddaljenost od s k g e Enqueue e l h j m n • 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|)
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 v in š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: Št. Komponent : = 0; for each v V: komponenta[v] : = – 1; for each s V: if komponenta[s] = – 1: Q : = prazna vrsta; Enqueue(Q, s); komponenta[s] : = Št. Komponent; while Q ni prazna: u : = Dequeue(Q); for vsako u-jevo naslednico v: if komponenta[v] = – 1: Enqueue(Q, v); komponenta[v] : = Št. Komponent; Št. Komponent : = Št. Komponent + 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 each v V: barva[v] : = bela; algoritem DFS(u): barva[u] : = siva; print u; for vsako u-jevo naslednico v: if barva[v] = bela: DFS(v); barva[u] : = črna; glavni klic: DFS(s); • Namesto rekurzije imamo lahko tudi iteracijo, točke pa odlagamo na sklad (pri vsaki si tudi zapomnimo, do katere naslednice 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.
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: a – 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 b d c k g e • 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) a f i b a d l c h j m b c k g e n e d g f h j l h j m n i k m i l n f
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 Artikulacijske točke a b e d • 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: f g n i h – Za vsako u V: delajmo se, da smo u pobrisali iz grafa, in preverimo, če je še vedno povezan – O(|V| + |E|)) k l • 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: j a m b c e f g h • Poglejmo vsakega od njenih otrok, w, v DFS drevesu i • Č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|) d j k l m n
Iskanje najkrajših poti • Recimo, da ima vsaka povezava (u, v) E neko dolžino d(u, v) – Če (u, v) E, si mislimo d(u, v) = – Dolžina poti (u 0, u 1, …, uk) je Si=1. . k d(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
u 0 Iskanje najkrajših poti p uk– 1 r (? ) • Če je p = (u 0, u 1, …, uk– 1, uk) najkrajša pot od u 0 do uk, je (u 0, u 1, …, uk– 1) najkrajša pot od u 0 do uk– 1 – Res: če bi obstajala od u 0 do uk– 1 neka krajša pot r, bi jo lahko podaljšali s korakom (uk– 1, uk) in tako dobili neko pot od u 0 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. uk
Iskanje najkrajših poti s topološkim urejanjem • Recimo, da oštevilčimo točke v topološkem vrstnem redu: u 0, u 1, …, un • Postopek: for i : = 0 to n – 1: // zdaj za u 0, …, ui– 1 že poznamo najkrajše poti (v tabeli d) p[ui] : = nil; if ui = s then d[ui] : = 0 else d[ui] : = ; for vsako ui-jevo predhodnico uj: // gotovo je j < i (zaradi topološkega vrstnega reda) if d[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 each v 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; • • u (*) v (*) Za večjo učinkovitost je dobro hraniti vse sive točke v prioritetni vrsti (npr. v kopici – heap). – • s Č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
Posplošitev problema najkrajših poti • Za pot p = (u 0, u 1, …, 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(u 0, …, uk– 1, uk) = f (J(u 0, …, uk– 1), uk– 1, uk) za neko funkcijo f (t, u, v). ki jo poznamo – J(u 0, …, uk– 1, uk) ≥ J(u 0, …, 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 v in 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 (z. A, z. B, k. A, k. B). – 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 = {(x. A, y. A, x. B, y. B) : vse koordinate med 1 in 30} E = {vsaka točka ima 4 4 sosede, ki ustrezajo možnim premikom A-ja in B-ja} – Če je u = (x. A, y. A, x. B, y. B), definirajmo j(u) : = ((x. A – x. B)2 + (y. A – y. B)2) – Definirajmo J(u 0, …, uk– 1, uk) = –min{j(u 1), j(u 2), …, 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čjo J(u 0, …, uk– 1, uk) = –min{j(u 1), j(u 2), …, j(uk)} = –min{j(u 1), j(u 2), …, j(uk– 1)}, j(uk)} = max{–min{j(u 1), j(u 2), …, j(uk– 1)}, –j(uk)} = max{J(u 0, …, 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 each v V: d[v] : = ; p[v] : = nil; d[s] : = 0; for k : = 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 each v V: d'[v] : = d[v]; p'[v] : = p[v]; for each (u, v) E: if d[u] + d(u, v) < d'[v]: d'[v] : = d[u] + d(u, v); p'[v] : = u; for each v V: d[v] : = d'[v]; p[v] : = p'[v]; • Časovna zahtevnost: O(|V||E|)
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? ! s – Da, ker imamo negativni cikel. 8 Večkrat ko gremo po njem, krajša bo pot. Najkrajša pot torej sploh ne obstaja. 5 – 3 – Bellman-Ford nam torej omogoča tudi opaziti, 1 – 4 če je iz s dosegljiv kakšen negativni cikel. 20 v
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 d 1[u, v] vemo, da je d 1[u, v] = 0 pri u = v, 1[u, v] = d(u, v) sicer. d 1[u, v] = u. p – Za večje k ga lahko računamo takole: algoritem Tralala(r, t): for each u V, for each v V: dr+t[u, v] : = ; pr+t[u, v] : = nil; for each w V: if dr[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 d 1, iz tega lahko izračunamo d 2, d 4, d 8, d 16, d 32, … 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 = {u 0, u 1, …, un– 1} • Naj bo dk[i, j] dolžina najkrajše poti od ui do uj, ki med njima ne obiskuje točk uk, uk+1, … – Trivialni podproblemi: d 0[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 to n – 1, for j : = 0 to n – 1: d 0[i, j] : = d(ui, uj); for k : = 0 to n – 1: for i : = 0 to n – 1, for j : = 0 to n – 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 zahtevnosti algoritmov za najkrajše poti • Algoritem iskanje v širino topološko urejanje Dijkstra brez kopice Dijkstra s kopico Bellman-Ford za APSP Floyd-Warshall Najkrajše poti od ene do ostalih med vsemi pari točk O(V+E) O(V 2+E) O(E log V) O(VE) O(V 2 +VE) O(V 3 +VE) O(VE log V) O(V 2 E) O(V 3 log V) O(V 3) [Spomnimo se: E = O(V) za redke grafe, E = O(V 2) za goste. ]
2fe29957124cb9c647f99dd21f51e627.ppt