• Nie Znaleziono Wyników

RKI — Zajęcia 13 — Przeszukiwanie grafu wszerz

N/A
N/A
Protected

Academic year: 2021

Share "RKI — Zajęcia 13 — Przeszukiwanie grafu wszerz"

Copied!
15
0
0

Pełen tekst

(1)

RKI — Zajęcia 13 — Przeszukiwanie grafu wszerz

Piersa Jarosław 2010-04-25

1 Wprowadzenie

Biega, krzyczy pan Hilary:

”Gdzie są moje okulary?”

Szuka w spodniach i w surducie, W prawym bucie, w lewym bucie.

Julian Tuwim

Pan Hilary swoich okularów szukał osobiście. Na dzisiejszej lekcji zastanowimy się jak wykorzystać do szukania komputer.

Wymagania wstępne:

• grafy, sposoby reprezentowania grafu,

• kolejka.

2 Czego można w grafie szukać

2.1 Problem

Przypomnijmy, że graf składa się z wierzchołków połączonych krawędziami. Wierzchołki mogą być za- równo bytami abstrakcyjnymi jak liczby, ale również i bytami jak najbardziej realnymi. W naszym przykładzie będą to pomieszczenia w domu Pana Hilarego. Pamiętajmy, że krawędzie w grafie okre- ślają możliwość bezpośredniego przejścia pomiędzy przeszukiwanymi obszarami. Wierzchołki połączone krawędzią będziemy dalej nazywać sąsiednimi.

Sąsiedztwo wierzchołków jest informacją, którą należy uwzględnić przeszukując graf. Informuje, że np. po sprawdzeniu kuchni Pan Hilary może się skierować na przykład do przedpokoju. Będziemy w tej lekcji zakładać, że graf jest nieskierowany, czyli krawędzie (możliwości przejścia) są symetryczne. Jeżeli zatem z kuchni można się skierować do wspomnianego przedpokoju, to zawsze można i z przedpokoju wrócić do kuchni.

Pan Hilary musi skądś rozpocząć poszukiwania — prawdopodobnie z kuchni, gdzie chciał poczytać gazetę przy porannej kawie.

Jeżeli może się udać w dwa różne miejsca, musi wybrać jedno z nich, ale również pamiętać by zajrzeć i do tego drugiego. Przykładowo z kuchni może iść do przedpokoju albo do sypialni. Pan Hilary wybrał przedpokój, ale po jego sprawdzeniu musi pamiętać by wrócić do sypialni. Sytuacja się komplikuje gdy z przedpokoju może iść również do biblioteczki. Pan Hilary zdecydował się sprawdzać pokoje w kolejności od tych sąsiadujących z kuchnią. Kolejność tę musi zapisywać na swojej podręcznej liście (jak ją odczytał bez okularów?). Ostatnią ważną obserwacją jest by nie szukać dwa razy w tym samym miejscu.

2.2 Algorytm

Przetłumaczmy problem na język grafów. Dane mamy:

• graf, którego wierzchołki reprezentują pomieszczenia w domu, a krawędzie możliwości przejścia między tymi pomieszczeniami,

• wierzchołek, który chcemy znaleźć w grafie, w naszym przykładzie pokój, w którym zostawione są okulary,

(2)

Rysunek 1: Graf pomieszczeń w domu Pana Hilarego.

• wierzchołek, od którego rozpoczynamy szukanie.

Potrzebna nam będzie tablica o rozmiarze równym liczbie wierzchołków w grafie, w której możemy oznaczyć wierzchołki jako odwiedzone lub nieodwiedzone. Na początku wszystkie są nieodwiedzone, wraz z postępem algorytmu będziemy je odwiedzać i oznaczać, tak aby uniknąć wielokrotnego sprawdzania jednego wierzchołka.

Dodatkowo będzie nam potrzebna kolejka, służąca do przechowywania wierzchołków, które oczekują na sprawdzenie. Na początku oczekuje tylko wierzchołek startowy, zatem wstawiamy go do kolejki i jednocześnie oznaczamy jako odwiedzony.

Następnie powtarzamy następujące kroki tak długo jak w kolejce będą oczekujące wierzchołki:

• zdejmujemy wierzchołek z czoła kolejki,

• jeżeli jest tym szukanym, to znaleźliśmy i możemy zakończyć poszukiwania,

• jeżeli nie, to dla każdego sąsiada sprawdzamy czy tenże sąsiad był już odwiedzony,

• jeżeli nie był, to oznaczamy go jako odwiedzonego, by więcej do niego nie wracać i dodajemy do kolejki.

Jeżeli w kolejce nie ma więcej elementów, to oznacza to, że przeszukaliśmy cały fragment grafu, jaki jest osiągalny z wierzchołka startowego. Jeżeli do tego czasu nie znaleźliśmy wierzchołka szukanego to, nie da się do niego dotrzeć.

Ponieważ komputer lepiej radzi sobie z zapamiętywaniem liczb niż okularów, w algorytmie będziemy szukać zmiennych typu int (na przykład 8).

Poniżej podany jest zapis algorytmu w pseudokodzie:

// dane:

int start;

int szukanyElement;

bool czyJuzOdwiedzony[n] = {false, ..., false};

kolejka = pustaKolejka();

czyJuzOdwiedzony[start] = true;

kolejka.push_back(start);

while (kolejka.empty() == false){

int w = kolejka.front();

kolejka.zdejmijPierwszyElement() if (w == szukanyElement){

kolejka.clear();

return "znalazlem";

} // if

(3)

for (int v = sasiedziWezla(w)){

if (czyJuzOdwiedzony[v] == false){

czyJuzOdwiedzony[v] = true;

kolejka.push_back(v);

} // if } // for v } // while

return "nie znalazlem";

2.3 Wyszukiwanie najkrótszej drogi

Zauważmy jeszcze jedną bardzo ważną właściwość tego algorytmu. Algorytm BSF zawsze dochodzi do wierzchołka najkrtótszą (tj. liczącą najmniej krawędzi) drogą, o ile istnieje jakakolwiek droga.

Aby tę drogę odtworzyć należy dla każdego wierzchołka, za wyjątkiem startowego, zapamiętać jego

„rodzica”, z którego do wierzchołka doszliśmy. Będzie nam do tego potrzebna tablica. Posłuży nam ona również do oznaczania wierzchołków nieodwiedzonych (w kodzie jest to wartość −1). Wierzchołek star- towy rodzica nie ma — jest początkiem wszystkich ścieżek, dlatego też rezerwujemy dla niego dodatkowe oznaczenie (u nas −10). Wierzchołki są numerowane liczbami od 0 do n − 1 więc oznaczenia nie będą kolidowały. Każde inne oznaczenia, które nie wprowadzają kolizji, będą równie dobre.

Aby odtworzyć drogę należy, po przeszukaniu grafu wybrać krawędź pomiędzy wierzchołkiem szuka- nym a jego „rodzicem”, następnie pomiędzy tymże „rodzicem” a „rodzicem rodzica” itd. aż dojdziemy do wierzchołka startowego

Tym sposobem jesteśmy w stanie zbudować drzewo najkrótszych dróg wychodzących z wierzchołka startowego. Określa się je również mianem drzewa osiągalności lub drzewa BFS.

Zapis algorytmu w pseudokodzie:

// dane:

int start;

int szukanyElement;

// rodzice

// tablica służy nam również do pamiętania czy wierzchołek został odwiedzony // w tym przypadku -1 oznacza, że nie został

int rodzice[n] = {-1, ..., -1};

kolejka = pustaKolejka();

// oznaczamy wierzchołek startowy, musimy przypisać wartość, która nie jest // numerem żadnego wierzchołka, ani oznaczeniem "nieodwiedzony"

czyJuzOdwiedzony[start] = -10;

kolejka.push_back(start);

while (kolejka.empty() == false){

int w = kolejka.front();

kolejka.zdejmijPierwszyElement() for (int v = sasiedziWezla(w)){

// wierzchołek v jeszcze nie był odwiedzony if ( rodzice[v] == -1){

// przypisujemy wierzchołkowi v jego rodzica - w czyJuzOdwiedzony[v] = w;

kolejka.push_back(v);

} // if } // for v } // while

// odczytywanie ścieżki

if (rodzice[szukanyElement] == -1){

return "nie znalazlem";

(4)

} else {

int wezel = szukanyElement;

int rodzic = rodzice[szukanyElement];

// dopóki nie dojdziemy do korzenia while (rodzic != -10){

std::cout << "(" << wezel << " " << rodzic << ")\n";

// przechodzimy o jeden krok w górę ścieżki;

wezel = rodzic;

rodzic = rodzice[wezel];

} // while

return "znalazlem";

} // if ... else

Uwaga. W tym punkcie jako najkrótszą drogę rozumiemy tę, która liczy najmniej krawędzi. W lekcji 15. poznamy inny sposób definiowania „odległości” pomiędzy wierzchołkami w grafie, który będzie wymagał innego algorytmu.

2.4 Analiza złożoności

Oznaczmy liczbę wierzchołków w grafie przez n oraz liczbę krawędzi przez m.

Algorytm na pierwszy rzut oka może wyglądać kosztownie, ze względu na złożoność czasową, gdyż ma pętlę for zagnieżdżoną wewnątrz while. Zauważmy jednak, że w każdym kroku z kolejki zdejmujemy jeden element. Z drugiej strony każdy z elementów dodajemy co najwyżej jeden raz, a nie możemy go zdjąć z kolejki jeżeli nie został wcześniej dodany. Co za tym idzie sumaryczna liczba dodawań (i analogicznie zdejmowań) z kolejki nie przekracza liczby wierzchołków w grafie czyli n.

Nieco gorzej jest ze sprawdzaniem czy wierzchołek był już odwiedzony. Na złożoność okazuje się mieć wpływ wykorzystany sposób reprezentacji grafu.

Dla list sąsiedztwa. Dla każdego wierzchołka trzeba sprawdzić wszystkich jego sąsiadów. Wierzcho- łek ma tylu sąsiadów, ile krawędzi z niego wychodzi. Każda krawędź łączy dwa wierzchołki, a każdy wierzchołek jest sprawdzany (co najwyżej) raz. Co za tym idzie, liczba sprawdzań nie przekracza liczby krawędzi w grafie przemnożonej przez 2. Otrzymujemy zatem algorytm o złożoności O(n + m).

Dla macierzy sąsiedztwa. Sprawdzenie wszystkich sąsiadów zawsze wymaga sprawdzenia całego wier- sza macierzy, który ma długość n, a w pesymistycznej sytuacji sprawdzamy dla każdego z n wierzchołków.

Oznacza to, że złożoność obliczeniowa wyniesie O(n2).

Złożoność pamięciowa. Algorytm wymaga tablicy pomocniczej rozmiaru n oraz kolejki. W kolejce przetrzymywane są wierzchołki, przy czym jeden wierzchołki co najwyżej jeden raz, a zatem liczba ele- mentów w kolejce nie przekroczy n. Złożoność pamięciowa wynosi zatem O(n).

Uwaga: w tej analizie nie bierzemy pod uwagę rozmiaru danych wejściowych. Lista krawędzi oraz dwie tablice zajmują O(n + m) pamięci, a macierz sąsiedztwa O(n2).

3 Ćwiczenie

Napisz program, który wczytuje kolejno:

• n liczbę wierzchołków w grafie,

• m liczbę krawędzi w grafie,

• m par liczb a b rozdzielonych spacjami, reprezentujących krawędzie w grafie, w którym wierzchołki są numerowane od 0 do n − 1 włącznie,

• x szukany wierzchołek w grafie,

• s wierzchołek grafu, od którego należy rozpocząć poszukiwanie.

(5)

Jako wynik program powinien wypisać: „TAK”, jeżeli da się znaleźć x startując z s i „NIE” w przeciwnym wypadku.

Wskazówki:

1. W lekcji 12 były omawiane metody reprezentowania grafu, rekomendowane to: dwie tablice, listy sąsiedztwa.

2. Tablice w C++ oraz Javie są indeksowane od zera, wykorzystaj to do wygodnej reprezentacji grafu.

Wbrew pozorom jest to znaczne ułatwienie.

3. Skorzystaj z dostępnych struktur w bibliotece standardowej C++ / Javy.

Porównaj czas działania algorytmu gdy graf jest reprezentowany przez macierz sąsiedztwa, z czasem uzyskanym dla list sąsiedztwa.

3.1 Rozwiązanie dla C++:

#include <i o s t r e a m >

#include <v e c t o r >

#include <queue>

i n t n ;

s t d : : v e c t o r <v e c t o r <int> > s a s i e d z i ; s t d : : queue<int> k o l e j k a ;

void w y c z y s c ( ) ;

i n t main ( i n t a r g c , char ∗∗ a r g v ) { s t d : : c i n >> n ;

// tworzymy p u s t e l i s t y s a s i a d o w f o r ( i n t i =0; i <n ; i ++){

v e c t o r <int> v ;

s a s i e d z i . p u s h b a c k ( v ) ;

} // f o r

i n t m;

s t d : : c i n >> m;

// w c z y t u j e m y k r a w e d z i e f o r ( i n t i =0; i <m; i ++){

i n t a , b ;

s t d : : c i n >> a >> b ;

s a s i e d z i . a t ( a ) . p u s h b a c k ( b ) ; s a s i e d z i . a t ( b ) . p u s h b a c k ( a ) ;

} // f o r

i n t s z u k a n y ;

s t d : : c i n >> s z u k a n y ; i n t s t a r t ;

s t d : : c i n >> s t a r t ;

// t a b l i c a pomocnicza bool czyOdwiedzony [ n ] ; f o r ( i n t i =0; i <n ; i ++){

czyOdwiedzony [ i ] = f a l s e ;

} // f o r

// a l g o r y t m BSF

// dodajemy do k o l e j k i s t a r t o w y e l e m e n t czyOdwiedzony [ s t a r t ] = true ;

k o l e j k a . push ( s t a r t ) ;

// d o p o k i w k o l e j c e s j a k i e k o l w i e k e l e m e n t y while ( k o l e j k a . empty ( ) == f a l s e ) {

// zdejmujemy e l e m e n t z k o l e j k i i n t w e z e l = k o l e j k a . f r o n t ( ) ; k o l e j k a . pop ( ) ;

(6)

// j e z e l i j e s t t o t e n s z u k a n y . . . i f ( w e z e l == s z u k a n y ) {

s t d : : c o u t << ”TAK\n” ; w y c z y s c ( ) ;

return 0 ;

} // i f

// d l a k a z d e g o s a s i a d a

f o r ( i n t i =0; i < s a s i e d z i . a t ( w e z e l ) . s i z e ( ) ; i ++){

i n t s a s i a d = s a s i e d z i . a t ( w e z e l ) . a t ( i ) ;

// j e z e l i s a s i a d n i e b y l o d w i e d z o n y t o wrzucamy go do k o l e j k i i f ( czyOdwiedzony [ s a s i a d ] == f a l s e ) {

czyOdwiedzony [ s a s i a d ] = true ; k o l e j k a . push ( s a s i a d ) ;

} // i f

} // f o r

} // w h i l e

s t d : : c o u t << ”NIE\n” ; w y c z y s c ( ) ;

return 0 ;

} // main ( )

void w y c z y s c ( ) {

// c z y s z c z e n i e g r a f u

f o r ( i n t i =0; i < s a s i e d z i . s i z e ( ) ; i ++){

s a s i e d z i . a t ( i ) . c l e a r ( ) ;

} // f o r

s a s i e d z i . c l e a r ( ) ;

// c z y s z c z e n i e k o l e j k i

while ( k o l e j k a . empty ( ) != f a l s e ) { k o l e j k a . pop ( ) ;

} // w h i l e

} // w y c z y s c ( )

3.2 Rozwiązanie w Javie

import j a v a . u t i l . S c a n n e r ; import j a v a . u t i l . V e c t o r ; import j a v a . u t i l . ArrayDeque ;

public c l a s s RKI BSF {

public s t a t i c void main ( S t r i n g [ ] a r g s ) { S c a n n e r s = new S c a n n e r ( System . i n ) ;

// w c z y t u j e m y i l o s c w e z l o w i k r a w e d z i i n t n = s . n e x t I n t ( ) ;

i n t m = s . n e x t I n t ( ) ;

V e c t o r<V e c t o r<I n t e g e r >> k r a w e d z i e = new V e c t o r<V e c t o r<I n t e g e r > >();

// tworzymy p u s t e l i s t y s a s i a d o w f o r ( i n t i =0; i <n ; i ++){

V e c t o r<I n t e g e r > v = new V e c t o r<I n t e g e r > ( ) ; k r a w e d z i e . add ( v ) ;

} // f o r

// dodajemy s a s i a d o w f o r ( i n t i =0; i <m; i ++){

i n t a = s . n e x t I n t ( ) ; i n t b = s . n e x t I n t ( ) ; k r a w e d z i e . g e t ( a ) . add ( b ) ; k r a w e d z i e . g e t ( b ) . add ( a ) ;

} // f o r

i n t s z u k a n y = s . n e x t I n t ( ) ; i n t s t a r t o w y = s . n e x t I n t ( ) ; // a l g o r y t m BSF :

// i n i c j a l i z u j e m y t a b l i c e pomocnicza boolean czyOdwiedzony [ ] = new boolean [ n ] ;

(7)

f o r ( i n t i =0; i <n ; i ++){

czyOdwiedzony [ i ] = f a l s e ;

} // f o r

// tworzymy k o l e j k e , dodajemy p i e r w s z y w i e s z c h o l e k ArrayDeque<I n t e g e r > k o l e j k a = new ArrayDeque<I n t e g e r > ( ) ; k o l e j k a . a d d L a s t ( s t a r t o w y ) ;

czyOdwiedzony [ s t a r t o w y ] = true ; boolean o d p o w i e d z = f a l s e ;

// d o p o k i k o l e j k a j e s t n i e p u s t a while ( k o l e j k a . s i z e ( ) > 0 ) {

// zdejmujemy e l e m e n t z k o l e j k i

i n t w i e r z c h o l e k = k o l e j k a . r e m o v e F i r s t ( ) ;

// j e s l i t o t e n s z u k a n y . . . i f ( w i e r z c h o l e k == s z u k a n y ) {

o d p o w i e d z = true ; k o l e j k a . c l e a r ( ) ; break ;

} // i f

// dodajemy s a s i a d o w do k o l e j k i

f o r ( i n t i =0; i < k r a w e d z i e . g e t ( w i e r z c h o l e k ) . s i z e ( ) ; i ++){

// j e z e l i s a s i a d n i e b y l o d w i e d z o n y // t o wrzucamy go do k o l e j k i

i n t s a s i a d = k r a w e d z i e . g e t ( w i e r z c h o l e k ) . g e t ( i ) ; i f ( czyOdwiedzony [ s a s i a d ] == f a l s e ) {

czyOdwiedzony [ s a s i a d ] = true ; k o l e j k a . add ( s a s i a d ) ;

} // i f

} // f o r

} // w h i l e

System . o u t . f o r m a t ( ”%s \n” , o d p o w i e d z ? ”TAK” : ”NIE” ) ;

} // main

} // c l a s s

4 Uwagi do rozwiązań

W programie skorzystaliśmy z bibliotek standardowych do pracy z kolejką oraz wektorem. Kolejka była omawiana w lekcji 11. Wektor był wprowadzony w lekcji 10. Zachęcamy do zapoznania się z treścią obu lekcji przed kontynuowaniem czytania.

4.1 Uwagi do rozwiązania w C++

Rozpoczniemy od omówienia kolejki. Abyśmy mogli użyć jej w programie musimy dołączyć plik nagłów- kowy.

#include <queue>

Następnie deklarujemy „zmienną typu kolejkowego” o identyfikatorze kolejka. W nawiasach trój- kątnych podajemy typ danych jaki kolejka ma przechowywać — u nas jest to typ całkowitoliczbowy int.

s t d : : queue<int> k o l e j k a ;

Przypomnijmy, że std:: jest standardową przestrzenią nazw, w której można znaleźć kolejkę (to samo będzie się tyczyło wektora). Aby korzystać z krótkiej formy zapisu (po prostu queue) należy na początku programu określić wykorzystywaną przestrzeń nazw:

using namespace s t d ;

Teraz kolejka jest gotowa do pracy. Możemy:

• dodawać element do kolejki:

k o l e j k a . push ( e l e m e n t ) ;

• odczytywać element z kolejki:

i n t e l e m e n t = k o l e j k a . f r o n t ( ) ;

(8)

• zdejmować element z kolejki:

k o l e j k a . pop ( ) ;

• sprawdzać czy kolejka jest pusta:

i f ( k o l e j k a . empty ( ) == f a l s e ) { . . . }

Do reprezentacji krawędzi w grafie wykorzystaliśmy bibliotekę std::vector. Należy o niej myśleć jak o dynamicznej tablicy. Struktura ta umożliwia dostęp do dowolnego elementu, a nie tylko do pierwszego jak w kolejce.

Wymaga ona załączenia pliku nagłówkowego

#include <v e c t o r >

Następnie możemy zainicjalizować „zmienną typu wektorowego”, która będzie przechowywała zmienne całkowitoliczbowe int

v e c t o r <int> v ;

Do reprezentacji krawędzi w grafie wykorzystaliśmy natomiast wektor, który przechowuje wektory, które z kolei przechowują liczby całkowite. Główny wektor przechowuje listy sąsiadów dla każdego z wierzchołków. Należy o tym myśleć jak o tablicy tablic.

s t d : : v e c t o r <v e c t o r <int> > s a s i e d z i ;

Na wektorze możemy wykonywać następujące operacje:

• dodawać elementy na koniec:

w e k t o r . p u s h b a c k ( e l e m e n t )

• odczytywać elementy z dowolnej pozycji

i n t a = w e k t o r . a t ( p o z y c j a )

Wektor sasiedzi zawiera wektory więc zwracany element jest wektorem i z niego dalej można odczytać elementy. Wykorzystaliśmy to przy szukaniu sąsiadów:

i n t s a s i a d = s a s i e d z i . a t ( i ) . a t ( j ) ;

Złożenie takie należy rozumieć jako dostęp do j-ego elementu w i-tym wektorze. W terminach grafowych: dostęp do j-tego sąsiada wierzchołka o numerze i.

• możemy odczytywać liczbę elementów w wektorze

i n t a = w e k t o r . s i z e ( ) ;

• wyczyścić zawartość wektora:

w e k t o r . c l e a r ( ) ;

Powyższa lista nie jest kompletna, ale na chwilę obecną powinna być wystarczająca.

4.2 Uwagi do rozwiązania w Javie

W programie skorzystaliśmy z narzędzi dostępnych w bibliotece standardowej Javy. Niektóre z nich były już wprowadzone w lekcji 10 lub 11.

Kolejka jest reprezentowana poprzez klasę ArrayDeque. Tego rodzaju „typy danych” w Javie określa się mianem „klas” i będziemy trzymać się tego terminu.

Aby z niej skorzystać wpierw musimy ją zaimportować:

import j a v a . u t i l . ArrayDeque ;

Następnie deklarowany i inicjalizowany jest obiekt tej klasy. W nawiasach trójkątnych podany jest typ jaki ma nasza kolejka przechowywać —w naszym przypadku są to zmienne całkowitoliczbowe.

ArrayDeque<I n t e g e r > k o l e j k a = new ArrayDeque<I n t e g e r > ( ) ;

Kolejka jest gotowa do pracy:

• możemy dodawać element na koniec kolejki:

k o l e j k a . a d d L a s t ( a ) ;

(9)

• możemy zdejmować elementy z początku kolejki:

i n t a = k o l e j k a . r e m o v e F i r s t ( ) ;

• możemy podejrzeć pierwszy element bez jego zdejmowania:

i n t a = k o l e j k a . g e t F i r s t

• możemy sprawdzić ile elementów liczy kolejka:

i n t a = k o l e j k a . s i z e ( )

• możemy wyczyścić całą zawartość kolejki

k o l e j k a . c l e a r ( ) ;

Skorzystaliśmy również z klasy Vector. Wpierw musimy ją zaimportować:

import j a v a . u t i l . V e c t o r ;

Teraz możemy zadeklarować i zainicjować obiekt klasy Vector:

V e c t o r<I n t e g e r > v = new V e c t o r<I n t e g e r > ( ) ;

Jak wyżej w nawiasach trójkątnych informujemy co wektor ma przechowywać. Krawędzie reprezentujemy jako wektor wektorów, które przechowują zmiene całkowitoliczbowe. Główny wektor przechowuje listy sąsiadów dla każdego z wierzchołków.

V e c t o r<V e c t o r<I n t e g e r >> k r a w e d z i e = new V e c t o r<V e c t o r<I n t e g e r > >();

Wektor oferuje dostęp do dowolnego z elementów, a nie tylko do pierwszego jak ArrayDeque. Niektóre z dostępnych operacji:

• Dodanie elementu na koniec:

k r a w e d z i e . add ( v ) ;

Wektor umożliwia również dodawanie elementów na dowolnej pozycji, ale zajmuje to nieco więcej czasu.

• Odczytanie elementu na określonej i-tej pozycji

i n t a = v . g e t ( i ) ;

Ponieważ nasza struktura to wektor wektorów więc

k r a w e d z i e . g e t ( i ) ;

zwróci obiekt klasy Vector<Integer>, z którego dalej możemy odczytywać elementy. Wykorzysta- liśmy to przy odczytywaniu krawędzi

i n t s a s i a d = k r a w e d z i e . g e t ( i ) . g e t ( j ) ;

Złożenie takie należy rozumieć jako dostęp do j-ego elementu w i-tym wektorze. W terminach grafowych: dostęp do j-tego sąsiada wierzchołka o numerze i.

• możemy odczytywać liczbę elementów w wektorze

i n t a = v . s i z e ( ) ;

Istnieją również inne operacje, ale nie będą nam na razie potrzebne.

5 Spójność grafu

5.1 Spójność grafu

Po znalezieniu okularów Pan Hilary może wreszcie udać się na mecz piłki nożnej z przyjaciółmi. Jako kapitan drużyny ma przywilej ustalenia terminu meczu. Ustalił termin na godzinę 14. Teraz musi poinformować resztę zawodników. Niestety ma numery telefonów tylko do niektórych z nich.

Pan Hilary wysyła wiadomość wszystkim graczom, do których zna numer i prosi o przekazanie infor- macji dalej. Osoba, która otrzymała wiadomość, wysyła ją do wszystkich osób, do których ma numer (z wyjątkiem tej, od której wiadomość odebrała). Czy wystarczy to do poinformowania o meczu wszystkich zawodników?

Problem daje się sprowadzić do rozważań na grafach:

(10)

Rysunek 2: Graf spójny (po lewej) i niespójny (po prawej).

• wierzchołkami w tym grafie będą zawodnicy,

• krawędź między zawodnikami oznacza, że nawzajem znają swoje numery telefonów.

Algorytm rozgłaszania wiadomości o terminie meczu jest niewielką modyfikacją przeszukiwania grafu.

Okazuje się, że wszyscy zostaną poinformowani jeżeli graf jest spójny.

Graf jest spójny, jeżeli z każdego wierzchołka da się dojść do wszystkich pozostałych. W przeciwnym wypadku graf jest określany jako niespójny, tj. jeżeli istnieją w nim wierzchołki takie, że nie da się z jednego dojść do drugiego. Grafy niespójne bywają kłopotliwe w pracy i wiele bardziej zaawansowa- nych algorytmów zakłada spójność, którą trzeba sprawdzić przed dalszymi obliczeniami. Korzystając z algorytmu przeszukiwania grafu możemy szybko sprawdzić czy graf jest spójny.

Pierwszy pomysł, to sprawdzić dla każdej pary wierzchołków, czy da się dojść z pierwszego do dru- giego. Pomysł jest skuteczny, ale niezbyt efektywny ze względu na wielokrotne (n22−n razy) uruchamianie algorytmu.

Można problem rozwiązać bardziej finezyjnie. Wystarczy „przeszukiwać” graf bez szukania konkret- nego wierzchołka, aż do wyczerpania elementów w kolejce. A po zakończeniu pętli sprawdzić, czy każdy wierzchołek został oznaczony jako odwiedzony. Jeżeli jakikolwiek pozostał nieodwiedzony to oznacza, że nie istnieje ścieżka między nim a wierzchołkiem startowym — czyli graf jest niespójny.

5.2 Zapis algorytmu w pseudokodzie

bool czyJuzOdwiedzony[n] = {false, ..., false};

int start = dowolnyWierzchołek;

kolejka = pustaKolejka();

czyJuzOdwiedzony[start] = true;

kolejka.push_back(start);

while (kolejka.empty() == false){

int w = kolejka.front();

kolejka.zdejmijPierwszyElement();

for (int v = sasiedziWezla(w)){

if (czyJuzOdwiedzony[v] == false){

czyJuzOdwiedzony[v] = true;

kolejka.push_back(v);

} // if } // for v } // while

if (czyJuzOdwiedzony == {true, ..., true}){

return "graf jest spojny";

} else {

return "graf jest niespojny";

} // if

(11)

6 Ćwiczenie — spójność grafu

Napisz program, który wczyta kolejno:

• n liczbę zawodników,

• m liczbę znanych połączeń między zawodnikami,

• m kolejnych par liczb a i b, które oznaczają, że osoby a i b znają nawzajem swoje numery telefonów.

Uwaga: zawodnicy są numerowani od 0 do n − 1 włącznie.

Następnie powinien sprawdzić czy wiadomość o meczu, wysłana przez kapitana drużyny (oznaczonego jako osoba 0) i dalej rozsyłana przez zawodników, dotrze do wszystkich zainteresowanych. Program powinien wypisać TAK gdy wszyscy zostaną poinformowani lub NIE, jeżeli przynajmniej jedna osoba nie zostanie.

Przykładowe dane:

3 2 0 1 1 2

Odpowiedź:

TAK

Przykładowe dane:

3 1 0 2

Odpowiedź:

NIE

6.1 Rozwiązanie w C++

#include <i o s t r e a m >

#include <v e c t o r >

#include <queue>

i n t n ;

s t d : : v e c t o r <s t d : : v e c t o r <int> > s a s i e d z i ; s t d : : queue<int> k o l e j k a ;

i n t main ( i n t a r g c , char ∗∗ a r g v ) { s t d : : c i n >> n ;

// tworzymy g r a f

f o r ( i n t i =0; i <n ; i ++){

s t d : : v e c t o r <int> v ; s a s i e d z i . p u s h b a c k ( v ) ;

} // f o r

i n t m;

s t d : : c i n >> m;

// w c z y t u j e m y k r a w e d z i e f o r ( i n t i =0; i <m; i ++){

i n t a , b ;

s t d : : c i n >> a >> b ;

s a s i e d z i . a t ( a ) . p u s h b a c k ( b ) ; s a s i e d z i . a t ( b ) . p u s h b a c k ( a ) ;

(12)

} // f o r

// t a b l i c a pomocnicza bool czyOdwiedzony [ n ] ; f o r ( i n t i =0; i <n ; i ++){

czyOdwiedzony [ i ] = f a l s e ;

} // f o r

// BSF

i n t s t a r t = 0 ;

czyOdwiedzony [ s t a r t ] = true ; k o l e j k a . push ( s t a r t ) ;

while ( k o l e j k a . empty ( ) == f a l s e ) { i n t w e z e l = k o l e j k a . f r o n t ( ) ; k o l e j k a . pop ( ) ;

// d l a k a z d e g o s a s i a d a

f o r ( i n t i =0; i < ( i n t ) s a s i e d z i . a t ( w e z e l ) . s i z e ( ) ; i ++){

i n t s a s i a d = s a s i e d z i . a t ( w e z e l ) . a t ( i ) ;

// j e z e l i s a s i a d n i e b y l o d w i e d z o n y t o wrzucamy go do k o l e j k i i f ( czyOdwiedzony [ s a s i a d ] == f a l s e ) {

czyOdwiedzony [ s a s i a d ] = true ; k o l e j k a . push ( s a s i a d ) ;

} // i f

} // f o r

} // w h i l e

// j e z e l i w s z y s t k i e s a o d w i e d z o n e t o g r a f j e s t s p o j n y bool o d p o w i e d z = true ;

f o r ( i n t i =0; i <n ; i ++){

i f ( czyOdwiedzony [ i ] == f a l s e ) { o d p o w i e d z = f a l s e ;

} // i f

} // f o r

s t d : : c o u t << ( o d p o w i e d z ? ”TAK\n” : ”NIE\n” ) ;

return 0 ;

} // main

6.2 Rozwiązanie w Javie

import j a v a . u t i l . S c a n n e r ; import j a v a . u t i l . V e c t o r ; import j a v a . u t i l . ArrayDeque ; public c l a s s RKI BSF Spojnosc {

public s t a t i c void main ( S t r i n g [ ] a r g s ) { // w c z y t u j e m y g r a f

S c a n n e r s = new S c a n n e r ( System . i n ) ; i n t n = s . n e x t I n t ( ) ;

i n t m =s . n e x t I n t ( ) ;

V e c t o r<V e c t o r<I n t e g e r >> k r a w e d z i e = new V e c t o r<V e c t o r<I n t e g e r > >();

f o r ( i n t i =0; i <n ; i ++){

V e c t o r<I n t e g e r > v = new V e c t o r<I n t e g e r > ( ) ; k r a w e d z i e . add ( v ) ;

} // f o r

// w c z y t u j e m y k r a w e d z i e f o r ( i n t i =0; i <m; i ++){

i n t a = s . n e x t I n t ( ) ; i n t b = s . n e x t I n t ( ) ; k r a w e d z i e . g e t ( a ) . add ( b ) ; k r a w e d z i e . g e t ( b ) . add ( a ) ;

} // f o r

// u s t a l a m y w i e r z c h o e k s t a r t o w y i n t s t a r t o w y = 0 ;

(13)

// t a b l i c a pomocnicza i k o l e j k a

boolean czyOdwiedzony [ ] = new boolean [ n ] ; f o r ( i n t i =0; i <n ; i ++){

czyOdwiedzony [ i ] = f a l s e ;

} // f o r

ArrayDeque<I n t e g e r > k o l e j k a = new ArrayDeque<I n t e g e r > ( ) ; k o l e j k a . a d d L a s t ( s t a r t o w y ) ;

czyOdwiedzony [ s t a r t o w y ] = true ;

// d o p o k i s a w k o l e j c e j a k i e s e l e m e n t y while ( k o l e j k a . s i z e ( ) > 0 ) {

// zdejmujemy w i e r z c h o l e k z k o l e j k i i n t w i e r z c h o l e k = k o l e j k a . r e m o v e F i r s t ( ) ;

// p r z e g l a d a m y k a z d e g o z j e g o s a s i a d o w

f o r ( i n t i =0; i < k r a w e d z i e . g e t ( w i e r z c h o l e k ) . s i z e ( ) ; i ++){

i n t s a s i a d = k r a w e d z i e . g e t ( w i e r z c h o l e k ) . g e t ( i ) ; i f ( czyOdwiedzony [ s a s i a d ] == f a l s e ) {

czyOdwiedzony [ s a s i a d ] = true ; k o l e j k a . add ( s a s i a d ) ;

} // i f

} // f o r

} // w h i l e

// j e z e l i j a k i s w i e r z c h o l e k p o z o s t a l n i e o d w i e d z o n y t o g r a f j e s t n i e s p o j n y boolean o d p o w i e d z = true ;

f o r ( i n t i =0; i <n ; i ++){

i f ( czyOdwiedzony [ i ] == f a l s e ) { o d p o w i e d z = f a l s e ;

} // i f

} // f o r

System . o u t . f o r m a t ( ”%s \n” , o d p o w i e d z ? ”TAK” : ”NIE” ) ;

} // main

} // c l a s s

7 Spójne składowe w grafie

Spójna składowa grafu jest podgrafem (intuicyjnie można myśleć o tym jak o fragmencie oryginalnego grafu) który:

• jest spójny,

• jest największy spośród podgrafów spójnych, tj. albo zawiera wszystkie wierzchołki i krawędzie oryginalnego, albo po dodaniu dodatkowego wierzchołka podgraf będzie niespójny.

Jeżeli graf jest spójny, to sam jest jednocześnie swoją jedyną spójną składową. Jeżeli graf nie jest spójny, to zawiera więcej niż jedną spójną składową. Rozważmy przykład na rysunku 3. Graf ma trzy spójne składowe:

• 1, 2, 3, 5, 6

• 4, 7, 8

• 9

Aby je znaleźć należy skorzystać np. z przeszukiwania grafu omawianego w tej lekcji:

1. Każdemu z wierzchołków przypisujemy −1, oznacza to, że jeszcze nie był odwiedzony; gdy wierz- chołek zostanie odwiedzony zostanie mu przypisany numer jego składowej.

2. Dopóki w grafie są wierzchołki bez przypisanego numeru powtarzamy:

• wybieramy dowolny wierzchołek, który jeszcze nie ma przypisanej składowej,

• przypisujemy mu kolejny, nie używany numer,

• startując z wybranego wierzchołka przeszukujemy graf, wszystkie odwiedzone wierzchołki do- stają ten sam numer składowej, co startowy.

W tym algorytmie przeszukiwanie grafu może być uruchamiane wielokrotnie, za każdym razem z innego wierzchołka. Stanowi zaledwie jedną, choć niezwykle ważną, śrubkę w większej machinie.

(14)

Rysunek 3: Graf oraz jego składowe spójne

8 Zadanie — spójne składowe

Napisz program który, wczyta kolejno:

• n liczbę wierzchołków w grafie, n < 10000,

• m liczbę krawędzi w grafie m < 100000,

• m par liczb a i b oddzielonych spacjami, które będą krawędziami w grafie. Krawędzie będą indek- sowane liczbami od 0 do n − 1 włącznie.

Następnie program powinien wypisać wszystkie spójne składowe grafu w następującym formacie:

• wierzchołki należące do tej samej składowej powinny być posortowane rosnąco i zgrupowane w nawiasy kwadratowe [ ].

• kolejność występowania składowych powinna być posortowana rosnąco według pierwszego elementu w danej składowej

Przykładowe dane:

3 2 0 1 1 2

Odpowiedź:

[ 0 1 2 ]

Przykładowe dane:

3 1 0 2

Odpowiedź:

[ 0 2 ] [ 1 ]

Wskazówki:

• Jedno przeszukiwanie grafu znajdzie jedną składową. Algorytm należy powtarzać startując z róż- nych punktów tak długo aż nie pokryje całego grafu.

• Ze względu na wymaganą kolejność wypisywania, jako startowy wierzchołek dobrze jest wybrać najniższy, który nie należy jeszcze do żadnej składowej.

(15)

• Wykorzystaj poznane wektory do zapamiętywania wierzchołków w danej składowej. Pamiętaj, że muszą zostać posortowane przed wypisaniem.

• Pamiętaj, że rozmiar macierzy sąsiedztwa, która przechowuje krawędzie, rośnie kwadratowo wraz z liczbą wierzchołków.

Cytaty

Powiązane dokumenty

Wykonując ćwiczenie laboratoryjne zapoznasz się z jednym z dostępnych programów do two- rzenia i przeszukiwania grafów i zastosujesz do wybranych problemów

Założenie: najefektywniejsze rozwiązanie stosu za pomocą tablicy – szczyt stosu to ostatni element wstawiony tablicy począwszy od miejsca o indeksie 0 (jeśli liczba elementów

[r]

(12 pkt)Czy graf G jest dwudzielny, hamiltonowski, eulerowski,

Jeśli graf G jest planarny, to zawiera wierzchołek stopnie niewi ekszego

Zaprojektuj efektywny algorytm sprawdzania, czy zadana rodzina posiada system różnych reprezentan- tów, a jeśli tak, to podaje jeden

Grafem zorientowanym (grafem skierowanym) nazywamy par¸e (V, E) gdzie V jest pewnym zbiorem zwanym zbiorem wierzchoÃlk´ow, natomiast E jest zbiorem pewnych par

Dla uzupełnienia więc powyższej listy problemów, które obecnie mogą być rozwiązywane za pomocą optymalnych algorytmów, w dalszej części tego para-