RKI Zajęcia 13 Przeszukiwanie grafu wszerz

Podobne dokumenty
RKI Zajęcia 14 Przeszukiwanie grafu w głąb

Grafem nazywamy strukturę G = (V, E): V zbiór węzłów lub wierzchołków, Grafy dzielimy na grafy skierowane i nieskierowane:

Przykłady grafów. Graf prosty, to graf bez pętli i bez krawędzi wielokrotnych.

a) 7 b) 19 c) 21 d) 34

Ogólne wiadomości o grafach

Struktury danych i złożoność obliczeniowa Wykład 5. Prof. dr hab. inż. Jan Magott

Algorytmiczna teoria grafów

1. Algorytmy przeszukiwania. Przeszukiwanie wszerz i w głąb.

Algorytmy grafowe. Wykład 2 Przeszukiwanie grafów. Tomasz Tyksiński CDV

8. Wektory. Przykłady Napisz program, który pobierze od użytkownika 10 liczb, a następnie wypisze je w kolejności odwrotnej niż podana.

Reprezentacje grafów nieskierowanych Reprezentacje grafów skierowanych. Wykład 2. Reprezentacja komputerowa grafów

Wstęp do programowania

Zofia Kruczkiewicz, Algorytmu i struktury danych, Wykład 14, 1

Kurs programowania. Wykład 9. Wojciech Macyna. 28 kwiecień 2016

Programowanie - wykład 4

Dynamiczny przydział pamięci w języku C. Dynamiczne struktury danych. dr inż. Jarosław Forenc. Metoda 1 (wektor N M-elementowy)

Tablice mgr Tomasz Xięski, Instytut Informatyki, Uniwersytet Śląski Katowice, 2011

Podstawy języka C++ Maciej Trzebiński. Instytut Fizyki Jądrowej Polskiej Akademii Nauk. Praktyki studenckie na LHC IVedycja,2016r.

Algorytmy i Struktury Danych.

Porównanie algorytmów wyszukiwania najkrótszych ścieżek międz. grafu. Daniel Golubiewski. 22 listopada Instytut Informatyki

Algorytmy równoległe. Rafał Walkowiak Politechnika Poznańska Studia inżynierskie Informatyka 2010

Informacje wstępne #include <nazwa> - derektywa procesora umożliwiająca włączenie do programu pliku o podanej nazwie. Typy danych: char, signed char

Wstęp do Programowania potok funkcyjny

Zasady programowania Dokumentacja

1 Podstawy c++ w pigułce.

Wykład 10 Grafy, algorytmy grafowe

Matematyczne Podstawy Informatyki

Rozwiązanie. #include <cstdlib> #include <iostream> using namespace std;

Wstęp do programowania

Matematyka dyskretna

Sortowanie topologiczne skierowanych grafów acyklicznych

Wstęp do programowania

Podstawy Programowania C++

Wstęp do programowania

Podstawy programowania skrót z wykładów:

Kurs programowania. Wykład 9. Wojciech Macyna

Programowanie Obiektowo Zorientowane w języku c++ Przestrzenie nazw

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Pytania sprawdzające wiedzę z programowania C++

. Podstawy Programowania 2. Grafy i ich reprezentacje. Arkadiusz Chrobot. 9 czerwca 2016

Zajęcia nr 5 Algorytmy i wskaźniki. dr inż. Łukasz Graczykowski mgr inż. Leszek Kosarzewski Wydział Fizyki Politechniki Warszawskiej

Wstęp do Informatyki zadania ze złożoności obliczeniowej z rozwiązaniami

Matematyczne Podstawy Informatyki

Pętle i tablice. Spotkanie 3. Pętle: for, while, do while. Tablice. Przykłady

Wstęp do informatyki- wykład 7

Podstawy Programowania 2 Grafy i ich reprezentacje. Plan. Wstęp. Teoria grafów Graf skierowany. Notatki. Notatki. Notatki. Notatki.

Podstawowe własności grafów. Wykład 3. Własności grafów

Programowanie i struktury danych

Pole wielokąta. Wejście. Wyjście. Przykład

4. Funkcje. Przykłady

AiSD zadanie trzecie

Struktury Struktura polami struct struct struct struct

Podstawy programowania. Wykład: 9. Łańcuchy znaków. dr Artur Bartoszewski -Podstawy programowania, sem 1 - WYKŁAD

Podstawy programowania, Poniedziałek , 8-10 Projekt, część 1

2. Tablice. Tablice jednowymiarowe - wektory. Algorytmy i Struktury Danych

Zadania do wykonania. Rozwiązując poniższe zadania użyj pętlę for.

Zajęcia nr 2 Programowanie strukturalne. dr inż. Łukasz Graczykowski mgr inż. Leszek Kosarzewski Wydział Fizyki Politechniki Warszawskiej

Matematyka dyskretna - 7.Drzewa

. Podstawy Programowania 2. Algorytmy dfs i bfs. Arkadiusz Chrobot. 2 czerwca 2019

Instrukcje dla zawodników

Digraf o V wierzchołkach posiada V 2 krawędzi, zatem liczba różnych digrafów o V wierzchołkach wynosi 2 VxV

Wykład 7. Algorytmy grafowe

Algorytm DFS Wprowadzenie teoretyczne. Algorytm DFS Wprowadzenie teoretyczne. Algorytm DFS Animacja. Algorytm DFS Animacja. Notatki. Notatki.

Podstawy algorytmiki i programowania - wykład 4 C-struktury

Znajdowanie wyjścia z labiryntu

Wydział Matematyki I Informatyki ul. Słoneczna Olsztyn

Podstawy Programowania

Algorytm Dijkstry znajdowania najkrótszej ścieżki w grafie

Kurs programowania. Wykład 1. Wojciech Macyna. 3 marca 2016

Algorytmy Równoległe i Rozproszone Część V - Model PRAM II

Prof. Danuta Makowiec Instytut Fizyki Teoretycznej i Astrofizyki pok. 353, tel danuta.makowiec at gmail.com

Algorytmy i złożoności. Wykład 3. Listy jednokierunkowe

1 Pierwsze kroki w C++ cz.3 2 Obsługa plików

Podstawy programowania 2. Temat: Drzewa binarne. Przygotował: mgr inż. Tomasz Michno

C-struktury wykład. Dorota Pylak

do instrukcja while (wyrażenie);

Programowanie obiektowe

utworz tworzącą w pamięci dynamicznej tablicę dwuwymiarową liczb rzeczywistych, a następnie zerującą jej wszystkie elementy,

Kontrola przebiegu programu

Wstęp do programowania. Zastosowania stosów i kolejek. Piotr Chrząstowski-Wachtel

Opis zagadnieo 1-3. Iteracja, rekurencja i ich realizacja

Podstawy języka C++ Maciej Trzebiński. Praktyki studenckie na LHC IFJ PAN. Instytut Fizyki Jądrowej Polskiej Akademii Nauk. M. Trzebiński C++ 1/16

tablica: dane_liczbowe

Konstrukcje warunkowe Pętle

Kurs programowania. Wykład 3. Wojciech Macyna. 22 marca 2019

Wstęp do Programowania potok funkcyjny

Matematyka dyskretna. Andrzej Łachwa, UJ, /14

Wstęp do sieci neuronowych, wykład 11 Łańcuchy Markova

dr inż. Paweł Myszkowski Wykład nr 11 ( )

dodatkowe operacje dla kopca binarnego: typu min oraz typu max:

Teoria grafów dla małolatów. Andrzej Przemysław Urbański Instytut Informatyki Politechnika Poznańska

Podstawy i języki programowania

Część 4 życie programu

Programowanie obiektowe

Dariusz Brzeziński. Politechnika Poznańska, Instytut Informatyki

Zadanie 1 Przygotuj algorytm programu - sortowanie przez wstawianie.

Dla każdej operacji łącznie tworzenia danych i zapisu ich do pliku przeprowadzić pomiar czasu wykonania polecenia. Wyniki przedstawić w tabelce.

Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych. Ćwiczenie 3 stos Laboratorium Metod i Języków Programowania

Informatyka II. Laboratorium.

Transkrypt:

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ć zaró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, 1

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 2

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 startowy 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 szukanym 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"; 3

} 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 wiersza 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(n 2 ). 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 elementó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(n 2 ). 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. 4

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 <iostream > #include <vector > #include <queue> int n ; std : : vector <vector <int> > s a s i e d z i ; std : : queue<int> k o l e j k a ; void wyczysc ( ) ; int main ( int argc, char argv ) { std : : c i n >> n ; // tworzymy p u s t e l i s t y sasiadow vector <int> v ; s a s i e d z i. push back ( v ) ; int m; std : : c i n >> m; // wczytujemy krawedzie for ( int i =0; i <m; i ++){ int a, b ; std : : c i n >> a >> b ; s a s i e d z i. at ( a ). push back ( b ) ; s a s i e d z i. at ( b ). push back ( a ) ; int szukany ; std : : c i n >> szukany ; int s t a r t ; std : : c i n >> s t a r t ; // t a b l i c a pomocnicza bool czyodwiedzony [ n ] ; czyodwiedzony [ i ] = f a l s e ; // algorytm BSF // dodajemy do k o l e j k i s t a r t o w y element czyodwiedzony [ s t a r t ] = true ; k o l e j k a. push ( s t a r t ) ; // dopoki w k o l e j c e s j a k i e k o l w i e k elementy while ( k o l e j k a. empty ( ) == f a l s e ){ // zdejmujemy element z k o l e j k i int wezel = k o l e j k a. f r o n t ( ) ; k o l e j k a. pop ( ) ; 5

// j e z e l i j e s t to ten szukany... i f ( wezel == szukany ){ std : : cout << TAK\n ; wyczysc ( ) ; return 0 ; // d l a kazdego s a s i a d a for ( int i =0; i < s a s i e d z i. at ( wezel ). s i z e ( ) ; i ++){ int s a s i a d = s a s i e d z i. at ( wezel ). at ( i ) ; // j e z e l i s a s i a d nie b y l odwiedzony to 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 ) ; } // w h i l e std : : cout << NIE\n ; wyczysc ( ) ; return 0 ; } // main ( ) void wyczysc ( ) { // c z y s z c z e n i e g r a f u for ( int i =0; i < s a s i e d z i. s i z e ( ) ; i ++){ s a s i e d z i. at ( i ). c l e a 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 } // wyczysc ( ) 3.2 Rozwiązanie w Javie import java. u t i l. Scanner ; import java. u t i l. Vector ; import java. u t i l. ArrayDeque ; public class RKI BSF { public s t a t i c void main ( S t r i n g [ ] a r g s ) { Scanner s = new Scanner ( System. i n ) ; // wczytujemy i l o s c wezlow i krawedzi int n = s. n e x t I n t ( ) ; int m = s. n e x t I n t ( ) ; Vector<Vector<I n t e g e r >> krawedzie = new Vector<Vector<I n t e g e r >>(); // tworzymy p u s t e l i s t y sasiadow Vector<I n t e g e r > v = new Vector<I n t e g e r >(); krawedzie. add ( v ) ; // dodajemy sasiadow for ( int i =0; i <m; i ++){ int a = s. n e x t I n t ( ) ; int b = s. n e x t I n t ( ) ; krawedzie. get ( a ). add ( b ) ; krawedzie. get ( b ). add ( a ) ; int szukany = s. n e x t I n t ( ) ; int startowy = s. n e x t I n t ( ) ; // algorytm 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 ] ; 6

czyodwiedzony [ i ] = f a l s e ; // 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. addlast ( startowy ) ; czyodwiedzony [ startowy ] = true ; boolean odpowiedz = f a l s e ; // dopoki 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 element z k o l e j k i int w i e r z c h o l e k = k o l e j k a. removefirst ( ) ; // j e s l i to ten szukany... i f ( w i e r z c h o l e k == szukany ){ odpowiedz = true ; k o l e j k a. c l e a r ( ) ; break ; // dodajemy sasiadow do k o l e j k i for ( int i =0; i < krawedzie. get ( 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 nie b y l odwiedzony // to wrzucamy go do k o l e j k i int s a s i a d = krawedzie. get ( w i e r z c h o l e k ). get ( 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 ) ; } // w h i l e System. out. format ( %s \n, odpowiedz? 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łówkowy. #include <queue> Następnie deklarujemy zmienną typu kolejkowego o identyfikatorze kolejka. W nawiasach trójkątnych podajemy typ danych jaki kolejka ma przechowywać u nas jest to typ całkowitoliczbowy int. std : : 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 std ; Teraz kolejka jest gotowa do pracy. Możemy: dodawać element do kolejki: k o l e j k a. push ( element ) ; odczytywać element z kolejki: int element = k o l e j k a. f r o n t ( ) ; 7

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 <vector > Następnie możemy zainicjalizować zmienną typu wektorowego, która będzie przechowywała zmienne całkowitoliczbowe int vector <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. std : : vector <vector <int> > s a s i e d z i ; Na wektorze możemy wykonywać następujące operacje: dodawać elementy na koniec: wektor. push back ( element ) odczytywać elementy z dowolnej pozycji int a = wektor. at ( pozycja ) 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: int s a s i a d = s a s i e d z i. at ( i ). at ( j ) ; Złożenie takie należy rozumieć jako dostęp do j-ego elementu w i-tym wektorze. grafowych: dostęp do j-tego sąsiada wierzchołka o numerze i. możemy odczytywać liczbę elementów w wektorze int a = wektor. s i z e ( ) ; W terminach wyczyścić zawartość wektora: wektor. 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 java. 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. addlast ( a ) ; 8

możemy zdejmować elementy z początku kolejki: int a = k o l e j k a. removefirst ( ) ; możemy podejrzeć pierwszy element bez jego zdejmowania: int a = k o l e j k a. g e t F i r s t możemy sprawdzić ile elementów liczy kolejka: int 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 java. u t i l. Vector ; Teraz możemy zadeklarować i zainicjować obiekt klasy Vector: Vector<I n t e g e r > v = new Vector<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. Vector<Vector<I n t e g e r >> krawedzie = new Vector<Vector<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: krawedzie. 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 int a = v. get ( i ) ; Ponieważ nasza struktura to wektor wektorów więc krawedzie. get ( i ) ; zwróci obiekt klasy Vector<Integer>, z którego dalej możemy odczytywać elementy. Wykorzystaliśmy to przy odczytywaniu krawędzi int s a s i a d = krawedzie. get ( i ). get ( j ) ; Złożenie takie należy rozumieć jako dostęp do j-ego elementu w i-tym wektorze. grafowych: dostęp do j-tego sąsiada wierzchołka o numerze i. możemy odczytywać liczbę elementów w wektorze int a = v. s i z e ( ) ; W terminach 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 informacji 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: 9

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 zaawansowanych 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 drugiego. Pomysł jest skuteczny, ale niezbyt efektywny ze względu na wielokrotne ( n2 n 2 razy) uruchamianie algorytmu. Można problem rozwiązać bardziej finezyjnie. Wystarczy przeszukiwać graf bez szukania konkretnego 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 10

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 TAK 3 1 0 2 NIE Odpowiedź: Przykładowe dane: Odpowiedź: 6.1 Rozwiązanie w C++ #include <iostream > #include <vector > #include <queue> int n ; std : : vector <std : : vector <int> > s a s i e d z i ; std : : queue<int> k o l e j k a ; int main ( int argc, char argv ) { std : : c i n >> n ; // tworzymy g r a f std : : vector <int> v ; s a s i e d z i. push back ( v ) ; int m; std : : c i n >> m; // wczytujemy krawedzie for ( int i =0; i <m; i ++){ int a, b ; std : : c i n >> a >> b ; s a s i e d z i. at ( a ). push back ( b ) ; s a s i e d z i. at ( b ). push back ( a ) ; 11

// t a b l i c a pomocnicza bool czyodwiedzony [ n ] ; czyodwiedzony [ i ] = f a l s e ; // BSF int 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 ){ int wezel = k o l e j k a. f r o n t ( ) ; k o l e j k a. pop ( ) ; // d l a kazdego s a s i a d a for ( int i =0; i < ( int ) s a s i e d z i. at ( wezel ). s i z e ( ) ; i ++){ int s a s i a d = s a s i e d z i. at ( wezel ). at ( i ) ; // j e z e l i s a s i a d nie b y l odwiedzony to 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 ) ; } // w h i l e // j e z e l i w s z y s t k i e sa odwiedzone to g r a f j e s t spojny bool odpowiedz = true ; i f ( czyodwiedzony [ i ] == f a l s e ){ odpowiedz = f a l s e ; std : : cout << ( odpowiedz? TAK\n : NIE\n ) ; return 0 ; } // main 6.2 Rozwiązanie w Javie import java. u t i l. Scanner ; import java. u t i l. Vector ; import java. u t i l. ArrayDeque ; public class RKI BSF Spojnosc { public s t a t i c void main ( S t r i n g [ ] a r g s ) { // wczytujemy g r a f Scanner s = new Scanner ( System. i n ) ; int n = s. n e x t I n t ( ) ; int m =s. n e x t I n t ( ) ; Vector<Vector<I n t e g e r >> krawedzie = new Vector<Vector<I n t e g e r >>(); Vector<I n t e g e r > v = new Vector<I n t e g e r >(); krawedzie. add ( v ) ; // wczytujemy krawedzie for ( int i =0; i <m; i ++){ int a = s. n e x t I n t ( ) ; int b = s. n e x t I n t ( ) ; krawedzie. get ( a ). add ( b ) ; krawedzie. get ( b ). add ( a ) ; // ustalamy w i e r z c h o e k s t a r t o w y int startowy = 0 ; 12

// t a b l i c a pomocnicza i k o l e j k a boolean czyodwiedzony [ ] = new boolean [ n ] ; czyodwiedzony [ i ] = f a l s e ; 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. addlast ( startowy ) ; czyodwiedzony [ startowy ] = true ; // dopoki sa w k o l e j c e j a k i e s elementy 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 int w i e r z c h o l e k = k o l e j k a. removefirst ( ) ; // przegladamy kazdego z j e g o sasiadow for ( int i =0; i < krawedzie. get ( w i e r z c h o l e k ). s i z e ( ) ; i ++){ int s a s i a d = krawedzie. get ( w i e r z c h o l e k ). get ( 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 ) ; } // 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 nieodwiedzony to g r a f j e s t n i e s p o j n y boolean odpowiedz = true ; i f ( czyodwiedzony [ i ] == f a l s e ){ odpowiedz = f a l s e ; System. out. format ( %s \n, odpowiedz? 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 wierzchoł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 dostają 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. 13

8 Zadanie spójne składowe Napisz program który, wczyta kolejno: 3 2 0 1 1 2 n liczbę wierzchołków w grafie, n < 10000, m liczbę krawędzi w grafie m < 100000, Rysunek 3: Graf oraz jego składowe spójne m par liczb a i b oddzielonych spacjami, które będą krawędziami w grafie. Krawędzie będą indeksowane 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: Odpowiedź: [ 0 1 2 ] 3 1 0 2 Przykładowe dane: 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. 14

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. 15