Vaja 2 Iskanje z razvijanjem v širino 1. Splošna predstavitev problema Preden se lotimo samega algoritma moramo definirati nov pojem graf. Graf G je v teoriji grafov definiran kot dvojica G={V, P}. V je množica vozlišč v grafu, P pa množica povezav med temi vozlišči. Te povezave so lahko usmerjene ali neusmerjene, utežene ali neutežene, kar tudi določa tip grafa. Pri tej vaji bomo uporabljali najpreprostejše neutežene, neusmerjene grafe. Iskanje z razvijanjem v širino je eden izmed napreprostejših algoritmov za preiskovanje grafov in osnova mnogo drugim algoritmom, ki delujejo na grafih. Sam algoritem deluje po naslednjem principu. Imejmo graf G={V, P} in določeno začetno vozlišče s. Algoritem iskanja z razvijanjem v širino sistematično preišče povezave v G in določi vsako vozlišče, do katerega lahko pridemo iz vozlišča s. Določi nam minimalno število povezav, ki so med s in trenutnim vozliščem. Končno nam tudi ustvari drevo s korenom s, ki vsebuje vsa vozlišča, do katerih lahko pridemo iz s. Navadno imamo definirano tudi ciljno vozlišče g, in če ga imamo, se bo algoritem, ko bo to vozlišče našel ustavil. Če ga nimamo, se algoritem ustavi šele, ko nimamo nobenega vozlišča več za razvijanje. To se zgodi, ko smo preiskali vsa vozlišča grafa ali v primeru, da do določenega vozlišča iz vozlišča s ne moremo priti. Algoritem deluje tako na usmerjenih, kot tudi neusmerjenih grafih. Ime iskanje z razvijanjem v širino je algoritem dobil zato, ker preiskuje vozlišča po širini iskalnega drevesa. To pomeni, da bo algoritem najprej razvil vsa vozlišča na globini k, preden bo pričel z razvijanjem vozlišč na globini k+1, kar je razvidno tudi na sliki 1. Slika 1: Tvorba iskalnega drevesa pri strategiji iskanja z razvijanjem v širino. Že razvita vozlišča so obarvana črno, tista, ki še čakajo na razvoj pa sivo. Algoritem bo najprej še razvil vozlišči 4 in 5 na globini 1, preden bo pričel razvijati vozlišča na globini 2 Da bomo lažje sledili primeru delovanja tega algoritma, povzamimo splošno različico iskanja z razvijanjem v širino v nekaj korakih: 1. Postavimo začetno vozlišče s v vrsto Q in ga obarvajmo s sivo barvo. 2. Vozlišču s inicializiramo razdaljo na 0, vsem ostalim pa na 3. Vzamimo vozlišče i, ki se nahaja na začetku vrste Q iz vrste in: a. če je vozlišče i enako ciljnemu vozlišču g potem smo našli rešitev in izstopimo iz zanke Stran 1 od 8
b. če vozlišče i ni ciljno vozlišče, v vrsto Q dodajmo vsa vozlišča, v katera lahko pridemo iz vozlišča i in še niso bila dodana v vrsto ali razvita. Vsakemu takemu vozlišču j določimo razdaljo po naslednji enačbi: razdalja_do_vozlišča_j = razdalja_do_vozlišča_i + 1. 4. Ponavljamo korak 3 dokler vrsta Q ni prazna. V našem primeru, ker ne bomo imeli definiranega končnega vozlišča g, bomo korak 3a izpustili. Tukaj se srečamo s pojmom vrsta. V računalništvu je vrsta zelo pomembna podatkovna struktura tipa FIFO (first-in-first-out), ki jo srečamo v mnogih aplikacijah. FIFO pomeni, da dodajamo elemente v vrsto na koncu vrste, jemljemo pa elemente iz vrste na njenem začetku. Analogija v realnem svetu je recimo vrsta pred blagajno v trgovini. Vstopimo na koncu, blagajničarka pa streže na začetku vrste. Sedaj pa si poglejmo kratek primer delovanja iskanja z razvijanjem v širino na manjšem grafu. Slika 2: Primer grafa. Vozlišča so tukaj označena z črkami od A do H, da ne bi prišlo do zamenjav z razdaljami Na grafu, ki ga vidimo na sliki 2 poiščimo razdalje iz startnega vozlišča s = A do vseh ostalih vozlišč. Da algoritem lahko sledi, katera vozlišča je že odkril in čakajo na razvijanje, ali jih je celo že razvil uporablja barvanje vozlišč. Vozlišča, ki so še neodkrita imajo belo barvo. Vozlišča, ki jih je algoritem že odkril in v vrsti čakajo na razvijanje so sive barve. Ko pa vozlišče razvijemo, ga obarvamo s črno barvo. Vrnimo se k našemu primeru. Na začetku so vsa vozlišča razen začetnega vozlišča A bele barve in imajo razdaljo. Vozlišče A je sivo, ima razdaljo 0 in v vrsti Q čaka na razvijanje. Slika 3: Začetno stanje pred vstopom v zanko Sedaj iz vrste Q vzamemo edino vozlišče, to je A in ga razvijemo: sosednji vozlišči sta B in H. Algoritem še nobenega od njiju ni odkril, zato obe obarvamo sivo ter ju dodamo v vrsto Q. Razdaljo pri obeh določimo kot: 0 + 1 = 1 Stran 2 od 8
vozlišče A sedaj obarvamo črno (slika 4). Slika 4: Stanje po razvoju vozlišča A Nadaljujmo z razvijanjem vozlišč. Naslednje vozlišče v vrsti Q je B. Vzamemo ga iz vrste in ga razvijemo: vozlišče B ima enega soseda, to je vozlišče A. Ker pa smo to vozlišče že razvili (vozlišče je obarvano s črno barvo) ga ne dodajamo v vrsto Q vozlišče B obarvamo s črno barvo (slika 5). Slika 5: Stanje po razvoju vozlišča B Sedaj iz vrste Q vzamemo vozlišče H in ga razvijemo: vozlišče H ima 2 soseda: o vozlišče A - to vozlišče smo že razvili in ga pustimo o vozlišče C - tega vozlišča pa algoritem še ni odkril in ga zato dodamo v vrsto Q, ga obarvamo sivo ter mu določimo razdaljo kot: 1 + 1 = 2 vozlišče H obarvamo s črno barvo (slika 6). Slika 6: Stanje po razvoju vozlišča H V vrsti Q se sedaj nahaja zgolj vozlišče C, zato ga vzamemo iz vrste in ga razvijemo: vozlišče C ima 3 sosede: o vozlišče H, ki smo ga že razvili in ga zato pustimo pri miru o vozlišči D in G, ki pa sta doslej še neodkriti. Zato ju vstavimo v vrsto Q, ju obarvamo sivo ter določimo razdaljo kot: 2 + 1 = 3 vozlišče C sedaj obarvamo s črno barvo (slika 7). Stran 3 od 8
Slika 7: Stanje po razvoju vozlišča C Naslednje vozlišče, ki v vrsti Q čaka na razvoj je vozlišče D. Zato ga vzamemo iz vrste in ga razvijemo: vozlišče D ima 3 sosede: o vozlišče C, ki je že razvito in ga zato pustimo pri miru o vozlišče E, ki je še neodkrito in ga zato dodamo v vrsto Q, ga pobarvamo s sivo barvo ter mu določimo razdaljo: 3 + 1 = 4 o vozlišče G, ki pa je že odkrito in čaka v vrsti Q na razvoj. Zato ga pustimo pri miru. vozlišče D sedaj pobarvamo s črno barvo (slika 8). Slika 8: Stanje po razvoju vozlišča D Sedaj v vrsti Q na razvoj čaka vozlišče G. Torej ga vzamemo iz vrste in ga razvijemo: vozlišče G ima 4 sosede: o vozlišče C, ki je že razvito in ga zato pustimo pri miru o vozlišče D, ki je prav tako že razvito in ga pustimo pri miru o vozlišče E, ki v vrsti Q čaka na razvoj in ga zato pustimo pri miru o vozlišče F, ki je pa še neodkrito, zato ga dodamo v vrsto Q, ga pobarvamo s sivo barvo ter mu določimo razdaljo: 3 + 1 = 4 vozlišče G sedaj pobarvamo s črno barvo (slika 9). Slika 9: Stanje po razvoju vozlišča G Algoritem sedaj najprej iz vrste vzame vozlišče E, potem pa še vozlišče F. Ker so vsi njuni sosedi že obdelani oz. že čakajo v vrsti za obdelavo pri vozliščih ne pride do nobene spremembe. Obe situaciji sta vidni na slikah 10 in 11. Stran 4 od 8
Slika 10: Stanje po razvoju vozlišča E Slika 11: Stanje po razvoju vozlišča F Kot smo že v začetku povedali, nam algoritem tvori tudi iskalno drevo. Iskalno drevo za naš primer je vidno na sliki 12. Slika 12: Iskalno drevo Stran 5 od 8
2. Pomoč pri implementaciji Preden se lotimo implementacije te vaje moramo še nekaj besed nameniti predstavitvi grafa, ki jo bomo uporabljali pri tej vaji. V glavnem poznamo 2 predstavitvi grafa v računalniku. Prva je predstavitev s seznami povezav, druga pa z matriko sosednosti. Pri tej vaji bomo uporabljali matriko sosednosti, zato se ji natančneje posvetimo. Predstavitev z matriko sosednosti je primernejši način predstavitve grafa G={V, P} v primeru, ko je P zelo blizu V 2 (to pomeni, da je veliko število povezav glede na število vozlišč) ali v primerih, ko moramo zelo hitro povedati ali je določeno vozlišče povezano z drugim vozliščem. Matrika sosednosti za neutežen graf G={V, P} je matrika C={c ij } velikosti kjer so elementi c ij definirani po naslednji enačbi: V V, c ij 1, če velja ( i, j) P 0, v vseh drugih primerih To preprosto povedano pomeni, da če imamo v grafu povezavo med vozliščema i in j, je na tistem mestu v matriki C vrednost 1, v nasprotnem primeru pa 0. Sedaj, ko poznamo predstavitev grafa, si poglejmo psevdokod algoritma: procedure ISKANJE_V_ŠIRINO(G, s) for each vozlišče v G.V - {s} do G.barva[v] := bela G.d[v] := G.predhodnik[v] := NIL G.barva[s] := siva G.d[s] := 0 G.predhodnik[s] := NIL VSTAVI_V_VRSTO(Q, s) while not(prazna(q)) do v := VZAMI_IZ_VRSTE(Q) for each vozlišče u G.Sosedi(v) do if G.barva[u] = bela then G.barva[u] := siva G.d[u] := G.d[v] + 1 G.predhodnik[u] := v VSTAVI_V_VRSTO(Q, u) G.barva[v] := črna Izpis 1: Psevdokod funkcije ISKANJE_V_ŠIRINO Stran 6 od 8
procedure IZPIS_POTI(G, s, v) if v = s then izpiši s + ": globina: " + G.d[s] else if G.predhodnik[v] = NIL then izpiši "med " + s + " in " + v + " ni poti" else IZPIS_POTI(G, s, G.predhodnik[v]) izpiši v + ": globina: " + G.d[v] Izpis 2: Psevdokod procedure IZPIS_POTI Namigi za implementacijo: 1. Algoritem boste najlažje implementirali, če boste uporabljali statična polja. Tako kreirajte 3 polja: barva, predhodnik in d. Vsa naj bodo enake velikosti, kot je število vozlišč v grafu. 2. Namesto barve uporabite neko številsko oznako (npr. 0, 1, 2). 3. Množica Sosedi(v) vsebuje vsa vozlišča, ki so sosedi vozlišča v. To lahko preprosto preberete iz matrike sosednosti. Ni prav nobene potrebe, da bi delali ločen seznam. 4. Vrsto Q implementirajte kot krožno vrsto v polju velikosti 21. 3. Zahteve naloge Za uspešno opravljeno 2. vajo mora študent v programskem jeziku C++ implementirati algoritem iskanja z razvijanjem v širino. Pri zagonu programa se mora na ekran izpisati meni, prikazan na sliki 13: Iskanje z razvijanjem v širino izbira: 1 Vnos podatkov - ročni 2 Zagon algoritma Iskanje z razvijanjem v širino 3 Izpis poti med vozliščem s in v 4 Konec Vaša izbira: Slika 13: Začetni meni, ob zagonu aplikacije Uporabnik mora nato vpisati vrednost od 1 do 4, kar sproži ustrezno akcijo. Po zaključeni akciji (če nismo izbrali 4, kar pomeni zaključek programa) se mora ponovno na ekranu zapisati meni prikazan na sliki 13. Pri vnosu podatkov iz tipkovnice najprej preberemo velikost grafa, potem pa posamezni element matrike sosednosti. Pri zagonu algoritma najprej uporabnik vnese začetno vozlišče s, nato pa se požene algoritem iskanja z razvijanjem v širino. Če je izbran izpis, najprej uporabnik vnese vozlišče v, potem pa se izpiše pot med s in v. Stran 7 od 8
POMEMBNO: Program mora delovati točno po predpisanem vmesniku. Ob odstopanjih se bo rešitev zavrnila. Namigi: Velikost grafa ne bo presegla 20. Pri vnosu matrike sosednosti si podobno kot pri prejšnji vaji pomagajte z preusmeritvijo vhodnega toka. V tekstovno datoteko si napišite vse vrednosti (tudi izbire pri meniju), ki bi jih sicer morali vnesti ročno, in potem poženite program v konzoli z naslednjim ukazom: operacijski sistem Windows XP: imeprograma.exe < imetekstovnedatoteke operacijski sistem Linux:./imePrograma < imetekstovnedatoteke Primer tekstovne datoteke za rešen zgled: 1 8 0 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 0 0 1 0 1 0 1 0 0 0 0 1 0 1 1 0 0 0 0 0 1 0 1 0 0 0 1 1 1 1 0 0 1 0 1 0 0 0 0 0 2 0 3 6 4 Stran 8 od 8