Matematyka Dyskretna Andrzej Szepietowski 25 marca 2004 roku
Rozdział 1 Stosy, kolejki i drzewa 1.1 Listy Lista to uporządkowany ciąg elementów. Przykładami list są tablice jednowymiarowe. W tablicach mamy dostęp do dowolnego elementu, poprzez podanie indeksu tego elementu. Przykład 1.1 W języku Pascal przykładem typu tablicy jednowymiarowej jest array[1..n] of integer. Jeżeli mamy zmienną tego typu a:array[1..n] of integer, to tablica a zawiera N elementów a[1], a[2],...,a[n]. W programie możemy odwoływać się do całej tablicy, na przykład w instrukcji przypisania a:=b, lub do pojedynczych elementów: a[i]:=a[i+1]. Możemy także używać tablic dwu lub więcej wymiarowych. Przykładem tablicy dwuwymiarowej jest typ array[1..n,1..m] of real. Zmienna c:array[1..n,1..m] of real zawiera N M elementów. Dla każdej pary liczb i, j spełniajacej warunki 1 i N, 1 j M, element c[i,j] zawiera liczbę typu real. 1.2 Stosy i kolejki Czasami wygodniej posługiwać się listami bez używania indeksów. Przykładami list, których można używać bez konieczności odwoływania się do indeksów poszczególnych ele- 3
4 Rozdział 1. Stosy, kolejki i drzewa mentów, są kolejki i stosy. Definicja 1.2 Kolejka jest lista z trzema operacjami: dodawania nowego elementu na koniec kolejki, zdejmowania pierwszego elementu z poczatku kolejki, sprawdzania, czy kolejka jest pusta. Taki sposób dodawania i odejmowania elementów jest określany angielskim skrótem FIFO (first in first out, czyli pierwszy, który wszedł, pierwszy wyjdzie). Przykłady kolejek spotykamy w sklepach, gdzie klienci czekający na obsłużenie tworzą kolejki. Definicja 1.3 Stos jest lista z trzema operacjami: dodawania elementu na wierzch stosu, zdejmowania elementu z wierzchu stosu, sprawdzania, czy stos jest pusty. Na stosie dodajemy i odejmujemy elementy z tego samego końca, podobnie jak w stosie talerzy spiętrzonym na stole. Talerze dokładane są na wierzch stosu i zdejmowane z wierzchu stosu. Taka organizacja obsługi listy określana jest angielskim skrótem LIFO (last in first out, czyli ostatni, który wszedł, pierwszy wyjdzie). Niektórzy w ten sposób organizują pracę na biurku. Przychodzące listy układają na stosie i jak mają czas, to zdejmują jeden list i odpowiadają na niego. Przyjrzyjmy się zastosowaniu kolejki lub stosu do szukania. Przypuśćmy, że szukamy przez telefon pewnej informacji (na przykład chcielibyśmy się dowiedzieć, kto z naszych znajomych ma pewną książkę). Algorytm szukania ksiażki wśród znajomych tworzymy STOS, który na początku jest pusty, wkładamy na STOS numery telefonów swoich znajomych, dopóki na stosie są jakieś numery, powtarzamy: zdejmujemy z wierzchu STOSU jeden numer telefonu, dzwonimy pod ten numer, jeżeli osoba, do której się dodzwoniliśmy, posiada szukaną książkę, to koniec poszukiwań, w przeciwnym przypadku pytamy ją o znajomych, którzy mogą mieć książkę numery tych znajomych są dopisywani na STOS. W powyższym algorytmie zamiast stosu może być użyta kolejka.
1.3 Implementacja stosu 1.3. Implementacja stosu 5 W wielu językach programowania stosy i kolejki nie występują jako standardowe struktury danych. W tym i następnym podrozdziale pokażemy jak można je utworzyć za pomocą tablic. Tworzymy tablicę ST OS[0..max] oraz zmienna W ierzchstosu. W tablicy przechowujemy elementy stosu. Element ST OS[0] leży na dnie stosu, a kolejne elementy nad nim. Zmienna W ierzchstosu wskazuje na pierwsze wolne miejsce w tablicy ST OS. W pustym stosie W ierzchstosu = 0. Operacja włożenia nowego elementu x na ST OS implementujemy za pomocą instrukcji: ST OS[W ierzchstosu] := x; W ierzchstosu := W ierzchstosu + 1; Tak skonstruowany stos może pomieścić co najwyżej max + 1 elementów. Jeżeli W ierzchstosu = max + 1, to stos jest pełny i nie można na niego wkładać nowych elementów. Operacja zdejmowania elementu z wierzchu ST OSU implementujemy za pomocą instrukcji: W ierzchstosu := W ierzchstosu 1; x := ST OS[W ierzchstosu]; Operację tą można wykonać, jeżeli stos nie jest pusty, to znaczy jeżeli W ierzchstosu > 0. 1.4 Implementacja kolejki Kolejkę też można utworzyć za pomocą tablicy. W tym celu deklarujemy tablicę KOLEJKA[0...max] oraz dwie zmienne P oczatekkolejki, KoniecKolejki. W tablicy przechowujemy elementy kolejki. Zmienna P oczatekkolejki wskazuje na pierwsze element kolejki, a zmienna KonieckKolejki na pierwsze wolne miejsce za kolejką. Kolejka jest pusta, jeżeli P oczatekkolejki = KonieckKolejki. Operacja włożenia nowego elementu x do KOLEJKI implementujemy za pomocą dwóch instrukcji: KOLEJKA[KoniecKolejki] := x; KoniecKolejki := KoniecKolejki + 1; Operacja zdejmowania elementu z końca KOLEJKI implementujemy za pomocą instrukcji: x := KOLEJKA[P oczatekkolejki]; P oczatekkolejki := P oczatekkolejki + 1;
6 Rozdział 1. Stosy, kolejki i drzewa Ta operacja może być wykonana, jeżeli kolejka nie jest pusta, to znaczy jeżeli P oczatekkolejki KoniecKolejki; Aby lepiej wykorzystać tablicę KOLEJKA, należy zapełniać ją cyklicznie. To znaczy po dojściu do końca (miejsca max 1) dalsze elementy wstawiamy na początek (jeżeli zwolniły się tam miejsca po zdjęciu elementów). Trzeba tylko zadbać, żeby za ostatni elementem w kolejce było przynajmniej jedno wolne miejsce. Operacje na kolejce przedstawiają się teraz następująco: Jeżeli (KoniecKolejki+1) mod (max + 1) = P oczatekkolejki, to KOLEJKA jest pełna. Jeżeli KoniecKolejki = P oczatekkolejki, to KOLEJKA jest pusta. Wstawianie nowego elementu x na koniec kolejki implementujemy za pomocą instrukcji: KOLEJKA[KoniecKolejki] := x; KoniecKolejki := (KoniecKolejki + 1) mod (max + 1); Zdejmowanie ostatniego elementu x z początku kolejki implementujemy za pomocą instrukcji: x := KOLEJKA[P oczatekkolejki]; P oczatekkolejki := (P oczatekkolejki + 1) mod (max + 1); 1.5 Drzewa ukorzenione Przypomnijmy, że drzewo ukorzenione to drzewo z wyróżnionym jednym wierzchołkiem, korzeniem. Każdy wierzchołek v różny od korzenia jest z nim połączony dokładnie jedną drogą prostą. Sąsiad leżący na drodze do korzenia nazywamy ojcem wierzchołka v. Pozastali sąsiedzi są synami wierzchołka v. Korzeń drzewa nie ma ojca, wszyscy jego sąsiedzi są jego synami. Wierzchołek, który nie posiada syna nazywamy liściem. 1.6 Drzewa binarne Drzewa binarne to takie drzewa ukorzenione, w których każdy wierzchołek ma co najwyżej dwóch synów. Do oznaczania wierzchołków w drzewie binarnym wygodnie jest używać ciągów zer i jedynek, czyli elementów zbioru {0, 1}. Wierzchołki drzewa oznaczamy w następujący sposób: korzeń drzewa oznaczamy przez λ pusty ciąg, jeżeli jakiś wierzchołek jest oznaczony przez x, to jego synowie oznaczeni są przez x0 i x1.
1.6. Drzewa binarne 7 Rysunek 1.1: Przykład drzewa binarnego λ 0 1 00 01 10 11 110 111 Przy takim oznaczeniu wierzchołków drzewa binarnego nazwa wierzchołka x mówi nam, jaka ścieżka prowadzi od korzenia do x. Na przykład, aby dojść od korzenia do wierzchołka 110 nalezy: pójść w prawo do 1, potem znowu w prawo do 11, a na końcu w lewo do 110. Jeżeli mamy drzewo binarne T, to z każdym wierzchołkiem x możemy skojarzyć poddrzewo T x złożone z wierzchołka x i wszystkich jego potomków. Na przykład w drzewie przedstawionym na rysunku 1.1 wierzchołek x = 1 wyznacza poddrzewo T x przedstawione na rysunku 1.2. Mówimy też, że drzewo T x składa się z korzenia Rysunek 1.2: Poddrzewo T 1 1 10 11 110 111 (wierzchołka x), z lewego poddrzewa T x0 i z prawego poddrzewa T x1. Wysokością drzewa nazywamy długość (liczbę krawędzi) najdłuższej ścieżki w drzewie prowadzącej od korzenia do liścia. Na przykład drzewo z rysunku 1.1 jest wysokości 3.
8 Rozdział 1. Stosy, kolejki i drzewa 1.7 Drzewa wyrażeń arytmetycznych Przykładem zastosowania drzew binarnych są drzewa wyrażeń arytmetycznych. Najpierw przykład. Na rysunku 1.3 przedstawiono drzewo wyrażenia 2a+3/d. W drzewie tym każdy wierzchołek ma etykietę. Liście etykietowane są stałymi albo zmiennymi. Wierzchołki nie będące liśćmi etykietowane są operacjami arytmetyczymi. Każdemu wierzchołkowi w drzewie możemy przypisać wyrażenie arytmetyczne według następującej zasady: dla liści wyrażeniami są etykiety tych liści (stałe lub zmienne), jeżeli wierzchołek x ma etykietę op, a jego synom przypisano wyrażenia W (x0) i W (x1), to wierzchołkowi x przypisujemy wyrażenie W (x) = (W (x0) op W (x1)). Rysunek 1.3: Drzewo wyrażenia 2a + 3/d + / 2 a 3 d Przykład 1.4 W drzewie z rysunku1.3 wierzchołkowi z etykieta odpowiada wyrażenie 2 a, wierzchołkowi z etykieta / wyrażenie 3/d, a korzeniowi wyrażenie ((2 a) + (3/d)). Wyrażenie to zawiera więcej nawiasów, niż to się zwykle stosuje. Normalnie to samo wyrażenie przedstawiamy bez nawiasów w postaci 2a + 3/d. Opuszczenie nawiasów może prowadzić do niejednoznaczności lub może zmienić sens wyrażenia. Na przykład wyrażenie 2(a + 3/d) po opuszczeniu nawiasów stanie się identyczne z wyrażeniem 2a + 3/d i zmieni sens. Drzewo, które odpowiada wyrażeniu 2(a + 3/d), przedstawiono na rysunku 1.4. Drzewo wyrażenia arytmetycznego oddaje logiczną strukturę i sposób obliczania tego wyrażenia.
1.7. Drzewa wyrażeń arytmetycznych 9 Rysunek 1.4: Drzewo wyrażenia 2(a + 3/d) 2 + a / 3 d Istnieje sposób przedstawiania wyrażeń arytmetycznych nie wymagający nawiasów. Jest to tak zwana notacja polska lub Łukasiewicza. Jest ona też nazywana notacją postfixową, ponieważ znak operacji stoi na końcu wyrażenia, za argumentami, czyli wyrażenie w notacji postfixowej ma postać: pierwszy argument drugi argument operacja. Notacja, do jakiej jesteśmy przyzwyczajeni, nazywa się infixowa, ponieważ operacja znajduje się pomiędzy argumentami, czyli wyrażenie w notacji infixowej ma postać: pierwszy argument operacja drugi argument. Przykład 1.5 Wyrażenie w postaci postfixowej 2, a+ ma w postaci infixowej postać a wyrażenie 2 + a, 2, a 3, d/+ jest postfixową postacią wyrażenia 2a + 3/d. W wyrażeniach w postaci postfixowej nie potrzeba nawiasów. Wartość wyrażenia można w sposób jednoznaczny odtworzyć z samego wyrażenia za pomocą następującego algorytmu.:
10 Rozdział 1. Stosy, kolejki i drzewa Algorytm obliczania wartości wyrażenia w postaci postfixowej. Dla kolejnych elementów zapisu wyrażenia: jeżeli element jest stałą lub zmienną, to wkładamy jego wartość na stos, jeżeli element jest znakiem operacji, to: zdejmujemy dwie wartości z wierzchu stosu, wykonujemy operację na tych wartościach, obliczoną wartość wkładamy na wierzch stosu. Po przejściu całego wyrażenia jego wartość znajduje się na stosie. Przykład 1.6 Zademonstrujmy ten algorytm na przykładzie wyrażenia: abc + de/+ Załóżmy, że zmienne maja następujące wartości: a = 3, b = 2, c = 1, d = 4, e = 2. Poniższa tabela przedstawia zawartość stosu po przeczytaniu kolejnych elementów wyrażenia. czytany element stos a 3, b 3, 2, c 3, 2, 1, + 3, 3, 9, d 9, 4, e 9, 4, 2, / 9, 2, + 11. 1.8 Przeszukiwanie drzew binarnych Zajmiemy się teraz dwoma algorytmami przeszukiwania drzew (binarnych): przeszukiwanie w głąb i wszerz. Różnią się one rodzajem użytych struktur danych. W algorytmie przeszukiwania w głąb użyjemy stosu, a w algorytmie przeszukiwania wszerz użyjemy kolejki. 1.8.1 Przeszukiwanie drzewa w głab Algorytm przeszukiwania drzewa w głab. Dane wejściowe: drzewo T. Odwiedzamy korzeń λ i wkładamy go na STOS; dopóki STOS nie jest pusty, powtarzamy: Niech v będzie wierzchołkiem na wierzchu STOSU,
1.8. Przeszukiwanie drzew binarnych 11 sprawdzamy, czy istnieje syn u wierzchołka v, który nie był jeszcze odwiedzony, jeżeli takie u się znajdzie, to odwiedzamy u i wkładamy go na wierzch STOSU, jeżeli takiego u nie ma, to zdejmujemy v ze STOSU; cofamy się do wierzchołka będącego na stosie pod spodem. Algorytm powinien dodatkowo zapamiętywać, jaki wierzchołek był ostatnio zdjęty ze stosu. Pozwala to stwierdzić, który z synów wierzchołka znajdującego się na stosie pod spodem należy teraz rozpatrzeć. Przykład 1.7 Poniższa tabela pokazuje jaki wierzchołek jest odwiedzany i jaka jest zawartość stosu po każdej kolejnej iteracji pętli algorytmu, gdy przeszukiwane jest drzewo z rysunku 1.1. Wierzchołek STOS λ λ 0 λ,0 00 λ,0,00 0 λ,0 01 λ,0,01 0 λ,0 λ λ 1 λ,1 10 λ,1,10 1 λ,1 11 λ,1,11 110 λ,1,11,110 11 λ,1,11 111 λ,1,11,111 11 λ,1,11 1 λ,1 λ λ W metodzie przeszukiwania w głąb po każdym kroku algorytmu wierzchołki znajdujące się na stosie tworzą ścieżkę od wierzchołka wejściowego do wierzchołka aktualnie odwiedzanego. 1.8.2 Przeszukiwanie drzewa wszerz Następny algorytm przeszukiwania drzew używa kolejki jako pomocniczej struktury danych. Algorytm przeszukiwania wszerz. Dane wejściowe: drzewo T.
12 Rozdział 1. Stosy, kolejki i drzewa Odwiedzamy korzeń drzewa λ i wkładamy go do KOLEJKI. Dopóki KOLEJKA nie jest pusta, powtarzamy: bierzemy jeden wierzchołek v z początku KOLEJKI, odwiedzamy wszystkich synów wierzchołka v każdego wkładamy na koniec kolejki. Poniżej przedstawiono odwiedzane wierzchołki oraz zawartość kolejki po każdej kolejnej iteracji pętli algorytmu przeszukiwania wszerz drzewa przedstawionego na rysunku 1.1. wierzchołki KOLEJKA λ λ 0,1 0,1 00,01 1,00,01 10,11 00,01,10,11-01,10,11-10,11-11 110,111 110,111-111 - - W metodzie przeszukiwania wszerz wierzchołki są przeszukiwane w kolejności od wierzchołków będących najbliżej wierzchołka początkowego do wierzchołków będących dalej. 1.9 Drzewa decyzyjne Rysunek 1.5 przedstawia drzewo decyzyjne dla pewnego sposobu posortowania trzech elementów a, b, c, które pochodzą z jakiegoś uporządkowanego zbioru. W korzeniu drzewa porównujemy elementy a i b. Jeżeli a < b, to idziemy do następnego wierzchołka w lewo wzdłuż krawędzi z etykietą <. Jeżeli a > b, to idziemy w prawo wzdłuż krawędzi z etykietą >. Podobnie postępujemy w innych wierzchołkach wewnętrznych. Liście drzewa odpowiadają ostatecznym decyzjom i mówią jak należy uporządkować elementy a, b i c, aby tworzyły one ciąg rosnący. Drzewo z rysunku 1.5 opisuje pewien sposób postępowania. Jak widać ten sposób wymaga maksymalnie trzech porównań. Można zadać pytanie, czy istnieje inny schemat postępowania wymagający mniej porównań. Aby odpowiedzieć na to pytanie, przypuśćmy, że na zmienne a, b, c podstawiamy wartości kolejnych permutacji π zbioru {1, 2, 3}. Zauważmy, że każda permutacja π powinna prowadzić do innego liścia. Rzeczywiście, przypuśćmy bowiem, że mamy dwie różne permutacje π i τ, które prowadzą do tego samego liścia. Ponieważ prowadzi je do tego samego liścia, to będą one tak samo posortowane. Ale z drugiej strony, ponieważ są one różne więc istnieją 1 i < j 3 takie, że π(i) < π(j) i τ(i) > τ(j). Z czego wynika, że nie powinny one być tak samo posortowane. Tak więc drzewo dowolnego schematu sortującego trzy elementy musi mieć 6 = 3! liści i głębokość co najmniej 3. Drzewo decyzyjne z rysunku 1.6 przedstawia schemat postępowania w następującym problemie. Mamy trzy monety a, b, c. Jedna z nich jest fałszywa, to znaczy trochę cięższa
1.9. Drzewa decyzyjne 13 Rysunek 1.5: Drzewo decyzyjne a?b < > b?c a?c < > < > abc a?c bac b?c < > < > acb cab bca cba Rysunek 1.6: Drzewo decyzyjne a?b < = > a?c a?c a?c < = < > = > a b+ c+ c b a+
14 Rozdział 1. Stosy, kolejki i drzewa lub trochę lżejsza od dwóch pozostałych. Schemat pokazuje jak za pomocą wagi szalkowej bez odważników znaleźć fałszywą monetę. W korzeniu na lewej szalce kładziemy monetą a, na prawej b. Jeżeli a okaże się lżejsz od b, to idziemy w dół wzdłuż krawędzi z etykietą <. Jeżeli a jest ciższa od b, idziemy wzdłuż krawędzi >, a jeżeli szalki będą w równowadze, to idziemy wzdłuż krawędzi =. W innych wewnętrznych wierzchołkach postępujemy podobnie. W liściach mamy ostateczne decyzje, która moneta jest fałszywa, oraz czy jest ona cięższa (znak +), czy lżejsza (znak ). 1.10 Drzewo gry Rozważmy następującą grę. Mamy trzy kupki kamyków. Gracze po kolei zabierają kamienie z kupek, przy czym wolno brać kamienie tylko z jednej kupki i oczywiście jakieś kamienie trzeba zabrać. Gra ta jest podobna do gry rozważanej w rozdziale o funkcjach boolowskich, tylko inna jest zasada, kto wygrywa. Teraz przegrywa gracz, który zabiera ostatnie kamienie. Rysunek 1.7: Drzewo gry 1(2, 1) 2(1, 1) 2(2, 0) 2(0, 1) 1(1, 0) 1(1, 0) 1(0, 0) 1(0, 0) 2(0, 0) 2(0, 0) Rysunek 1.7 przedstawia drzewo dla tej gry. Dla uproszczenia przyjęto, że są tylko dwie kupki, w jednej są dwa kamienie, a w drugiej jeden. Wierzchołki drzewa posiadają etykiety postaci x(y, z); gdzie x oznacza, czyja jest kolej na wykonanie ruchu, pierwszego czy drugiego gracza; y i z oznaczają liczby kamieni w kupkach. W korzeniu pierwszy gracz ma ruch, na kupkach są 2 i 1 kamień. Trzej synowie korzenia odpowiadają sytuacjom po wykonaniu pierwszego ruchu. W liściach mamy puste kupki.
1.10. Drzewo gry 15 Rysunek 1.8: Drzewo gry z waluacją 1(2, 1)+ 2(1, 1) 2(2, 0) 2(0, 1)+ 1(1, 0) 1(1, 0) 1(0, 0)+ 1(0, 0)+ 2(0, 0) 2(0, 0) Każdemu wierzchołkowi drzewa przypisujemy teraz wartość 1 lub -1, w zależności czy jest to konfiguracja wygrywająca, czy przegrywająca dla pierwszego gracza. Przypisywanie wartości zaczynamy od liści. Jeżeli w liściu ruch ma pierwszy gracz, to nadajemy mu wartość 1, ponieważ wygrał on grę, przed chwilą drugi gracz zabrał ostatnie kamienie. Jeżeli ruch należy do drugiego gracza, to liść dostaje wartość -1. Następnie nadajemy wartości wewnętrznym wierzchołkom. Jeżeli wszyscy synowie jakiegoś wierzchołka v mają już wartości, to można przypisać wartość wierzchołkowi v. Jeżeli ruch ma pierwszy gracz, to wierzchołkowi v nadajemy wartość równą największej wartości synów v. Jeżeli ruch należy do drugiego gracza, to bierzemy minimum. Na rysunku 1.8 zaznaczono wartości wszystkich wierzchołków. Znak + oznacza wartość +1, znak wartość -1. Korzeń ma wartość +1, co oznacza, że gracz pierwszy może wygrać tą grę. Powinien w pierwszym ruchu przejść do wierzchołka z wartością 1 (w tym wypadku do trzeciego syna). 1.10.1 Algorytm waluacji drzewa gry Opiszemy teraz algorytm, który oblicza wartości wszystkich wierzchołków w drzewie gry. Używając stosu będzie on przeszukiwał w głąb drzewo gry. Elementy na stosie są następującej postaci: gdzie x = (cr(x), konf(x), war(x), nr(x)),
16 Rozdział 1. Stosy, kolejki i drzewa cr(x) oznacza, czyj ruch, konf(x) oznacza konfigurację kamieni, czyli ile kamieni jest w poszczególnych kupkach. war(x) oznacza wartość wierchołka, nr(x) oznacza, numer następnego do rozpatrzenia ruchu. Algorytmu waluacji drzewa gry Tworzymy elemenet dla konfiguracji rozpoczynaąjcej grę i wkładamy go na stos. Dopóki stos nie jest pusty obrabiamy element x z na wierzchu stosu. Procedura obróbki elementu x z wierzchu stosu Jeżeli element x jest liściem, to: nadajemy mu wartość według zasady: jeżeli cr(x) = 2, to war(x) := 1, jeżeli cr(x) = 1, to war(x) := 1, zdejmujemy x ze stosu. Jeżeli poprzednią operacją na stosie było zdjęcie elementu z, to: uaktualniamy wartość war(x) według zasady: Jeżeli cr(x) = 1, to war(x) := max(war(x), war(z)). Jeżeli cr(x) = 2, to war(x) := min(war(x), war(z)). Jeżeli można wykonać następny ruch (z numerem nr(x)), to tworzymy nowy element y z polami: cr(y) := 3 cr(x); konf(y) := conf(x) po wykonaniu ruchu nr(x); jeżeli cr(y) = 2, to war(y) := 1, jeżeli cr(y) = 1), to war(y) = 1. nr(y) := 1. nr(x) := nr(x) + 1 wkładamy element y na stos (element x nie jest zdejmowany ze stosu i y jest kładzione na nim. Jeżeli ruchu o numerze nr(x) nie można wykonać, to zdejmujemy x ze stosu. Pierwszy element odpowiadający początkowej konfiguracji jest ze stosu zdejmowany na samym końcu. Jego wartość w chwili zdjęcia ze stosu jest końcową wartością. Jeżeli jest ona równa 1, to gracz pierwszy posiada w tej grze strategię wygrywającą. Strategia ta polega na wybieraniu takiego ruchu, który w drzewie gry prowadzi do stanu z największą wartością. W czasie algorytmu waluacji całe drzewo nie jest zapamiętywane. Nawet dla niewielkiej gry może ono być bardzo duże. W każdym momencie pamiętana jest na stosie tylko ścieżka od korzenia do bieżącej konfiguracji. Aby wiedzieć jak pierwszy gracz powinien grać, dobrze jest zapamiętywać numer ruchu, który prowadzi z korzenia do elementu z
1.10. Drzewo gry 17 wartością 1. Dla innych elementów taka informacja nie może być zapamiętywana, bo wymagałoby to zbyt duęj pamięci. Dlatego po wykonaniu pierwszego ruchu i po odpowiedzi drugiego gracza pierwszy gracz musi znowu obliczać wartosc dla bieżącej konfiguracji. Przykład 1.8 Zobaczmy jaka będzie zawartość stosu po kolejnych krokach algorytmu waluacji dla gry opisanej wyżej. Zaczynamy od konfiguracji poczatkowej [1, (2, 1), 1, 1], która odpowiada poczatkowi gry. Przypominamy, że pierwsza jedynka oznacza, że ruch należy do pierwszego gracza, para (2, 1) oznacza konfigurację kamieni, 1 jest wstępna waluacją korzenia gry, a ostatnia jedynka oznacza, że następny do rozpatrzenia jest pierwszy syn. Po trzech kolejnych iteracjach głównej pętli algorytmu stos wyglada następująco: [1, (2, 1), 1, 2] [2, (1, 1), +1, 1] [1, (2, 1), 1, 2] [2, (1, 1), +1, 2] [1, (1, 0), 1, 1] [1, (2, 1), 1, 2] [2, (1, 1), +1, 2] [1, (1, 0), 1, 2] [2, (0, 0), +1, 1] Ostatni element na stosie jest liściem. Można teraz obliczyć jego wartość -1. [1, (2, 1), 1, 2] [2, (1, 1), +1, 2] [1, (1, 0), 1, 2] [2, (0, 0), +1, 1] W następnych iteracjach zdejmowane sa kolejne elementy ze stosu: [1, (2, 1), 1, 2] [2, (1, 1), +1, 2] [1, (1, 0), 1, 2] [1, (2, 1), 1, 2] [2, (1, 1), 1, 2] [1, (2, 1), 1, 2] Nastapił powrót do korzenia. Teraz rozważany będzie drugi ruch w konfiguracji poczat- kowej. [1, (2, 1), 1, 3] [2, (2, 0), +1, 1] [1, (2, 1), 1, 3] [2, (2, 0), +1, 2] [1, (1, 0), 1, 1] [1, (2, 1), 1, 3] [2, (2, 0), +1, 2] [1, (1, 0), 1, 2] [2, (0, 0), +1, 1] Ostatni element na stosie jest liściem. Obliczamy jego wartość 1. [1, (2, 1), 1, 3] [2, (2, 0), +1, 2] [1, (1, 0), 1, 2] [2, (0, 0), +1, 1] W następnych krokach zdejmowane sa kolejne elementy ze stosu: [1, (2, 1), 1, 3] [2, (2, 0), +1, 2] [1, (1, 0), 1, 2] [1, (2, 1), 1, 3] [2, (2, 0), 1, 3] Nastapił powrót do drugiego syna konfiguracji poczatkowej. Teraz rozpatywany będzie drugi ruch z tej konfiguracji. [1, (2, 1), 1, 3] [2, (2, 0), 1, 3] [1, (0, 0), 1, 1] Znowu dotarliśmy do liścia; jego wartość wunosi +1. [1, (2, 1), 1, 3] [2, (2, 0), 1, 3] [1, (0, 0), 1, 1] Zdejmujemy dwa elementy ze stosu [1, (2, 1), 1, 3] [2, (2, 0), 1, 3] [1, (2, 1), 1, 3] Znowy jesteśmy w korzeniu drzewa. Do rozpatrzenia został trzeci ruch [1, (2, 1), 1, 4] [2, (1, 0), 1, 1] [1, (2, 1), 1, 4] [2, (1, 0), +1, 1] [1, (0, 0), 1, 1] Znowu dotarliśmy do liścia; jego wartość wynosi +1 [1, (2, 1), 1, 4] [2, (1, 0), +1, 1] [1, (0, 0), 1, 1] Teraz zostana po kolei zdjęte wszystkie elementy ze stosu [1, (2, 1), 1, 4] [2, (1, 0), +1, 1]
18 Rozdział 1. Stosy, kolejki i drzewa [1, (2, 1), +1, 4] Koniec pracy algorytmu; ostateczna wartość korzenia wynosi +1. 1.11 Zadania 1. Ile wierzchołków może mieć drzewo binarne wysokości h? 2. Przeszukaj metodą w głąb ( wszerz ) drzewo z rysunku??. 3. Narysuj drzewo dla wyrażenie 2(a + 3)/(b + 4). Przedstaw to wyrażenie w postaci postfixowej i prefixowej. 4. Narysuj drzewo dla wyrażenie 2, 3+5/7 3, 1. Przedstaw to wyrażenie w postaci infixowej i prefixowej. Oblicz wartość tego wyrażenia. Przedstaw to wyrażenie w postaci infixowej i prefixowej. 5. Udowodnij, że każde pełne drzewo binarne o n liściach ma n 1 wierzchołków wewnętrznych. Drzewo binarne nazywa się pełne, jeżeli każdy jego wierzchołek ma albo dwóch synów, albo nie ma synów wcale (jest liściem). 6. Narysuj drzewo decyzyjne dla sortowania czterech elementów. 7. Jak implementować drzewo binarne za pomocą tablic. 1.12 Problemy 1.12.1 Szukanie fałszywej monety 1. Zaprojektuj drzewo decyzyjne dla szukania jednej fałszywej spośród 9 monet, jeżeli wiadomo, że fałszywa moneta jest lżejsza. 2. Opisz jak za pomocą k ważeń wyszukać lżejszą spośród 3 k monet. 3. Zaprojektuj drzewo decyzyjne dla szukania jednej fałszywej (lżejszej lub cięzszej) spośród monet a, b, c i d, jeżeli mamy dodatkowo do dyspozycji dobrą monetę x. 4. Pokaż, że jeżeli mamy dodatkowe dobre monety, to za pomocą k ważeń możemy zidentyfikować fałszywą monetę (i powiedzieć, czy jest lżejsza, czy cięższa) dla co najwyżej (3k 1) 2 monet. 5. Pokaż, że jeżeli nie mamy dodatkowych monet, to za pomocą k ważeń możemy zidentyfikować fałszywą monetę dla co najwyżej (3k 3) 2 monet.