Grafy, przechodzenie grafu, minimalne drzewo rozpinające

Download Report

Transcript Grafy, przechodzenie grafu, minimalne drzewo rozpinające

Algorytmy i Struktury Danych
Wykład 5
Grafy. Przechodzenie grafu wszerz, w głąb.
Minimalne drzewo rozpinające: algorytm
Kruskala i algorytm Prima.
Podstawowe definicje







Grafem to struktura oznaczona jako G=(V,E) składająca się
ze skończonego i niepustego zbioru wierzchołków V i zbioru
krawędzi E.
Krawędź od wierzchołków u do v oznaczamy jako (vi,vj).
Drogą od wierzchołka v1 do vn nazywamy sekwencję
krawędzi: (v1,v2), (v2,v3),…, (vn-1,vn)
Dwa wierzchołki vi i vj nazywamy sąsiędnimi, jeżeli w zbiorze
D istnieje krawędź (vi,vj). Taką krawędź nazywamy incydentną
do wierzchołków vi, vj.
Stopniem wierzchołka v deg(v) – nazywamy liczbę krawędzi
do niego incydentnych. Jeśli deg(v)=0 to jest to wierzchołek
izolowany.
Graf rzadki = to graf, dla którego |E| jest dużo mniejsze od
|V|2.
Graf gęsty – to graf, dla którego |E| jest bliskie |V|2.
Oznaczenia dla złożoności

Czas działania dla grafu G=(V,E)
najczęściej się mierzy w zależności od
liczby wierzchołków |V| i liczby krawędzi
|E|. Przyjmijmy, że zapis O(|V||E|)
będziemy oznaczać jako O(V E).
Rodzaje grafów:
Grafy proste
graf pełny – każdy
wierzchołek jest połączony
krawędzią z każdym
wierzchołkiem
Rodzaje grafów:
Grafy złożone
Multigraf – dwa wierzchołki mogą
być połączone kilkoma
krawędziami
Pseudograf – pojawiają się pętle
czyli krawędzie wychodzące z i
wchodzące do tego samego
wierzchołka
Rodzaje grafów:
Grafy złożone
Graf skierowany – krawędzie,
zwane też łukami, spełniają
warunek (u,v)≠(v,u)
5
Graf ważony – gdy
krawędziom zostają
przyporządkowane
liczby – wagi
4
2
6
1
5
2
5
REPREZENTACJA
GRAFÓW:
Lista sąsiedztwa
1
2
3
4
5
234
1
14
13
Lista sąsiedztwa





Niech będzie dany graf G=(V,E). Jego reprezentacja w postaci listy sąsiedztwa
będzie zapisana w tablicy Adj zawierającej |V| list, gdzie każda lista odpowiadać
będzie. odpowiedniemu wierzchołkowi z V. Dla każdego uV elementami listy
sąsiedztwa Adj[u] są wszystkie wierzchołki v takie że krawędź (u,v) należy do
zbioru E. To oznacza, że Adj[u] zawiera wszystkie wierzchołki (lub wskaźniki do
nich) sąsiadujące z u w grafie G.
Formę listową preferuję się dla grafów rzadkich. Formę macierzową dla gęstych
lub w przypadku
Jeżeli graf G będzie miał skierowane krawędzie, to suma długości wszystkich list
będzie wynosić |E|, związane jest to z tym, że krawędź postaci (u,v) to
wystąpienie v na liście Adj[u]. Jeżeli zaś graf G będzie miał nieskierowane
krawędzie to suma długości wszystkich list wierzchołków sąsiednich będzie
dana wzorem 2|E|, gdyż dla nieskierowanej krawędzi (u,v), u znajdzie się na
liście sąsiadów v a v na liście sąsiadów wierzchołka u.
Rozmiar wymaganej pamięci przez reprezentację listową, niezależnie czy graf
jest skierowany czy nie, wynosi (V+E).
Listę sąsiedztwa można wykorzystać do reprezentacji grafów ważonych.
Wówczas posługujemy się funkcją wagową w:ER . Dla danego grafu
ważonego G=(V,E) funkcję wagową dla krawędzi (u,v) zapiszemy jako w(u,v). W
praktyce oznacza to, że wagę pamiętamy obok wierzchołka sąsiedniego.
Macierz sąsiedztwa
1
2
3
4
5
1
0
1
1
1
0
2
1
0
0
0
0
3
1
0
0
1
0
4
1
0
1
0
0
5
0
0
0
0
0
Macierz sąsiedztwa

Niech w grafie G=(V,E) wierzchołki będą ponumerowane od
1,2, …, |V| w pewien dowolny sposób. Jeżeli mamy etykiety
wierzchołków zapisane innymi znakami należy im
przyporządkować numery od 1,2, …, |V|. Wówczas macierz
sąsiedztwa dla grafu G będzie zapisana w postaci macierzy
A=(aij) wymiaru |V|x|V| w sposób następujący:
1
𝑗𝑒ś𝑙𝑖 (𝑖, 𝑗) ∈ 𝐸
0 𝑤 𝑝𝑟𝑧𝑒𝑐𝑖𝑤𝑛𝑦𝑚 𝑝𝑟𝑧𝑦𝑝𝑎𝑑𝑘𝑢
 Dla grafów nieskierowanych macierz sąsiedztwa jest
symetryczna.
 Niech graf G=(V,E) będzie grafem ważonym z funkcją
wagową w. Wówczas waga w(u,v) krawędzi (u,v)E będzie
zapisana w wierszu u i kolumnie v macierzy sąsiedztwa. Jeśli
dana krawędź nie istnieje to wpisujemy, NIL, 0 albo ∞, w
zależności od implementacji analizowanego problemu.

𝑎𝑖𝑗 =
Macierz incydencji
1
2
3
4
5
1
2
3
4
5
(1,2) (1,3) (1,4)(3,4)
1
1
1 0
1
0
0 0
0
1
0 1
0
0
1 1
0
0
0 0
1 2 3 4 5
0 -1 -1 1 0
1 0 0 0 0
1 0 0 -1-1
-1 0 1 0 0
0 0 1 0 0
Macierz incydencji



Niech będzie dany graf G=(V,E). Jeżeli graf G będzie grafem
skierowanym wówczas macierz incydencji zdefiniujemy jako
macierz B=(bij) o wymiarze |V|x|E| taką, że
−1,
𝑏𝑖𝑗 = 1,
0
𝑗𝑒ś𝑙𝑖 𝑘𝑟𝑎𝑤ę𝑑ź 𝑗 𝑤𝑦𝑐ℎ𝑜𝑑𝑧𝑖 𝑧 𝑤𝑖𝑒𝑟𝑧𝑐ℎ𝑜ł𝑘𝑎 𝑖,
𝑗𝑒ś𝑙𝑖 𝑘𝑟𝑎𝑤ę𝑑ź 𝑗 𝑤𝑐ℎ𝑜𝑑𝑧𝑖 𝑑𝑜 𝑤𝑖𝑒𝑟𝑧𝑐ℎ𝑜ł𝑘𝑎 𝑖,
𝑤 𝑝𝑟𝑧𝑒𝑐𝑖𝑤𝑛𝑦𝑚 𝑝𝑟𝑧𝑦𝑝𝑎𝑑𝑘𝑢.
Jeżeli graf G będzie grafem nieskierowanym wówczas macierz
incydencji zdefiniujemy jako macierz C=(cij) o wymiarze |V|x|E|
taką, że
𝑐𝑖𝑗
=
1
0
𝑗𝑒ż𝑒𝑙𝑖 𝑘𝑟𝑎𝑤ę𝑑ź 𝑖, 𝑗 𝑗𝑒𝑠𝑡 𝑖𝑛𝑐𝑦𝑑𝑒𝑛𝑡𝑛𝑎 𝑑𝑜 𝑤𝑖𝑒𝑟𝑧𝑐ℎ𝑜ł𝑘ó𝑤 𝑖, 𝑗
𝑤 𝑝𝑟𝑧𝑒𝑐𝑖𝑤𝑛𝑦𝑚 𝑝𝑟𝑧𝑦𝑝𝑎𝑑𝑘𝑢.
Algorytm przechodzenia grafu
Niech G(V,E), n- liczba elementów zbioru V, m – liczba
elementow zbioru m, pV.
 Wybieramy wierzchołek startowy pV, odwiedzamy
go i zaznaczamy jako odwiedzony. Odkładamy
wierzchołek w wybranej strukturze danych (stos –
przechodzenie w głąb, kolejka –przechodzenie
wszerz).
 Wykonujemy dopóki struktura danych wybrana do
przechowywania wierzchołków nie jest pusta

 odczytujemy numer wierzchołka ze struktury,
 Jeśli odczytany wierzchołek ma sąsiada to:
 odwiedzamy najbliższego jego „sąsiada” i zaznaczamy go jako odwiedzony,
 jeśli „sąsiad” ma wierzchołki do odwiedzenia to odkładamy go do wybranej
struktury,
 jeśli odczytany wierzchołek nie ma sąsiada to usuwamy go ze
struktury.
Przechodzenie grafu w głąb
Inna nazwa dfs z ang. Depth First Search
Założenia:
 Graf jest reprezentowany w postaci listy
sąsiedztwa lstw, gdzie lstw[i][0] – liczba sąsiadów
dla i-tego wierzchołka, lstw[i][k] – gdzie
k=1,2,…,maxs przyjmują wartości kolejnych
wierzchołków „sąsiadów”,
 visited[|V|] – tablica przechowująca informacje,
czy wierzchołek był odwiedzony – 1 czy nie -0,
 current[|V|] – tablica przechowująca wskaźniki do
najbliższego sąsiada danego wierzchołka.
 Wierzchołki grafu są przechowywane na stosie,

DFS
if ((current[p]) <= (lstw[p][0])) {
void dfs(int n){
stos[0]=0;
int startowy=1;
push(stos,p);
short int visited[maxw];
while (stos[0] > 0) {
int current[maxw];
int v,p,w;
v=front(stos);
for (v=1;v<=n;v++) {
w=lstw[v][current[v]];
visited[v]=0;
current[v]++;
current[v]=startowy;
if (current[v]>lstw[v][0])pop(stos);
}
if (visited[w]!=1){
p=startowy;
wizyta(w);
wizyta(p); // odwiedzamy
visited[w]=1;
wierzchołek p
if ((current[w]) <= (lstw[w][0]))
visited[p]=1;// zaznaczmy p
jako odwiedzony
push(stos,w);
} }} }
Przechodzenie wszerz
Inna nazwa bfs - Breadth-first search
Założenia:
 Graf jest reprezentowany w postaci listy
sąsiedztwa lstw, gdzie lstw[i][0] – liczba sąsiadów
dla i-tego wierzchołka, lstw[i][k] – gdzie
k=1,2,…,maxs przyjmują wartości kolejnych
wierzchołków „sąsiadów”,
 visited[|V|] – tablica przechowująca informacje,
czy wierzchołek był odwiedzony – 1 czy nie -0,
 current[|V|] – tablica przechowująca wskaźniki do
najbliższego sąsiada danego wierzchołka.
 Wierzchołki grafu są przechowywane w kolejce,

BFS
void dfs(int n){
int startowy=1;
short int visited[maxw];
int current[maxw];
int v,p,w;
for (v=1;v<=n;v++) {
visited[v]=0;
current[v]=startowy;
}
p=startowy;
wizyta(p); // odwiedzamy
wierzchołek p
visited[p]=1;// zaznaczmy p jako
odwiedzony
if ((current[p]) <= (lstw[p][0])) {
enqueue(p);
while (kolejka jest nie pusta) {
v=front(kolejka);
w=lstw[v][current[v]];
current[v]++;
if
(current[v]>lstw[v][0])dequeue();
if (visited[w]!=1){
wizyta(w);
visited[w]=1;
if ((current[w]) <=
(lstw[w][0])) enqueue(w);
} }} }
Przechodzenie w głąb-Przykład
Wierzchołek startowy = {1},
S- stos
Vo – zbiór wierzchołków odwiedzonych
1
1
2
4
8
5
S={1}
Vo ={1}
2
3
4
8
5
7
6
3
S={1,2}
Vo ={1,2}
7
6
1
1
2
2
3
4
8
5
4
8
5
7
6
S={1,2,3}
3
S={1,2,3,8}
Vo ={1,2,3}
7
6
Vo ={1,2,3,8}
1
1
2
4
8
5
S={1,2,3,8,7}
Vo ={1,2,3,8,7}
2
3
7
6
3
4
8
5
6
S={1,2,3,8,7,6}
Vo ={1,2,3,8,7,6}
7
1
1
2
2
3
4
8
5
S={1,2,3,8,7,6,5}
Vo ={1,2,3,8,7,6,5}
4
8
5
7
6
3
S=}
7
6
Vo ={1,2,3,8,7,6,4}
Przechodzenie wszerz przykład
Wierzchołek startowy = {1},
K - kolejka
Vo – zbiór wierzchołków odwiedzonych
1
1
2
4
8
5
K={1}
Vo ={1}
2
3
7
6
3
4
8
5
K={1,2}
Vo ={1,2}
7
6
1
1
2
2
3
4
4
8
5
8
5
7
K={1,2,3}
3
7
K={2,3,4}
6
6
Vo ={1,2,3,4}
Vo ={1,2,3}
1
1
2
4
8
5
K={2,3,4,5}
Vo ={1,2,3,4,5}
2
3
7
6
3
4
8
5
6
K={2,3,4,5,7}
Vo ={1,2,3,4,5,7}
7
1
1
2
2
3
4
8
5
K={3,4,5,7,8}
Vo ={1,2,3,4,5,7,8}
4
8
5
7
6
3
K={}
7
6
Vo ={1,2,3,4,5,7,8,6}
Minimalne drzewa ropinające
MST- minimum spanning tree
Niech graf G=(V,E) będzie spójnym
grafem nieskierowanym zaś z każdą
krawędzią (u,v) E jest związana waga
w(u,v). Wówczas minimalnym drzewem
rozpinającym będziemy nazywać drzewo T
będące acyklicznym podzbiorem TE,
łączącym wszystkie wierzchołki i którego
łączna waga określona wzorem:
 𝑤 𝑇 = (𝑢,𝑣)∈𝑇 𝑤(𝑢, 𝑣)
 jest najmniejsza.

Rozrastanie się minimalnego drzewa
rozpinającego

Załóżmy, że mamy spójny graf nieskierowany G=(V,E)
z funkcją wagową w:ER. Aby znaleźć minimalne
drzewo rozpinające możemy użyć algorytmu
zachłannego polegającego na dołączaniu pojedynczych
krawędzi w poszczególnych krokach. Oznaczmy przez
Z – zbiór krawędzi. Na początku każdej iteracji zbiór
Z stanowi podzbiór pewnego minimalnego drzewa
rozpinającego. Jest to niezmiennik pętli. W każdym
kroku szukamy krawędzi (u,v) którą możemy dodać
do Z tak aby Z{(u,v)} był podzbiorem minimalnego
drzewa rozpinającego. Jest to krawędź bezpieczna
dla Z, gdyż można ją dodać bezpiecznie do Z bez
naruszenia niezmiennika.
MST - algorytm generyczny
void generic_mst(graf G, waga w)
{
Z=
while Z nie tworzy minimalnego
drzewa rozpinającego
znajdź krawędź (u,v), będącą
bezpieczną dla Z
Z=Z{(u,v)}
return Z
}
Podstawowe definicje
Przekrój (S,V-S) grafu nieskierowanego G=(V,E)
nazywamy podział V na zbiory S i V-S.
 Jeżeli jeden z krańców krawędzi (u,v)E należy do S,
a drugi do V-S to mówimy wówczas, że krawędź ta
krzyżuje się z przekrojem (S,V-S).
 Przekrój uwzględnia zbiór krawędzi Z, jeżeli żadna z
krawędzi z Z nie krzyżuje się z tym przekrojem.

S
7
2
3
6
4
1
4
2
12
4
7
9
8
10
15
9
6
1
5
7
2
V-S
6
11
Podstawowe definicje

Jeżeli krawędź krzyżuje się z przekrojem
to nazywa się ją krawędzią lekką, pod
warunkiem że jej waga jest najmniejsza
spośród wszystkich wag krawędzi
krzyżujących się z tym przekrojem.
Uogólniając, krawędź będziemy nazywać
krawędzią lekką o danej własności,
jeżeli jej waga będzie najmniejsza spośród
wag wszystkich krawędzi o danej
własności.
Twierdzenie
 Niech będzie dany spójny graf nieskierowany G=(V,E) z
funkcją wagową w:ER, Z określone jako podzbiór E
zawarty w pewny minimalnym drzewie rozpinającym grafu G
oraz niech (S, V-S) będzie dowolnym przekrojem G
uwzględniającym Z i niech (u,v) będzie krawędzią lekką
krzyżującą się z (S,V-S). Wówczas krawędź (u,v) jest
bezpieczna dla Z.
Wniosek
 Niech G=(V,E) będzie spójnym grafem nieskierowanym z
funkcją wagową w o wartościach rzeczywistych, określoną
na E. Niech Z będzie podzbiorem E zawartym w pewnym
minimalnym drzewie rozpinającym grafu G i niech D=(VD,ED)
będzie spójną składową (drzewem) w lesie Gz=(V,Z). jeśli
(u,v) jest krawędzią lekką łączącą D z pewną inną składową w
Gz, to krawędź (u,v) jest bezpieczna dla Z.
Algorytm Kruskala
void mst_kruskal(graf G, wagi w)
{
A=
for każdy wierzchołek vG.V
MAKE_SET(v)
posortuj krawędzie z G.E rosnąco względem wag
w
for każda krawędź (u,v)G.E, w kolejności
rosnących wag
{if FIND_SET(u)FIND_SET(v)
Z=Z{(u,v)}
UNION(u,v)
}
return Z
}
Znaczenie operacji z pseudokodu
MAKE_SET(x) – tworzy nowy zbiór, którego
jedynym elementem jest x i x nie może być
elementem innego zbioru (warunek
rozłączności zbiorów),
 UNION(y, z) łączy dwa zbiory dynamiczne
zawierające odpowiednio y i z w nowy zbiór
będący ich sumą. Zakładamy, że przed
wykonaniem operacji te dwa zbiory były
rozłączne.
 FIND_SET(x) – zwraca wskaźnik do
reprezentanta (jedynego) zbioru
zawierającego x.

Algorytm Kruskala - przykład
7
2
3
6
4
1
4
2
12
4
7
9
8
10
15
9
6
5
1
7
2
6
11
7
2
3
6
4
1
4
2
12
4
7
9
8
10
15
9
6
1
5
7
2
6
11
7
2
3
6
4
1
4
2
12
4
8
10
15
9
7
9
6
5
1
7
2
6
11
7
2
3
6
4
1
4
2
12
4
8
10
15
9
7
9
6
1
5
7
2
6
11
7
2
3
6
4
1
4
2
12
4
7
9
8
10
15
9
6
5
1
7
2
6
11
7
2
3
6
4
1
4
2
12
4
8
10
15
9
7
9
6
1
5
7
2
6
11
7
2
3
6
4
1
4
2
12
4
7
9
8
10
15
9
6
5
1
7
6
2
11
7
2
3
6
4
1
4
2
12
4
8
10
15
9
7
9
6
1
5
7
2
6
11
Algorytm Prima





. Krawędzie ze zbioru Z tworzą tu zawsze pojedyncze drzewo.
Najpierw drzewo zawiera dowolnie wybrany wierzchołek-korzeń r.
Następnie w każdym kroku dodaje się do niego krawędź lekką
łączącą wierzchołek z drzewa Z z izolowanym wierzchołkiem
Gz=(V,Z).
Wszystkie wierzchołki znajdujące się poza drzewem, ustawione są
w kolejce priorytetowej Q typu min. Dla każdego wierzchołka v
kluczem v.key , który ustala pozycję w kolejce, jest minimalna waga
spośród wag krawędzi łączących v z wierzchołkami drzewa. Jeśli
krawędzi nie ma to przyjmujemy, że v.key=∞. Natomiast atrybut v.
oznacza ojca wierzchołka v w obliczanym drzewie. Podczas
wykonywania algorytmu zbiór Z z procedury generic_mst jest
pamiętany niejawnie jako
Z={(v,v.):vV-{r}-Q}
Kiedy algorytm kończy zadanie, kolejka priorytetowa Q jest pusta a
minimalnym drzewem rozpinającym Z w grafie G jest:
Z={(v,v.):vV-{r}}.
Algorytm Prima - pseudokod
void mst_prim(graf G, wagi w, korzeń r)
{
for każdy uG.V
{
u.key=∞
u.=NIL
}
r.key=0
Q=G.V
while Q
{
u=EXTRACT-MIN(Q)
for każdy vG.adj[u]
if vQ I w(u,v)<v.key
{
v.=u
v.key=w(u,v)
}}}
Algorytm Prima - przykład
7
2
3
6
4
1
4
2
12
4
7
9
10
15
9
8
6
5
1
7
2
6
11
7
2
3
6
4
1
4
2
12
4
8
10
15
9
7
9
6
1
5
7
2
6
11
7
2
3
6
4
1
4
2
12
4
7
9
8
10
15
9
6
5
1
7
6
2
11
7
2
3
6
4
1
4
2
12
4
8
10
15
9
7
9
6
1
5
7
2
6
11
7
2
3
6
4
1
4
2
12
4
8
10
15
9
7
9
6
5
1
7
2
6
11
7
2
3
6
4
1
4
2
12
4
7
9
8
10
15
9
6
1
5
7
2
6
11
7
2
3
6
4
1
4
2
12
4
10
15
9
7
9
8
6
5
1
7
6
2
11
7
2
3
6
4
12
1
4
2
4
8
10
15
9
7
9
6
1
5
7
2
6
11
7
2
3
6
4
1
4
2
12
4
9
8
6
5
1
7
6
2
V

1 NIL
2 NIL
3 NIL
4 NIL
5 NIL
6 NIL
7 NIL
8 NIL
9 NIL
w
∞
∞
∞
∞
∞
∞
∞
∞
∞


w
10
15
9
7

w
11

w

w
---- ---- ---- ---- ---- ---- ---- ---1
NIL
NIL
NIL
NIL
NIL
1
NIL
2
NIL
NIL
NIL
NIL
9
∞
1
NIL
3
NIL
∞
3
NIL
9
∞
6
w
---- ---- ---- ---- ---- ----
3
6
6
11
4 ---- ----
∞

---- ---- ---- ---- ---- ----
7 ---- ---- ---- ----
∞
∞
∞
∞
w
---- ---- ---- ---- ---- ----
4 ---- ---- ---- ---- ---- ----
∞
∞
∞
∞
∞

w
3
6 ---- ---- ---- ----
6 11
4 10 ---- ----
---- ---- ---- ---- ---- ----
6
2 ---- ---- ---- ---- ---- ----
1
9
1
9
3
2 ---- ----
7
1 ---- ---- ---- ----
---- ---- ---- ---- ---- ----
Bibliografia
A. Drozdek, „C++. Algorytmy i struktury
danych”, Helion, Gliwice 2004;
 L. Banachowski, K. Diks, W. Rytter,
„Algorytmy i struktury danych”, WNT,
Warszawa1996;
 Cormen Thomas; Leiserson Charles;
Rivest Ronald; Stein Clifford,
„Wprowadzenie do Algorytmów”,
Wydawnictwo Naukowe PWN, Warszawa
2012.
