G R A F Y G R A F Y

Transkrypt

G R A F Y G R A F Y
GRAFY
Grafy stosuje się do znajdywaniu optymalnych dróg, obmyślania strategii gry,
rozwiązywaniu łamigłówek oraz rozwiązywania wielu złożonych problemów
technicznych. Grafy powstały dzięki niemieckiemu matematykowi Eulerowi. Za ich
pomocą rozwiązał on problem miasta, w którym mieszkańcy zastanawiali się jak
pokonać wszystkie 7 mostów znajdujących się w mieście, bez przechodzenia
dwukrotnie przez ten sam most.
Euler wyjaśnił, że jest to nie możliwe stosując w tym, celu model grafu. Powstał tym
samym cykl zwany Cyklem Eulera, w którym ten sam wierzchołek grafu może być
odwiedzany kilkakrotnie. W grafie można znaleźć cykl Eulera wtedy i tylko wtedy, gdy
graf jest spójny i każdy jego wierzchołek ma parzysty stopień ( liczba krawędzi
dochodzących do wierzchołka określa ich stopień). Poniżej przedstawiłem przykład
grafu:
B
C
Grafem nazywamy parę liczb G(X,Y) gdzie
A
X to liczba węzłów grafu,
D
Y to liczba krawędzi grafu.
E
F
Grafy dzielimy na skierowane i niekierowane. Skierowany graf odpowiada grafowi
przedstawionemu powyżej, graf nie skierowany to natomiast taki, w którym
krawędzie nie są określone kierunkowo. Podstawowymi prawami rządzącymi w
algorytmie grafów są ich cykle, Cykl Eulera, oraz cykl Hamiltona, który jest
przeciwieństwem cyklu Eulera. Mówi on nam, że gdy przechodzimy od pierwszego
węzła grafu do ostatniego nie możemy, dwukrotnie odwiedzać tego samego węzła.
Metody reprezentowania grafów w pamięci komputera:
1. Tablica dwuwymiarowa.
2. Lista węzłów.
G R A F Y
Tablica dwuwymiarowa.
W tablicy dwuwymiarowej wiersze to węzły początkowe, a kolumny to węzły
końcowe. Dla poniższego grafu implementacja tablicowa została zaprezentowana w
postaci tabeli.
Poniższa tabela przedstawia zasadę tablicowej implementacji grafu, w pamięci
komputera:
A
B
C
D
E
F
A
0
0
0
0
0
0
B
1
0
0
0
0
0
C
0
1
1
0
0
0
D
0
1
0
0
0
0
E
0
0
1
1
0
0
B
F
0
0
0
1
1
0
C
A
D
E
F
„1” na pozycji (x,y) oznacza, że pomiędzy węzłami istnieje krawędź skierowana w
stronę Y. Pola, w których znajdują się wartości „0” symbolizują, że między tymi
węzłami nie istnieją krawędzie. Zaleta jest prostota w implementacji, a wadą to, że
grafy o ustalonej z góry liczbie węzłów mogą być łatwo reprezentowane
Lista węzłów
1. Następniki- węzły odchodzące.
2. Poprzedniki- węzły dochodzące.
Dla poniższego grafu implementacja za pomocą listy została zaprezentowana w
postaci dwóch tabel, których zebrano węzły nadchodzące i dochodzące.
Tabela węzłów następników - odchodzących:
A
B
C
D
E
F
B
C,D
C,E
E,F
F,
NULL
B
C
A
D
E
F
2
G R A F Y
Tabela węzłów poprzedników - dochodzących:
A
B
C
D
E
F
NULL
A
B,C
B
C,D
D,E
Zaleta to liczba węzłów, która może ulegać zmianie w trakcie wykonywania
programu. Tabela taka może być łatwo zrealizowany za pomocą listy list.
Eksploracja grafów.
Dużo zadań, w których użyto grafu do modelowania pewnej sytuacji wymaga
systematycznego przeszukiwania grafu. Przeszukiwanie grafów stosowane jest w
celu odnalezienia optymalnej strategii gry, rozwiązaniu łamigłówki lub konkretnemu
problemu technicznego przedstawionego za pomocą grafów.
Techniki eksploracji grafów:
1. Strategia „W głąb” DFS - (DEPTH FIRST)
2. Strategia „Wszerz” BFS - (BREADTH FIRST)
Strategia przeszukiwania „W głąb” – DFS.
Strategia „W głąb” bada daną drogę aż do jej całkowitego wyczerpania, przed
wybraniem następnej w celu uzyskania kolejności przeszukiwania grafu. Procedurę
zastosowaną przeze mnie do analizy algorytmu nazwano zwiedzaj przeszukuje ona
listę wierzchołków, przylegających do wierzchołka i, i jeśli któryś z tych przyległych
wierzchołków nie był jeszcze badany, wywołuje dla niego funkcje zwiedzaj, która
zaznacza ten wierzchołek jako zbadany a następnie szuka wierzchołków, przyległych
do niego, i sprawdza kolejno, które z nich były już badane. Dla pierwszego z
niebadanych wywoływana jest ponownie procedura zwiedzaj, i tym sposobem
algorytm „idzie w głąb” grafu, aż do całkowitego wyczerpania raz obranej drogi.
Poniżej zamieszczono graf na podstawie, którego analizowano strategie „w głąb”
DEPTH FIRST.
0
3
6
Lista przyległych wierzchołków:
0 - 1,3,4
1
4
2
5
1 - 0,2,4
2 - 1,6
3
G R A F Y
3 - 0,4,6
4 - 0,1,3,5
5-4
6 - 2,3
Poniżej przedstawiona treść wykonywania algorytmu w pseudokodzie:
zwiedzaj(i)
{
zaznacz i jako zbadany;
dla każdego k przylegającego do i
jeśli k nie był jeszcze zbadany;
zwiedzaj (k);
}
Uruchomienie programu poinformuje nas, że kolejność przeszukiwanych
wierzchołków jest następująca: 0, 1, 2, 6, 3, 4, 5. Algorytm według powyższego
pseudokodu działa następująco:
Zwiedzamy
Zbadaliśmy
Przyległe k=
0
0
1,3,4
1
1
0,2,4
2
2
1,6
6
6
2,3
3
3
0,4,6
4
4
0,1,3,5
5
5
4
Fragment kodu programu realizującego strategie przeszukiwania „w głąb” w
C++:
int i, j, g[n][n], v[n];
// g[n][n] – graf [n][n],
// v[n] – przechowuje informacje czy badany wierzchołek był już badany jak tak to 1
jak nie to 0.
void szukaj(int g[n][n], int v[n])
{
int i;
for(i=0; i<n; i++)
4
G R A F Y
v[i]=0; // wierzcholek nie byl jeszcze badany.
for(i=0; i<n; i++)
if(v[i]==0)
zwiedzaj(g,v,i)
}
void zwiedzaj( int g[n][n], v[n] int i)
{
v[i]=1;
// zaznaczamy wierzchołek jako zbadany
for( int k=0; k<n; k++)
if ( g[i][k]!=0)
//czy istnieje przejscie
if (v[k]==0)
zwiedzaj(g,v,k);
}
Strategia przeszukiwania Wszerz – BFS.
W myśl strategii przeszukiwania WSZERZ, najpierw badamy wszystkie poziomy
grafu o jednakowej głębokości.
0
3
6
Lista wierzchołków przyległych:
0 – 1,3,4
0,2,4
0,4,6
1
4
2
5
1,3,5
2 – 1,6
5–4
5
G R A F Y
6 – 2,3
Problemem, jaki pojawia się w tym algorytmie to konieczność zapamiętywania
przeszukiwania danego wierzchołka oraz uwzględniania, że mamy jeszcze
ewentualnie inne wierzchołki czekające na przebadanie na tym samym poziomie
grafu. Okazuje się ze najlepiej jest do tego wykorzystać zwykłą kolejkę FIFO, która
sprawiedliwie obsłuży wszystkie wierzchołki, zgodnie z kolejności ich wchodzenia do
kolejki.
Poniżej przedstawiona treść wykonywania algorytmu w pseudokodzie:
szukaj(i)
{
wstaw i do kolejki;
dopóki kolejka nie jest pusta wykonaj:
{
wyjmij wierzchołek s z kolejki
zaznacz s że był już zbadany;
dla każdego wierzchołka k przyległego do wierzchołka s
jeśli k nie był jeszcze przebadany
{
zaznacz k jako zbadany;
wpisz k do kolejki;
}
}
}
Dla grafu na podstawie, którego analizowano powyżej zapisany algorytm, pierwszym
wyszukanym wierzchołkiem był wierzchołek 0. Zgodnie z algorytmem zapisano go do
kolejki, następnie za każdym razem sprawdzano warunek czy kolejka nie jest pusta,
jeśli był on spełniony, wówczas wykonywano dalszą część algorytmu. Struktura
kolejki FIFO, która była pomocna w celu poprawnego wykonania algorytmu, i nie
pominięcia żadnego z węzłów na tym samym poziomie, polegała na kolejnym
cyklicznym wyjmowaniu z kolejki jej wierzchołka. Odbywało się to za każdym razem
dla innego punktu węzłowego. Następnie wyszukiwane były punkty, które przylegały
do danego wierzchołka, jeśli nie były one uprzednio badane, wstawiałem je do
6
G R A F Y
kolejki, zachowując tym samym sens działania algorytmu opartego na kolejce FIFO.
Cała procedura odbywała się cyklicznie aż do momentu przejścia przez wszystkie
węzły grafu. Poniższy schemat przedstawia kolejność wpisywania danych
wierzchołków do kolejki, oś pionowa opisuje zawartość kolejki, a oś pionowa etap
wykonywania kolejki.
-
4
3
1
0
2
4
3
4
3
2
4
6
2
4
5
6
2
6
2
5
6
5
-
Poniższa tabela przedstawia, zestawienie wykonywania operacji, dla przykładowego
podanego przeze mnie grafu są to operacje: „wyjęcia” z kolejki wierzchołka, zbadania
węzła oraz wypisania tych węzłów, które są dla danego wierzchołka przyległe.
Przedstawia też kolejność wstawiania do kolejki FIFO poszczególnych węzłów.
Zawartość
kolejki FIFO
_
Wyjęte z
kolejki
Zbadane
węzły
Przyległe
węzły
Zbadane
węzły
0
0
4
3
1
4
3
1
2
4
3
2
4
3
6
2
4
6
2
4
5
6
2
5
6
2
5
6
5
6
0
1
3
4
2
6
1,3,4
0,2,4
0,4,6
0,1,3,5
1,6
2,3
1,3,4
2
6
5
_
Fragment kodu programu realizującego strategie przeszukiwania „wszerz” w
C++:
void szukaj( int g[n][n], int v[n], int i)
// rozpoczynamy od wierzchołka i
// g[n][n] - graf n na n
// v[n] przechowuje informacje czy dany wierzchołek był już badany (1) czy tez nie (0)
{
FIFO<int> kolejka(n);
kolejka.wstaw(i);
//wprowadź dane do kolejki
int s;
while(!kolejka.pusta())
// dopóki kolejka pusta
7
G R A F Y
{
kolejka.obsluz(s);
// usuń z kolejki pewien wierzchołek s
v[s]=1;
// zaznacz wierzchołek s jako zbadany
for( int k=0; k<n; k++)
if(g[s][k]!=0)
//istnieje przejście
if(v[k]==0)
// k nie był jeszcze badany
{
v[k]=1;
// k jako zbadany
kolejka.wstaw(k);
}
}
}
Algorytmy obliczania optymalnych dróg w grafie.
Do tego typu algorytmów zaliczyć należy algorytmy Roy-Warshalla, Floyda-Warshalla
oraz algorytm Dijkstry. Podczas wykonywania różnego rodzaju operacji na grafach
przydatne okazują się algorytmy znajdywania najbardziej optymalnych dróg
przechodzenia po grafie.
Jednym z takich algorytmów jest algorytm Floyda - Warshalla.
D[i,j]=min(D[i,j], D[i,k]+D[k,j])
i
j
k
8
G R A F Y
Działanie algorytmu można wyjaśnić na przykładzie poniżej zaprezentowanego
grafu:
0
3
5
1
4
6
2
Dla powyżej zaprezentowanego grafu obliczenie, która z dróg od danego punktu
węzłowego do drugiego jest najbardziej optymalna np.: od 1 do 4, polega na badaniu
poszczególnych krawędzi grafu. Bierzemy wówczas pod uwagę przydzielone im wagi
i sumujemy je w celu konkretnego określenia kosztu, jaki poniesieni przechodząc
daną drogą. Przykładowo przechodząc po grafie od punktu 0 do 4, mamy do wyboru
dwie drogi, z czego jedna jest bardziej optymalna, a druga mniej.
Droga 1 od punktu -> 1 do 4: 0 – 1 – 2 – 4 koszt 45
Droga 2 od punktu -> 1 do 4: 0 – 1 – 4
koszt 50
Poniższy algorytm zapisany w kodzie c++, wyłącznie oblicza wartość optymalnej
drogi, ale jej nie zapamiętuje.
void floyd( int g[n][n] )
{
for (int k=0; k<n; k++)
for ( int i=0; i<n; i++)
for ( int j=0; j<n; j++)
g[i][j]=min(g[i][j], g[i][k]+g[k][j])
}
Aby algorytm mógł nie tylko obliczać drogę, ale także zapamiętywać która z nich
była bardziej optymalna należy wprowadzić następującą poprawkę do algorytmu
Floyda:
9
G R A F Y
if (g[i][k]+g[k][j] < g[i][j])
{
g[i][j] = g[i][k]+g[k][j];
R[i][j]=k;
//tablica kierowania ruchem
}
Optymalna droga będzie zapamiętywana w matrycy kierowania ruchem R. Aby
odtworzyć optymalną drogę od wierzchołka i do wierzchołka j, patrzymy na wartość
R[I][J]. Jeśli jest ona równa zero, to mamy do czynienia z przypadkiem
elementarnym, tzn. z krawędzią którą należy przejść. Jeśli nie, to droga wiedzie od i
do R[I][J] i następnie od R[I][J] do j. Z uwagi na to, że powyższe drogi mogą nie być
elementarne, łatwo zauważyć rekurencyjny charakter procedury.
void droga (int i, int j)
{
int k=R[i][j];
if (k!=0)
{
droga (i,k);
cout << k <<” ”;
droga(k,j);
}
}
10

Podobne dokumenty