32.Prednaska
32. Prednáška |
Generovanie grafu
Aby sme mohli testovať rôzne spôsoby prehľadávania grafov, vytvoríme triedu TGraf, ktorá okrem štandardných metód (pridávanie vrcholu a hrany, zisťovanie, či je medzi dvoma vrcholmi hrana) bude obsahovať aj metódy na nakreslenie grafu, resp. zafarbenie vrcholu a hrany. Napr. pre graf reprezentovaný poľom množín (môžete použiť tento pripravený projekt):
type TVrchol = class private X, Y: Integer; Sus: set of Byte; // množina susedov public constructor Create(XX, YY: Integer); procedure Kresli(C: TCanvas); function Vzd(XX, YY: Integer): Integer; end; TGraf = class Image: TImage; G: array of TVrchol; constructor Create(II: TImage); procedure PridajVrchol(XX, YY: Integer); procedure PridajHranu(V1, V2: Integer); function JeHrana(V1, V2: Integer): Boolean; procedure ZafarbiVrchol(V1: Integer; Farba: TColor); procedure ZafarbiHranu(V1, V2: Integer; Farba: TColor); procedure Kresli; end;
Niektoré metódy potom môžu vyzerať takto:
function TGraf.JeHrana(V1, V2: Integer): Boolean; begin Result := V2 in G[V1].Sus; end; procedure TGraf.ZafarbiVrchol(V1: Integer; Farba: TColor); begin with Image.Canvas do begin Pen.Color := Farba; Brush.Color := Farba; Brush.Style := bsSolid; G[V1].Kresli(Image.Canvas); Brush.Style := bsClear; end; end; procedure TGraf.ZafarbiHranu(V1, V2: Integer; Farba: TColor); begin with Image.Canvas do begin Pen.Color := Farba; Line(G[V1].X, G[V1].Y, G[V2].X, G[V2].Y); end; end; procedure TGraf.Kresli; var I, J: Integer; begin with Image.Canvas do begin Brush.Color := clWhite; Brush.Style := bsSolid; FillRect(Image.ClientRect); Pen.Width := 3; end; for I := 0 to High(G) do begin ZafarbiVrchol(I, clLtGray); for J := 0 to High(G) do if JeHrana(I, J) then ZafarbiHranu(I, J, clLtGray); end; end;
Teraz môžeme zostaviť aplikáciu, ktorá vygeneruje graf s 10 náhodne položenými vrcholmi. Program tiež náhodne pospája niektoré vrcholy grafu hranami. Inštanciu Graf triedy TGraf zadeklarujeme ako globálnu premennú:
var Graf: TGraf; procedure TForm1.Button1Click(Sender: TObject); var I, J: Integer; begin Graf.Free; Graf := TGraf.Create(Image1); for I := 1 to 10 do begin Graf.PridajVrchol(Random(Image1.Width), Random(Image1.Height)); for J := 0 to High(Graf.G) - 1 do if Random(4) = 0 then Graf.PridajHranu(High(Graf.G), J); end; Graf.Kresli; end;
Takto vygenerovaný graf je ale veľmi neprehľadný. Graf budeme preto generovať tak, že vrcholy rovnomerne rozložíme vo štvorcovej sieti. Do formulára vložíme aj komponent vstupný riadok Edit1 (veľkosť siete), do ktorého môžeme vkladať rôzne číselné hodnoty a tým generovať rôzne veľké grafy:
procedure TForm1.Button2Click(Sender: TObject); var I, J, N, D: Integer; begin Graf.Free; Graf := TGraf.Create(Image1); N := StrToIntDef(Edit1.Text, 4); D := Image1.Width div (N + 2); for I := 0 to N - 1 do for J := 0 to N - 1 do begin Graf.PridajVrchol(D * J + D, D * I + D); if (Random(4) > 0) and (J > 0) then Graf.PridajHranu(High(Graf.G), High(Graf.G) - 1); if (Random(4) > 0) and (I > 0) then Graf.PridajHranu(High(Graf.G), High(Graf.G) - N); end; Graf.Kresli; end;
Nakoľko táto reprezentácia grafu (pole množín) umožňuje pracovať s grafom, ktorý má maximálne 256 vrcholov. Ak zadáme väčšiu veľkosť siete ako 16x16, vygeneruje sa len 256 vrcholov.
Algoritmus do hĺbky
Prehľadávanie grafu znamená taký algoritmus, ktorý postupne prechádza vrcholy grafu. S každým vrcholom pritom vykoná nejakú akciu (napr. ho zafarbí) a ďalej pokračuje na niektorom z jeho susedných vrcholov. Prehľadávanie sa začne z nejakého (ľubovoľného) vrcholu. Každý vrchol navštívime maximálne raz a možno práve raz (závisí to od zadania úlohy).
Ukážeme dva základné algoritmy:
- prehľadávanie do hĺbky - funguje podobne ako preorder na stromoch: najprv spracuje vrchol a potom postupne pokračuje v prehľadávaní všetkých svojich susedov - opäť rekurzívnym algoritmom
- prehľadávanie do šírky - prehľadáva vrcholy podobne ako prehľadávanie vrcholov po úrovniach v stromoch - najprv všetky, ktoré majú vzdialenosť len 1, potom všetky, ktoré majú vzdialenosť presne 2, atď.
Pri prehľadávaní grafu pomocou nejakého algoritmu si potrebujeme evidovať, ktoré vrcholy sa už spracovali. Keďže samotný algoritmus môže byť rekurzívny, túto evidenciu nemôžeme robiť v lokálnej premennej samotnej procedúry - môže to byť buď globálna premenná, alebo stavová premenná triedy TGraf:
Visited: array of Boolean;
alebo pre grafy do 256 vrcholov aj ako
Visited: set of Byte;
Ak je každý vrchol objektom (napr. TVrchol), tak sem môžeme dodefinivať novú stavovú premennú (namiesto poľa alebo množiny Visited):
Visited: Boolean;
Najprv zapíšeme algoritmus do hĺbky všeobecne: prehľadávanie, ak začíname vo vrchole V:
procedure Dohlbky(V); begin Visited := Visited + [V]; // spracovanie vrcholu V for I := všetkých susedov V do if not (I in Visited) then Dohlbky(I); end;
A teraz konkrétne algoritmus prehľadávania do hĺbky - zafarbuje navštívené vrcholy aj hrany, po ktorých algoritmus prechádza. Najprv vytvoríme obyčajnú procedúru, ktorá sa spúšťa pri zatlačení tlačidla:
procedure TForm1.Button3Click(Sender: TObject); var Visited: set of Byte; procedure Dohlbky(V: Integer); var I: Integer; begin Visited := Visited + [V]; // spracuj vrchol Graf.ZafarbiVrchol(V, clRed); for I := 0 to High(Graf.G) do if not (I in Visited) and Graf.JeHrana(V, I) then begin Graf.ZafarbiHranu(V, I, clRed); Graf.Wait(100); Dohlbky(I); end; end; begin Visited := []; Graf.Kresli; Dohlbky(0); end;
Môžete vidieť, že premenná Visited je globálna voči samotnej procedúre Dohlbky.
Teraz zapíšeme to isté, ale ako metódu triedy TGraf. Premenná Visited musí byť teraz stavovou premennou (atribútom) triedy TGraf:
type
TGraf = class
Visited: set of Byte;
...
procedure Dohlbky(V: Integer);
end;
procedure TGraf.Dohlbky(V: Integer);
var
I: Integer;
begin
Visited := Visited + [V];
ZafarbiVrchol(V, clRed); // spracuj vrchol
for I := 0 to High(G) do
if not (I in Visited) and JeHrana(V, I) then
begin
ZafarbiHranu(V, I, clRed); // spracuj hranu
// Wait(50);
Dohlbky(I);
end;
end;
Prepíšeme aj procedúru TForm1.Button3Click:
procedure TForm1.Button3Click(Sender: TObject); begin Graf.Visited := []; Graf.Kresli; Graf.Dohlbky(0); if Graf.Visited = [0..High(Graf.G)] then Image1.Canvas.TextOut(5, 5, 'graf je súvislý') else Image1.Canvas.TextOut(5, 5, 'graf nie je súvislý'); end;
Treba si uvedomiť, že po skončení algoritmu Dohlbky bude premenná Visited obsahovať množinu všetkých navštívených vrcholov. Na základe toho, môžeme usúdiť, či je graf súvislý - ak táto množina obsahuje všetky vrcholy grafu, tento graf je súvislý, inak (niektoré vrcholy sa nepodarilo dosiahnuť) je nesúvislý.
Komponenty grafu
Farbenie komponentov grafu
Ďalší variant prehľadávania do hĺbky zafarbí danou farbou (parameter Farba) všetky vrcholy aj všetky hrany nejakého komponentu:
procedure TGraf.Dohlbky(V: Integer; Farba: TColor); var I: Integer; begin Visited := Visited + [V]; ZafarbiVrchol(V, Farba); Wait(10); for I := 0 to High(G) do if JeHrana(V, I) then begin ZafarbiHranu(V, I, Farba); if not (I in Visited) then Dohlbky(I, Farba); end; end;
Túto verziu prechádzania grafu algoritmom do hĺbky môžeme otestovať takto:
procedure TForm1.Button4Click(Sender: TObject); const Farba: array[0..10] of TColor = (clRed, clBlue, clGreen, clBlack, clYellow, clNavy, clTeal, clLime, clAqua, clFuchsia, clOlive); var N, I: Integer; begin N := 0; Graf.Visited := []; Graf.Kresli; for I := 0 to High(Graf.G) do if not (I in Graf.Visited) then begin Graf.Dohlbky(I, Farba[N]); Inc(N); end; Image1.Canvas.TextOut(5, 5, 'počet komponentov = ' + IntToStr(N)); end;
Počet komponentov grafu
Nasledujúca metóda pomocou algoritmu do hĺbky zistí počet komponentov grafu:
function TGraf.PocetKomponentov: Integer;
var
I: Integer;
begin
Result := 0;
Visited := [];
for I := 0 to High(G) do
if not (I in Visited) then
begin
Dohlbky(I);
Inc(Result);
end;
end;
Samotný algoritmus do hĺbky pre zisťovanie počtu komponentov teraz už nemusí nič zafarbovať - stačí len evidovať navštívené vrcholy v premennej Visited, napr.
procedure TGraf.Dohlbky(V: Integer); var I: Integer; begin Visited := Visited + [V]; for I := 0 to High(G) do if not (I in Visited) and JeHrana(V, I) then Dohlbky(I); end;
Algoritmus do hĺbky môžeme teda použiť na:
- zistenie počtu komponentov grafu, resp. zisťovanie, či je graf súvislý
- zistenie, či sú dva vrcholy v tom istom komponente
- zisťovanie počtov vrcholov v jednotlivých komponentoch grafu
- zafarbovanie vrcholov a hrán jednotlivých komponentov grafu
Nerekurzívny algoritmus do hĺbky
Prepíšme teraz rekurzívny algoritmus Dohlbky na nerekurzívnu verziu. Využijeme pritom triedu zásobník z unitu StackUnit. Nerekurzívny algoritmus môže vyzerať takto:
procedure TGraf.DohlbkyNR(V: Integer); var Stack: TStack; I: Integer; begin Stack := TStack.Create; Stack.Push(V); repeat Stack.Pop(V); Visited := Visited + [V]; ZafarbiVrchol(V, clRed); // spracuj vrchol for I := 0 to High(G) do if not (I in Visited) and JeHrana(V, I) then Stack.Push(I); until Stack.Empty; Stack.Free; end;
Zrejme zásobník musí mať prvky typu Integer.
Hoci tento algoritmus vyzerá v poriadku, ak ho otestujeme na nejakom jednoduchom grafe, zistíme, že niektorý vrchol sa bude spracovávať (zafarbovať) dvakrát a to môže byť v niektorých situáciách neprijateľné. Ak by sme si algoritmus odtrasovali ručne, zistili by sme, že niektoré čísla vrcholov sa dostávajú do zásobníka viackrát (napr. vrchol, ktorý ešte nebol navštívený, ale je susedom viacerých už navštívených vrcholov). Jednoducho to môžeme preveriť aj tak, že do tohto algoritmu vložíme počítadlo zafarbených vrcholov a po skončení algoritmu porovnáme tento počet so skutočným počtom vrcholov komponentu. Napr.
type TGraf = class Pocet: Integer; ... procedure DohlbkyNR(V: Integer); end; procedure TGraf.DohlbkyNR(V: Integer); var Stack: TStack; I: Integer; begin Stack := TStack.Create; Stack.Push(V); repeat Stack.Pop(V); Visited := Visited + [V]; ZafarbiVrchol(V, clRed); // spracuj vrchol Inc(Pocet); for I := 0 to High(G) do if not (I in Visited) and JeHrana(V, I) then Stack.Push(I); until Stack.Empty; Stack.Free; end; procedure TForm1.Button3Click(Sender: TObject); var I, N: Integer; begin Graf.Visited := []; Graf.Kresli; Graf.Pocet := 0; Graf.DohlbkyNR(0); N := 0; for I in Visited do Inc(N); Image1.Canvas.TextOut(5, 5, 'Visited = ' + IntToStr(N)); Image1.Canvas.TextOut(5, 25, 'Pocet = ' + IntToStr(Graf.Pocet)); end;
Správne by mal nerekurzívny algoritmus vyzerať takto:
procedure TGraf.DohlbkyNR(V: Integer);
var
Stack: TStack;
I: Integer;
begin
Stack := TStack.Create;
Stack.Push(V);
repeat
Stack.Pop(V);
if not (V in Visited) then
begin
Visited := Visited + [V];
ZafarbiVrchol(V, clRed);
for I := 0 to High(G) do
if not (I in Visited) and JeHrana(V, I) then
Stack.Push(I);
end;
until Stack.Empty;
Stack.Free;
end;
Takýto nerekurzívny algoritmus použijeme napr. vtedy, keď z nejakého dôvodu nechceme v programe spúšťať rekurziu. Tento nerekurzívny algoritmus inak nemá žiadne špeciálne výhody - skôr naopak - je to komplikovanejší kód, pri ktorom ľahšie urobíme chybu.
Algoritmus do šírky
Ak by sme potrebovali prehľadávať napr. binárny strom po úrovniach (algoritmus do šírky), potrebovali by sme dátovú štruktúru rad (front alebo queue). Podobne budeme postupovať aj pri algoritme pre graf. Tento algoritmus sa bude od nerekurzívneho algoritmu do hĺbky líšiť len tým, že namiesto TStack použijeme TQueue (a teda aj premenujeme jeho metódy):
procedure TGraf.Dosirky(V: Integer); var Queue: TQueue; I: Integer; begin Queue := TQueue.Create; Queue.Append(V); repeat Queue.Serve(V); if not (V in Visited) then begin Visited := Visited + [V]; ZafarbiVrchol(V, clRed); for I := 0 to High(G) do if not (I in Visited) and JeHrana(V, I) then Queue.Append(I); end; until Queue.Empty; Queue.Free; end;
Takto zapísaný algoritmus bude robiť skoro presne to isté ako nerekurzívny algoritmus do hĺbky. Rozdiel bud v tom, že sa v inom poradí navštívia všetky vrcholy grafu: najprv sa spracujú všetky najbližšie susediace vrcholy ku štartovému; potom sa spracujú všetci ich ešte nenavštívení susedia atď. Vďaka tejto vlastnosti algoritmu ho môžeme veľmi jednoducho vylepšiť: pri každom spracovávanom vrchole si budeme pamätať aj jeho momentálnu vzdialenosť - teda úroven "vnorenia". Štartový vrchol prehľadávania bude mať úroveň 0, všetci jeho bezprostrední susedia budú mať úroveň 1 a každý ďalší vrchol bude mať úroveň o 1 väčšiu ako vrchol, od ktorého sa sem prišlo.
Program prepíšeme tak, že vo fronte si budeme pri každom vrchole pamätať aj jeho úroveň v premennej Uroven. Použijeme dátovú štruktúru TQueue definovanú v unite QueueUnit (používa rad, do ktorého vkladáme aj vyberáme vždy dve celočíselné hodnoty).
procedure TGraf.Dosirky(V: Integer); var I, Uroven: Integer; Queue: TQueue; begin Queue := TQueue.Create; Queue.Append(V, 0); repeat Queue.Serve(V, Uroven); Wait(1); if not (V in Visited) then begin Visited := Visited + [V]; ZafarbiVrchol(V, clRed); Image.Canvas.TextOut(G[V].X, G[V].Y, IntToStr(Uroven)); for I := 0 to High(G) do if not (I in Visited) and JeHrana(V, I) then Queue.Append(I, Uroven + 1); end; until Queue.Empty; Queue.Free; end;
Všimnite si, že program teraz pri každom navštívenom vrchole vypíše aj jeho vzdialenosť (úroveň) od štartového vrcholu.
Algoritmus do šírky môžeme otestovať tak, že pri kliknutí do grafickej plochy na niektorý vrchol, sa z tohto miesta naštartuje prehľadávanie do šírky:
procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var I: Integer; begin I := High(Graf.G); while (I >= 0) and (Graf.G[I].Vzd(X, Y) > 30) do Dec(I); if I >= 0 then begin Graf.Visited := []; Graf.Dosirky(I); end else Graf.Kresli; end;
V tomto programe sme využili metódu Vzd, ktorá počíta vzdialenosť nakresleného vrcholu od ľubovoľného bodu (X, Y) v grafickej ploche.
Vzdialenosť a najkratšia cesta
Vzdialenosť dvoch vrcholov
Algoritmus do šírky môžeme využiť aj na zisťovanie vzdialenosti ľubovoľných dvoch vrcholov v grafe. Pod vzdialenosťou tu rozumieme dĺžku najkratšej cesty z jedného vrcholu do druhého. Ak takáto cesta neexistuje (vrcholy sú v rôznych komponentoch grafu), metóda vráti hodnotu -1:
function TGraf.Vzdialenost(V1, V2: Integer): Integer; var I, Uroven: Integer; Queue: TQueue; begin Visited := []; Queue := TQueue.Create; Queue.Append(V1, 0); Result := -1; repeat Queue.Serve(V1, Uroven); if not (V1 in Visited) then begin Visited := Visited + [V1]; if V1 = V2 then begin Result := Uroven; Break; end; for I := 0 to High(G) do if not (I in Visited) and JeHrana(V1, I) then Queue.Append(I, Uroven + 1); end; until Queue.Empty; Queue.Free; end;
Najkratšia cesta v grafe
Rozšírime tento algoritmus nielen na zistenie vzdialenosti dvoch vrcholov, ale aj na zapamätanie tejto najkratšej cesty. Použijeme takúto ideu: vytvoríme pomocné pole, v ktorom si budeme pre každý vrchol pamätať jeho predchodcu v ceste, t.j. vždy, keď si v rade zapamätáme vrchol (Append), ktorý plánujeme v budúcnosti preskúmať, zapamätáme si s ním aj jeho predchodcu (odkiaľ sme k nemu prišli). Keď vrchol z frontu vyberieme (Serve), tak vieme aj jeho predchodcu na ceste a môžeme si ho zaevidovať v pomocnom poli. Na záver, keď sme dorazili do cieľa, pomocou predchodcov uložených v pomocnom poli sa vieme postupne vracať späť a teda vieme skonštruovať výsledné dynamické pole (hodnota -1 v poli znamená, že vrchol nemá predchodcu - je prvý na ceste).
Algoritmus hľadania najkratšej cesty v grafe:
type TPostupnost = array of Integer; ... function TGraf.NajkratsiaCesta(V1, V2: Integer): TPostupnost; var I, V0: Integer; Queue: TQueue; P: TPostupnost; begin Result := nil; SetLength(P, Length(G)); Visited := []; for I := 0 to High(P) do P[I] := -1; Queue := TQueue.Create; Queue.Append(V1, -1); repeat Queue.Serve(V1, V0); if not (V1 in Visited) then begin P[V1] := V0; if V1 = V2 then begin I := 0; repeat // zistíme dĺžku cesty Inc(I); V2 := P[V2]; until V2 < 0; SetLength(Result, I); // pripravíme výsledné pole repeat // prekopírujeme Dec(I); Result[I] := V1; V1 := P[V1]; until V1 < 0; Break; end; Visited := Visited + [V1]; for I := 0 to High(G) do if not (I in Visited) and JeHrana(V1, I) then Queue.Append(I, V1); end; until Queue.Empty; Queue.Free; end;
Otestovať tento algoritmus môžeme opäť klikaním do grafickej plochy: prvé kliknutie (ľavým tlačidlom myši) na nejaký vrchol tento vrchol označí a potom klikania na ďalšie vrcholy (pravým tlačidlom, alebo ľavým so zatlačeným klávesom Ctrl) vykreslia najkratšiu cestu:
var Start: Integer = -1; procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var I: Integer; P: TPostupnost; begin I := High(Graf.G); while (I >= 0) and (Graf.G[I].Vzd(X, Y) > 30) do Dec(I); if I < 0 then Exit; Graf.Kresli; if Shift = [ssLeft] then begin Start := I; Graf.ZafarbiVrchol(Start, clRed); end else if Start >= 0 then begin P := Graf.NajkratsiaCesta(Start, I); for I := 1 to High(P) do Graf.ZafarbiHranu(P[I - 1], P[I], clBlue); Graf.ZafarbiVrchol(Start, clRed); end; end;
Zhrňme použitie algoritmu do šírky:
- podobne ako do hĺbky, vieme zistiť vrcholy, ktoré patria do jedného komponentu a teda aj zistiť počet komponentov
- vieme zistiť (zafarbiť) vrcholy (alebo hrany), ktoré sú rovnako vzdialené od daného vrcholu
- vieme zistiť vzdialenosť, resp. najkratšiu cestu medzi dvoma vrcholmi v grafe
- napr. pomocou High(NajkratsiaCesta(V1, V2)) vieme zistiť vzdialenosť dvoch vrcholov v grafe