Analiza Algorytmów Ćwiczenia

background image

Rozdział 7

Wyszukiwanie wyczerpujące

7.1.

Wprowadzenie

Rozwiązanie wielu problemów wymaga odpowiedzi na pytania typu: „ jak wiele jest możliwości. . . ”,

„znaleźć wszystkie możliwe zbiory spełniające pewien warunek”, „czy istnieje sposób. . . ”.

Znalezienie odpowiedzi na te pytania polega zwykle na analizie wszystkich potencjalnych rozwiązań

i wyborze spośród nich tych, które spełniają warunki zadania. Ponieważ w ten sposób zostają wyczerpane

wszystkie możliwe rozwiązania, postępowanie takie nazywamy wyszukiwaniem wyczerpującym (ang.

exhaustive search). Sposobem tym rozwiązujemy problemy, dla których nie istnieje lub nie jest znany

algorytm generujący rozwiązanie w inny sposób. Wyszukiwanie wyczerpujące jest bowiem czasochłonne,

co w skrajnych przypadkach może uniemożliwiać praktyczne jego zastosowanie.

7.2.

Organizacja poszukiwań

Dalej zostaną przedstawione dwie ogólne metody organizacji tego rodzaju poszukiwań: metoda powro-

tów i metoda sita.



metoda powrotów (ang. backtrack) polega na kostruowaniu pewnego częściowego rozwiązania

problemu i próby rozszerzania tego rozwiązania w kolejnych krokach. Jeśli rozszerzenie nie jest

możliwe, należy skrócić rozwiązanie częściowe i spróbować rozszerzyć je w inny niż poprzednio

sposób. W razie niepowodzenia skracanie (czyli właśnie powroty) jest kontynuowane. Metoda ta

gwarantuje znalezienie rozwiązania, jeśli tylko takie istnieje i jest poprawnie opisane w algorytmie.

Metoda powrotów jest stosowana dla szerokiej klasy problemów wyszukiwawczych, m.in. w rozbio-

rach zdań, grach itp.

93

background image

94

Analiza algorytmów - ćwiczenia

Termin backtrack wprowadził D. H. Lehmer w 1950 roku, ale sama metoda była wielokrotnie

wcześniej odkrywana, chociaż nie została opisana w ogólnej postaci.



metoda sita jest logicznym uzupełnieniem metody powrotów i polega na eliminacji rozwiązań

nie spełniających warunków zadania. Zbiór rozwiązań konstruuje się więc „z góry”, wychodząc od

wszystkich możliwych rozwiązań. Sita są wygodne przede wszystkim w zastosowaniach z zakresu

teorii liczb.

Metody powrotów i sita są tylko metodami ogólnymi. Bezpośrednie ich stosowanie prowadzi zwykle

do algorytmów o ogromnych wymaganiach czasowych iłub pamięciowych. Na komputerach PC w roz-

sądnym czasie rzędu godzin można przeszukiwać zbiory o mocy 10

10

. Dlatego też metody te powinny

być uważane za schemat konstrukcji rozwiązania, a nie rozwiązanie. Schemat taki musi być każdorazo-

wo dostosowywany do konkretnych potrzeb, i to na tyle pomysłowo, aby możliwa była jego praktyczna

realizacja.

7.3.

Metoda powrotów

Koncepcja metody powrotów zostanie wyjaśniona na przykładzie zadania o przechodzeniu przez labirynt.

7.1. Szukanie wyjścia z labiryntu

Celem jest znalezienie drogi z pola początkowego (we) do pola końcowego (wy) przez sąsiadujące

pola, nie przechodząc oczywiście przez ściany (rys. ??).

Najbardziej prymitywnym rozwiązaniem byłoby ponumerowanie wszystkich pól labiryntu, wyge-

nerowanie wszystkich możliwych permutacji ciągów tych numerów o długościach nie przekraczających

liczby pól labiryntu i sprawdzenie, które z tych permutacji opisują rozwiązanie. Dla przykładowego la-

birytnu z rysunku ?? oznaczałoby to sprawdzenie 25! + 24! + . . . + 1! permutacji. Liczba ta przekracza

1, 6e25 i realizacja takiego algorytmu w rozsądnyn czasie jest oczywiście nierealna.

Istnieje też algorytm znajdujący drogę w czasie O(n

2

), gdzie n jest długością boku labiryntu. Tutaj

zostanie przedstawiona metoda powrotów, która również pozwala na efektywne znalezienie rozwiązania.

Algorytm zbudowany zgodnie z tą koncepcją może wyglądać następująco:

background image

Rozdział 7: Wyszukiwanie wyczerpujące

95

1. Jeśli znajdujemy się na końcowym polu, to rozwiązanie zostało znalezione – koniec szukania.

2. Spośród pól sąsiadujących z tym, na którym aktualnie stoimy, wybieramy jedno z tych, które jeszcze

nie należy do drogi, do którego da się przejść i jednocześnie nie było jeszcze analizowane Jeśli jest

takie pole, wydłużamy drogę o to pole i realizujemy szukanie drogi tak, jakby było ono polem

wyjściowym. Jeśli nie, to skracamy drogę o aktualne pole. Jeśli nie da się skrócić, to rozwiązania

nie ma.

Reguły te mówią, w jaki sposób przedłużyć aktualną drogę (o ile to możliwe) i co zrobić w razie

niepowodzenia.

Na tym polega metodów: należy rozszerzać aktualne rozwiązanie dopóki jest to możliwe, a gdy

rozwiązanie nie może być dalej rozszerzane, należy wrócić do stanu bezpośrednio poprzedzającego stan

„zablokowany” i sprawdzić nie badane dotychczas rozwiązania.

Jest to rekurencyjna wersja algorytmu. Wymaga pamiętania na każdym poziomie (odpowiadającym

bieżącej długości drogi) listy pól sąsiadujących już przeanalizowanych i pozostałych do analizy.

7.4.

Ogólny algorytm

W najogólniejszym przypadku zakładamy, że rozwiązaniem problemu jest wektor (a

1

, a

2

, . . . a

k

) o skoń-

czonej, ale nie znanej z góry długości, spełniający pewne warunki. Każda „współrzędna” a

i

jest ele-

mentem skończonego, liniowo uporządkowanego zbioru A

i

, oznaczającego możliwe wartości a

i

, być

może różne dla różnych i.

W wyszukiwaniu należy więc jako potencjalne rozwiązania rozważyć elementy zbioru A

1

×A

2

×. . . A

k

dla k = 0, 1, 2, . . . .

W przykładzie z labiryntem wszystkie zbiory A

i

będą jednakowe:

A

i

=

{ruch w lewo, ruch w górę, ruch w prawo, ruch w dół}

Reguły można opisać następująco: nie wolno przechodzić przez ściany, po ruchu w górę nie może

wystąpić ruch w dół, po ruchu w lewo nie może wystąpić ruch w prawo itd., nie można wykonać ruchu

na pole już znajdujące się w bieżącej drodze.

Powróćmy do ogólnego algorytmu. Na początek przyjmuje się jako rozwiązanie częściowe wektor

pusty (), a na podstawie danych ograniczeń określamy, które elementy zbioru A

1

są kandydatami na

współrzędną a

1

. Taki podzbiór zbioru A

1

oznaczymy przez S

1

. Wybieramy jako a

1

pierwszy element ze

zbioru S

1

otrzymując w ten sposób rozwiązanie częściowe (a

1

).

Ogólnie, różne ograniczenia opisujące rozwiązania mówią, jaki podzbiór S

k

zbioru A

k

jest zbiorem

kandydatów na roszerzenie rozwiązanie częściowego (a

1

, . . . a

k−1

) do (a

1

, . . . a

k−1

, a

k

). Jeśli S

k

=

∅, to

background image

96

Analiza algorytmów - ćwiczenia

„wracamy” i wybieramy jako a

k−1

inny element S

k−1

. Jeśli znowu S

k−1

=

∅, to cofamy się do a

k−2

. Jeśli

wycofamy się aż do a

1

i też okaże się, że nie ma już innych elementów S

1

do rozszerzenia rozwiązania,

to skończyliśmy przeszukiwanie.

Dzięki ograniczaniu możliwych ruchów do elementów zbioru S

k

zamiast A

k

, mamy możliwość znacz-

nego okrojenia tzw. drzewa wyszukiwania. Bez ograniczania musielibyśmy przeanalizować drzewo o

|A

1

| · |A

2

| · . . . · |A

d

| liściach, gdzie d jest długością rozwiązania, jak przedstawiono na rys. 7.2.

7.2. Ilustracja możliwości obcinania gałęzi drzewa wyszukiwania. Bez obcinania przeanalizowane musia-

łyby być wszystkie węzły. Obcięcia znacznie redukują liczbę węzłów, tym bardziej, im wyżej (bliżej

korzenia) są dokonywane.

Algorytm z powrotami pozwala wcześnie odcinać całe gałęzie drzewa wyszukiwania, zmniejszając

liczbę koniecznych obliczeń.

Rysunek 7.3 przedstawia przykładowe drzewo z odciętymi gałęziami i kolejność przeszukiwania

węzłów drzewa przez algorytm wyszukiwania wyczerpującego. Jeśli rozwiązania istnieją, to na pewno

będą w tym drzewie – to musi zagwarantować metoda odcinania gałęzi.

Korzeń drzewa reprezentuje pusty wektor rozwiązania ().

Każdy wierzchołek drzewa reprezentuje pewne rozwiązanie częściowe (a

1

, a

2

, . . . , a

i

).

Ogólny algorytm wyszukiwania wyczerpującego (z powrotami) można zapisać następująco:

Powyższa wersja wymaga jednoczesnego zapamiętywania wielu zbiorów S

k

, dla różnych k (pozio-

mów). Może to być zrealizowane np. w tablicy. Zmienna k numeruje „poziomy”, w przypadku labiryntu

background image

Rozdział 7: Wyszukiwanie wyczerpujące

97

7.3. Przykładowe drzewo z odciętymi gałęziami i kolejność przeszukiwania węzłów drzewa przez algorytm

wyszukiwania wyczerpującego, zaznaczona przerywaną linią.

byłyby to długości aktualnie analizowanej drogi. Przy skracaniu rozwiązania (tzn. powrotach z powodu

niepowodzenia w rozszerzaniu wektora (a

1

, a

2

, . . . , a

k

)) potrzebna jest informacja o możliwościach – ele-

mentach zbiorów S

k−1

, S

k−2

itd., które nie zostały jeszcze przeanalizowane. Stąd konieczność jawnego

przechowywania wielu S

k

np. w tablicy. Jest to właściwie zapis rekurencyjnego algorytmu w postaci

nierekurencyjnej, ze stosem emulowanym w tablicy. Wersja rekurencyjna może okazać się czytelniejsza:

Operator

|| oznacza konkatenację ciągów, czyli „zlepianie” ciągów:

(a

1

, a

2

, . . . , a

k

)

|| (b

1

, b

2

, . . . , b

l

) = (a

1

, a

2

, . . . , a

k

, b

1

, b

2

, . . . , b

l

)

W powyższym algorytmie do ciągu w dołącza się nowy element a.

7.5.

Metoda podziału i ograniczeń

Metoda podziału i ograniczeń jest odmianą metody powrotów. Zakładamy, że z każdym rozwiązaniem

jest związany pewien koszt, a rozwiązaniem optymalnym jest rozwiązaniem o najniższym koszcie. Aby

metoda to mogła być zastosowana, musi też być możliwe zdefiniowanie kosztu rozwiązań częściowych.

background image

98

Analiza algorytmów - ćwiczenia

1

procedure wyszukiwanie z powrotami;

2

begin

3

„wyznacz S

1

na podstawie A

1

i ograniczeń”;

4

k := 1;

5

while k > 0 do

6

while S

k

= ∅ do

7

a

k

:= element z S

k

;

8

S

k

:= S

k

− {a

k

};

9

if „(a

1

, a

2

, . . . , a

k

) jest rozwiązaniem” then

10

„wypisz (a

1

, a

2

, . . . , a

k

)”;

11

end if;

12

k := k + 1;

f

następny „poziom”

g

13

„wyznacz S

k

na podstawie A

k

i ograniczeń”;

14

end while;

15

k := k

− 1;

f

powrót

g

16

end while;

17

end.

1

procedure wysz z pow rek(w: wektor; i: integer);

2

var a: element wektora; S: zbiór;

3

begin

4

if „w jest rozwiązaniem” then

5

„wypisz(w)”;

6

end if;

7

„wyznacz S”;

8

for a

∈ S to Poprawić BookAA.cls do

9

wysz z pow rek(w

|| (a), i + 1);

10

end for;

11

end.

background image

Rozdział 7: Wyszukiwanie wyczerpujące

99

Ponadto, rozszerzanie rozwiązania nie może zmniejszać jego kosztu:

koszt(a

1

, a

2

, . . . , a

k−1

)

 koszt(a

1

, a

2

, . . . , a

k−1

, a

k

)

Jeśli są spełnione te warunki, to można odrzucać rozwiązania częściowe, których koszt jest większy od

dotychczas znalezionego rozwiązania optymalnego. Dzięki temu odcinaniu gałęzi drzewa wyszukiwania

uzyskuje się dalsze zmniejszenienie liczby węzłów do analizy.

Algorytm w ogólnej postaci jest podobny do przedstawionego wcześniej algorytmu ogólnego wyszu-

kiwania wyczerpującego. Jest tylko uzupełniony o wyznaczanie kosztu, zapamiętywanie aktualnej mini-

malnej wartości tego kosztu i odcinanie gałęzi drzewa na pewno prowadzących do rozwiązań o wyższym

koszcie niż bieżące minimum.

Oczywiście metoda ta może być stosowana tylko dla szczególnego rodzaju problemów, najczęściej

takich, w których każdy liść pełnego drzewa wyszukiwania reprezentuje poprawne rozwiązanie i należy

tylko wybrać spośród nich rozwiązanie optymalne (najtańsze).

Ogólny algorytm rozwiązujący problem metodą podziału i ograniczeń można zapisać następująco:

1

procedure wyszukiwanie z powrotami;

2

begin

3

min koszt :=

∞;

f

nieskończoność, bieżące minimum

g

4

koszt := 0;

f

aktualny koszt

g

5

„wyznacz S

1

na podstawie A

1

i ograniczeń”;

6

k := 1;

7

while k > 0 do

8

while (S

k

= ∅) and (koszt < min koszt) do

9

a

k

:= element z S

k

;

10

S

k

:= S

k

− {a

k

};

11

koszt := koszt(a

1

, a

2

, . . . , a

k

);

12

if „(a

1

, a

2

, . . . , a

k

) jest rozwiązaniem” and koszt < min koszt then

13

min koszt := koszt((a

1

, a

2

, . . . , a

k

));

14

end if;

15

k := k + 1;

f

następny „poziom”

g

16

„wyznacz S

k

na podstawie A

k

i ograniczeń”;

17

end while;

18

k := k

− 1;

f

powrót

g

19

koszt := koszt((a

1

, a

2

, . . . , a

k

));

20

end while;

21

end.

background image

100

Analiza algorytmów - ćwiczenia

7.6.

Modyfikacje metody powrotów

W konkretnych przypadkach zadań rozwiązywanych metodą wyszukiwania wyczerpującego możliwe są

różnego rodzaju optymalizacje przyspieszające przeszukiwanie drzewa. Wymienimy tu niektóre z nich:



wykluczanie (lub obcinanie gałęzi) polega na usuwaniu pewnych poddrzew z drzewa wszyst-

kich rozwiązań, czyli obcinaniu gałęzi nie tylko w przypadku prostego braku możliwości rozszerze-

nia rozwiązania wynikającego z ograniczeń nałożonych przez zadanie, ale również przy spełnieniu

innych warunków. Pewnym odejściem od ścisłego przeszukiwania wyczerpującego mogą tu być

heurystyczne metody oceny stanu wyszukiwania. Na podstawie oceny można odciąć gałęzie „nie

rokujące nadziei” na dojście do rozwiązania, ale bez pewności, czy ocena była prawidłowa i czy nie

odrzucono gałęzi prowadzącej do (być może jedynego) rozwiązania.



skelejanie (lub łączenie gałęzi) polega na unikaniu wielokrotnego powtarzania tego samego

przeszukiwania, jeśli w drzewie są dwa lub więcej podrzewa izomorficzne, czyli takiego samego

kształtu

1

. Wtedy wystarczy przeszukać tylko jedno z nich, a dla kolejnych prób przeszukiwania

odczytać wcześniej uzyskany wynik. Przykładowo, na rys. 7.4 przedstawiono labirynt, którego lewa

część połączona jest z prawą tylko jednym przejściem. W takiej sytuacji początkowa część drzewa

może zawierać wiele węzłów opisujących sposób dojścia do tego przejścia. Ale każdy z tych węzłów

będzie korzeniem identycznego poddrzewa opisującego dalszą drogę z tego przejścia do wyjścia z

labiryntu. Wystarczyłoby przeanalizować tylko jedno takie podrzewo. Praktyczna implementacja

tego sposobu może być bardzo trudna.

7.4. Labirynt ilustrujący możliwość łączenia gałęzi drzewa wyszukiwania



reorganizacja wyszukiwania - jest to metoda heurystyczna, użyteczna, gdy istnienie rozwią-

zań jest wątpliwe, lub gdy poszukuje się tylko jednego (jakiegokolwiek) rozwiązania. Jeśli istnieje

podejrzenie, że rozwiązanie będzie miało jakiąś szczególną własność, warto tak poprowadzić poszu-

kiwania, aby to potencjalne rozwiązanie było sprawdzone w pierwszej kolejności, a przynajmniej

jak najszybciej. Jeśli jest to możliwe, drzewo powinno być tak przebudowane, aby węzły niskiego

1

przez jednakowy kształt rozumiemy tu identyczność drzew

background image

Rozdział 7: Wyszukiwanie wyczerpujące

101

stopnia (tzn. o małej liczbie synów) znalazły się blisko korzenia drzewa. Wtedy ewentualne odcięcie

gałęzi spowoduje eliminację większej liczby liści. Ilustruje to rys. 7.5.

7.5. Reorganizacja drzewa. Obydwa drzewa mają jednakową liczbę liści, ale drzewo (a) można efektywniej

przeszukać niż (b), ponieważ odcięcie gałęzi wyeliminuje naraz więcej liści.

7.7.

Szacowanie efektywności

Na ogół metoda powrotów prowadzi do algorytmów o wykładniczej złożoności względem długości wektora

rozwiązań. Jeśli rozwiązania maja długość co najmniej d, to należy w drzewie sprawdzić

d



i=1

|A

i

|

liści, gdzie

|A

i

| jest liczbą elementów w zbiorze A. Nawet przy szerokim zastosowaniu wykluczania i

sklejania można się spodziewać, że

|S

i

| będzie stałą – pewnym ułamkiem |A

i

|.

Realizacja algorytmu wyszukiwania może więc trwać bardzo długo. Użytkownik chciałby wiedzieć,

jakiego czasu działania ma się spodziewać i znać przybliżony czas zakończenia obliczeń. Oszacowanie

tego czasu nie jest możliwe bez znajomości liczby węzłów drzewa wyszukiwania, które będą analizowane.

Wtedy można obliczyć stosunek liczby dotychczas przeanalizowanych węzłów do wszystkich węzłów i

wyznaczyć ułamek wykonanej pracy, który można np. przedstawić w postaci „paska postępu” (ang.

progress bar). Jednak dokładne obliczenie liczby wszystkich węzłów metodami analitycznymi jest bardzo

trudne ze względu na nieprzewidywalność oddziaływań różnych ograniczeń podczas schodzenia w głąb

drzewa.

7.7.1.

Metoda Monte Carlo

W przypadku trudnych do obliczenia metodami analitycznymi wartości stosuje się czasem metodę Monte

Carlo. Prawidłowe jest skojarzenie tej nazwy z kasynami i grami losowymi. Metoda polega na wykonaniu

pewnej liczby eksperymentów losowych i oszacowaniu szukanej wartości na podstawie wyników tych

eksperymentów. Przykładowo, chcąc wyznaczyć pole koła albo innej figury nie znając dokładego wzoru

opisującego to pole, można umieścić figurę w kwadracie o jednostkowym polu. Następnie wylosować dużą

liczbę punktów w tym kwadracie (rozkład prawdopodobieństwa powinien być równomierny), dla każdego

sprawdzić, czy należy do figury czy nie i ostatecznie wyznaczyć stosunek liczby punktów „trafionych” do

background image

102

Analiza algorytmów - ćwiczenia

wszystkich wylosowanych, otrzymując przybliżoną wartość pola figury. Błąd, którym będzie obarczony

wynik, będzie tym mniejszy, im więcej punktów zostanie wylosowanych.

7.7.2.

Szacowanie wielkości drzewa

Tę samą metodę można zastosować do oszacowania wielkości drzewa wyszukiwania. W tym celu zostanie

przeprowadzonych kilka eksperymentów polegających na wykonaniu wyszukiwania metodą powrotów z

wybranymi losowo wartościami a

i

.

Wędrując przy każdym eksperymencie w głąb drzewa będziemy szacować wielkość całego drzewa.

W tym celu założymy, że wszystkie rozgałęzienia wyglądają tak, jak napotykane wzdłuż ścieżki, którą

przechodzimy, czyli mają tyle samo synów. Dla każdego eksperymentu uzyskuje się więc jedno oszacowa-

nie liczby węzłów drzewa. Ilustruje to rys. ??. Wylosowana ścieżka, którą poruszamy się w dół drzewa,

po wyjściu z korzenia rzechodzi przez węzeł o dwóch synach. Przyjmujemy więc, że wszystkie pozostałe

węzły na tym poziomie też mają dwóch synów. Na następnych poziomach również przyjmujemy takie

założenie, aż do liścia kończącego ścieżkę.

7.6. Szacowanie wielkości drzewa

Jeśli oznaczymy liczbę synów węzłów na wylosowanej ścieżce przez x

1

, x

2

, . . . , x

d

, gdzie d jest

długością ścieżki, to całkowita liczba węzłów W drzewa (bez korzenia) zostanie oszacowana przez:

W = x

1

+ x

1

x

2

+ x

1

x

2

x

3

+ . . . +

d



j=1

x

j

=

d



i=1

i



j=1

x

j

W jednym eksperymencie sprawdzamy tylko jedną ścieżkę. Aby uzyskać wiarygodny wynik, nale-

ży wykonać więcej eksperymentów i uśrednić wynik. Liczba eksperymentów zależy głównie od czasu,

który możemy poświęcić na szacowanie wielkości drzewa. Im więcej eksperymentów, tym dokładniejsze

oszacowanie.

Algorytm szacujący wielkość drzewa jest modyfikacją podstawowego algorytmu wyszukiwania wy-

czerpującego, ale bez powrotów – za każdym razem interesuje nas tylko jedna droga z korzenia do liścia,

więc nie ma potrzeby implementacji skracania rozwiązania. Poza tym różni się od niego wyborem loso-

wego elementu z S

i

zamiast kolejnego i jest uzupełniony o obliczanie średniej liczby węzłów.

Przykładowo, szacując wielkość drzewa wyszukiwania dla problemu rozmieszczania 11 hetmanów

na szachownicy 11 na 11, można otrzymać dla N = 1000 oszacowanie liczby węzłów na 70806, przy

rzeczywistej liczbie węzłów 70214.

background image

Rozdział 7: Wyszukiwanie wyczerpujące

103

1

procedure szacowanie;

2

begin

3

f

N oznacza liczbę eksperymentów do przeprowadzenia

g

4

średnia := 0;

5

for i := 1 to N do

6

iloczyn := 1;

f

na iloczyn x

1

x

2

x

3

. . .

g

7

suma := 0;

f

na sumę iloczynów x

1

+ x

1

x

2

+ . . .

g

8

„wyznacz S

1

na podstawie A

1

i ograniczeń”;

9

k := 1;

10

while S

k

= ∅ do

11

iloczyn := iloczyn *

|S

k

|;

12

suma := suma + iloczyn;

13

a

k

:= losowy element z S

k

;

14

f

nie musimy usuwać a

k

z S

k

, bo i tak nie będzie powrotu

g

15

k := k + 1;

f

następny „poziom”

g

16

„wyznacz S

k

na podstawie A

k

i ograniczeń”;

17

end while;

18

f

dotarliśmy do liścia, mamy oszacowanie liczby węzłów w tym eksperymencie

g

19

średnia := średnia + suma;

20

end for;

21

średnia := średnia / N ;

22

end.

background image

104

Analiza algorytmów - ćwiczenia

7.8.

Drzewa gier i

α–β obcięcie

Inny wariant metody podziału i ograniczeń jest stosowany podczas budowania drzewa gier.

Drzewo gry jest to drzewo, które powstaje w wyniku sprawdzania metodą powrotów wszystkich

możliwych ciągów posunięć. Korzeniem drzewa jest początkowy stan gry, synami korzenia są wszystkie

możliwe stany, jakie mogą wystąpić po ruchu pierwszego gracza, synami tych węzłów są z kolei wszystkie

możliwe stany, które mogą wystąpić po odpowiadającym ruchu drugiego gracza itd.

7.7. Fragmenty drzewa gry w kółko i krzyżyk. Źródło: [1].

Na przykład, rysunek 7.7 przedstawia fragmenty drzewa gry w kółko i krzyżyk na planszy 3*3.

Drzewo pokazuje tylko synów nierównoważnych ze względu na obroty i symetrie.

Każdy liść drzewa gry odpowiada jednemu z możliwych zakończeń gry. W omawianym przypadku

będzie to zwycięstwo krzyżyka, zwycięstwo kółka lub remis.

Spróbujmy użyć tego drzewa do znalezienia strategii wygrywającej dla grającego krzyżykiem. Stra-

tegia wygrywająca to takie postępowanie, które doprowadzi do wygranej niezależnie od posunięć drugiego

gracza. Oczywiście nie zawsze istnieje taka strategia. Zbudowanie i analiza pełnego drzewa gry pozwala

odpowiedzieć na pytanie o istnienie takiej strategii.

Budujemy drzewo „z punktu widzenia” pierwszego gracza, grającego krzyżykami. Po zbudowaniu

drzewa przystępujemy do przypisywania wartości liściom tego drzewa. Każdemu z nich przypisujemy

wartość +1 w przypadku wygranej x,

−1 w przypadku przegranej x i 0 w przypadku remisu.

Wartości pozostałych węzłów wyznaczamy sukcesywnie począwszy od liście w kierunku korzenia

drzewa. Będą one oceniały wartość węzła z punktu widzenia gracza x, to znaczy będą mówiły, czy dla

stanu gry opisanego danym węzłem istnieje dla niego strategia wygrywająca (wtedy węzeł ma wartość

background image

Rozdział 7: Wyszukiwanie wyczerpujące

105

+1), czy też istnieje strategia wygrywająca dla gracza o (wtedy węzeł ma wartość

−1), czy też nie istnieją

takie strategie (wtedy węzeł otrzyma wartość 0).

Dla węzła N o synach N

1

, N

2

, . . . , N

k

wartość v(N ) definiujemy w następujący sposób:

v(N ) =


max(v(N

1

), . . . , v(N

k

))

jeśli poziom węzła N jest nieparzysty

min(v(N

1

), . . . , v(N

k

))

jeśli poziom węzła N jest parzysty

Zakładamy więc, że gracz x jest inteligenty i zawsze starać się zmaksymalizować swoją wygraną. Prze-

ciwnik (o) również jest inteligentny i też stara się zmaksymalizować swoją wygraną, czyli zminimalizować

wygraną x. Jest to tzw. reguła mini-maksowa.

Przesuwamy się od liści w górę drzewa przypisując węzłom odpowiednie wartości, aż dotrzemy do

korzenia

2

.

Wyznaczenie wartości węzła jest istotne, ponieważ na podstawie tej wartości można ustalić, który

ruch (gałąź) należy wybrać. Przy wartości +1 nie ma wątpliwości, że kierując się w tę stronę dotrzemy

do liścia z naszą wygraną.

Okazuje się, że budując pełne drzewo gry dla gry w kółko i krzyżyk na planszy 3*3 i oceniając w

ten sposób węzły, uzyskamy wartość korzenia równą 0. Oznacza to, że nie istnieje strategia wygrywająca

dla żadnego z graczy. Gracze grając uważnie powinni zawsze doprowadzić do remisu, co czyni grę mało

interesującą.

Takie drzewo można budować dla wielu gier, na przykład dla szachów. Po dokonaniu wartościowania

liści tego drzewa i następnie wszystkich węzłów, można znaleźć odpowiedź na pytanie, czy istnieje stra-

tegia wygrywająca, a jeśli tak, to dla którego z graczy i w jaki sposób należy postępować, aby na pewno

wygrać. Niestety drzewo gry dla szachów jest tak duże, że (na razie) nie ma możliwości skonstruowania

go, a tym samym nie można dokonać oceny węzłów.

W takiej sytuacji pozostaje stosowanie kombinowanych rozwiązań heurystycznych. Zamiast budo-

wać pełne drzewo, konstruujemy tylko jego fragment od bieżącego stanu gry do takiej głębokości, jaką

da się praktycznie przeanalizować – np. kilku ruchów w przód. Na tym najniższym poziomie dokonujemy

wartościowania węzłów metodami heurystycznymi, tzn. oceniamy sytuację jako „bardzo dobrą”, „dobrą”,

„złą” itd., z punktu widzenia jednego z graczy. Ocenę wyrażamy jako liczbę z większego przedziału (nie

ograniczamy się tylko do

−1, 0 i +1). Ocena jest heurystyczna, tzn. dokonywana na podstawie reguł,

które wydają się sensowne osobie implementującej algorytm, ale nie wynikają ze ścisłej analizy. Następnie

węzły leżące powyżej tego poziomu są oceniane zgodnie z regułą mini-maksową, aż do węzła opisującego

bieżący stan gry. W ten sposób gracz orientuje się, jaka jest jego sytuacja i jak postąpić, aby dotrzeć do

węzła o najkorzystniejszej dla niego (największej) wartości.

Do obliczenia wartości wszystkich węzłów można oczywiście zastosować zwykłą metodę powrotów.

Lepsza (szybsza) jest jednak metoda nazywana α–β obcięciem, w której stosuje się technikę podziału i

ograniczeń do odcinania poddrzew z drzewa gry.

2

celowe jest przedstawienie fikcyjnego drzewa gry i opisanie go zgodnie z omawianą metodą

background image

106

Analiza algorytmów - ćwiczenia

Wyznaczenie wartości związanej z danym węzłem daje informację na temat wartości, która będzie

przypisana jego ojcu. Jeśli ojciec znajduje się na poziomie, dla którego v(N ) oblicza się jako maksimum,

to wartość przypisana temu węzłowi jest dolną granicą wartości związanej z jego ojcem. Ta dolna granica

jest nazywana α-wartością. W symetrycznej sytuacji, kiedy obliaczane jest minimum, górną granicę

nazywa się β-wartością.

7.8. Ilustracja α–β obcięcia. Źródło: [1].

Rysunek 7.8 ilustruje tę technikę. Jeśi węzłowi d przypisywana jest wartość 3, to znaczy, że węzeł m

nie może mieć wartości większej niż 3. Jeśli więc okaże się, że leżący dwa poziomy niżej węzeł f ma wartość

5, a węzeł h w związku z tym wartość nie mniejszą niż 5, to nie ma sensu dalej analizować poddrzewa

h, bo i tak nie doprowadzi to do przypisania h wartości mniejszej niż 3. Taką sytuację nazywamy β-

odcięciem: występuje wtedy, kiedy pewien węzeł leżący dwa poziomy poniżej węzła z β-wartością ma

wartość większą niż β.

Analogicznie wygląda sytuacja dla poziomu, na którym obliczane jest maksimum. Węzeł m ma

wartość 3, a węzeł z wartość nie większą niż 2 (ponieważ wartość q jest równa 2). Wobec tego wartość

korzenia będzie równa co najmniej 3 i można pominąć fragment drzewa leżący poniżej z. Taką sytuację

nazywamy α-odcięciem.

Oszczędność w stosunku do normalnego wyszukiwania w dużym stopniu zależy od uporządkowania

background image

Rozdział 7: Wyszukiwanie wyczerpujące

107

węzłów w drzewie.

7.9.

Przykład wyszukiwania wyczerpującego

Jednym z podręcznikowych przykładów problemu rozwiązywanego metodą wyszukiwania wyczerpującego

jest problem 8 hetmanów:

Rozmieścić na szachownicy 8 hetmanów w taki sposób, aby żaden z nich nie atakował innego.

Problemem tym zajmował się C. F. Gauss w 1850 roku, ale nie znalazł pełnego rozwiązania. Wy-

maga to bowiem nie analitycznego rozwiązania (w czym Gauss był biegły), ale po prostu wypróbowania

wszystkich możliwości. W czasach, kiedy nie istniały komputery, nawet takie niewielkie zadanie rzeczy-

wiście było wyczerpujące.

Algorytm rozwiązujący to zadanie metodą powrotów wygląda następująco:

1

procedure próbuj(i: integer);

2

var k: integer;

3

begin

4

„wyznacz możliwe pozycje hetmana w kolumnie i”;

5

for k := 1 to liczba pozycji do

6

„wybierz k-tego kandydata”;

7

if „dopuszczalny” then

8

„wstaw k-tego kandydata”;

9

if i < 8 then

10

próbuj(i+1);

11

else

12

„wydrukuj rozwiązanie”;

13

end if;

14

„usuń k-tego kandydata”;

15

end if;

16

end for;;

17

end.

Procedurę wywołujemy z parametrem i = 1. Algorytm podejmuje próby umieszczania hetmanów w

kolejnych kolumnach. Bieżące rozmieszczenie hetmanów może być przechowywane w zmiennej globalnej, a

możliwe pozycje hetmana w zmiennej lokalnej procedury. Wybór k-tego kandydata oznacza też usunięcie

go ze zbioru znalezionych kandydatów.

Algorytm znajduje 92 rozwiązania, których tylko 12 jest istotnie różnych. Pozostałe powstają przez

symetrie i obroty jednego z 12 rozwiązań.

background image

108

Analiza algorytmów - ćwiczenia

Inne problemy, które mogą być rozwiązywane metodą wyszukiwania wyczerpującego, to szukanie

drogi skoczka na szachownicy

3

, problem komiwojażera itp.

7.10.

Sita

Sito jest techniką kombinatoryczną polegającą na generacji wszystkich potencjalnych rozwiązań proble-

mu, a następnie eliminacji tych elementów, które nie spełniają warunków zadania. Metody sitowe są

logicznym uzupełnieniem metod powrotów.

Klasycznym przykładem jest sito Eratostenesa

4

do znajdowania liczb pierwszych w zadanym prze-

dziale od 2 do N dla pewnego N .

W pierwszym kroku wypisujemy wszystkie liczby z interesującego nas przedziału (N = 14):

2,

3,

4,

5,

6,

7,

8,

9,

10, 11, 12, 13, 14

Pierwszą z lewej liczbą jest 2. Oznaczamy ją jako pierwszą i usuwamy pozostałe wielokrotności 2, czyli

co drugą liczbę zaczynając od czwórki:

2,

3,

5,

7,

9,

11,

13

Następną jeszcze nie skreśloną liczbą jest 3. Zaznaczamy ją jako pierwszą i usuwamy co trzecią zaczynając

od szóstki:

2,

3,

5,

7,

11,

13

Znaleźliśmy kolejną liczbę pierwszą: piątkę. Zaznaczamy ją jako pierwszą. W tym miejscu okazuje się, że

dalej nie musimy skreślać, ponieważ 5 >

N i wszystkie liczby złożone zostały już wcześniej skreślone

(co najmniej jeden z czynników każdej liczby złożonej nie większej od N jest nie większy od

N ).

Ostatecznie otrzymujemy:

2,

3,

5,

7,

11,

13

Jeśli elementy w zbiorze potencjalnych rozwiązań mogą być indeksowane liczbami naturalnymi i

w prosty sposób wyznaczane na podstawie indeksu, to nie ma potrzeby przechowywania w pamięci

wszystkich tych rozwiązań. Wystarczy zastosować tzw. wektor charakterystyczny, którym jest taki

3

dla szczególnych rozmiarów szachownicy istnieją lepsze metody

4

a nie Erastotenesa

background image

Rozdział 7: Wyszukiwanie wyczerpujące

109

ciąg bitów, że i-ty bit jest równy 0, jeśli i-ty element zbioru nie jest rozwiązaniem, albo 1, jeśli i-ty

element zbioru jest rozwiązaniem. W ten sposób jeden bajt pamięci może przechować informację o ośmiu

elementach zbioru.

Sito Eratostenesa jest szczególnym przypadkiem sita modularnego. Sita modularne pozwalają

znajdować liczby całkowite z zadanego przedziału, które należą jednocześnie do wielu różnych ciągów

arytmetycznych.

Literatura

[1] Edward M. Reingold, Jurg Nievergelt, Narsingh Deo, Alorytmy kombinatoryczne, Państwowe Wy-

dawnictwo Naukowe, Warszawa 1985

[2] Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Wprowadzenie do algorytmów, WNT,

Warszawa 1997

Wersja 0.04 (2000-06-02, 2001-11-37) (PF)

background image

110

Analiza algorytmów - ćwiczenia

background image

Rozdział 8

Algorytmy zachłanne

111

background image

112

Analiza algorytmów - ćwiczenia

background image

Rozdział 9

Algorytmy grafowe

9.1.

Wprowadzenie

Grafy są często stosowaną strukturą danych w informatyce, a algorytmy na nich operujące należą do

podstawowych. W niniejszym rozdziale przedstawimy w skrócie sposoby reprezentowania grafów w pa-

mięci komputera. W dalszym ciągu sformułujemy kilka problemów związanych z grafami, a następnie

omówimy wybrane algorytmy ich rozwiązywania.

9.2.

Oznaczenia i pojęcia

Graf skierowany G (digraf) jest opisany parą (V, E), gdzie V jest skończonym zbiorem wierzchołków

grafu, a E jest relacją binarną w V . Elementy zbioru E nazywamy łukami grafu. W grafie nieskierowa-

nyn G = (V, E) zbiór E to zbiór nieuporządkowanych par wierzchołków. Każda taka para wierzchołków

określana jest mianem krawędzi grafu. Jeśli (u, v) jest łukiem digrafu, to mówimy, że wychodzi ona

z wierzchołka u i wchodzi do wierzchołka v. Jeśli jest ona krawędzią grafu nieskierowanego, to mówimy,

że jest incydentna (sąsiednia) z wierzchołkami u i v. Jeśli istnieje w grafie krawędź (u, v) to mówimy, że

wierzchołek v jest sąsiedni do wierzchołka u. Dla grafów nieskierowanych relacja sąsiedztwa jest syme-

tryczna, dla skierowanych nie musi być symetryczna. Stopniem wierzchołka w grafie nieskierowanym jest

liczba incydentnych z nim krawędzi. Dla wierzchołków grafów skierowanych definiuje się odpowiednio

stopień wyjściowy oraz stopień wejściowy, a także stopień, będący sumą stopnia wejściowego i stopnia

wyjściowego. Droga (ścieżka) o długości k z wierzchołka p do q w grafie G = (V, E) jest ciągiem wierz-

chołków

v

0

, v

1

, . . . , v

k

 takich, że p = v

0

, q = v

k

i (v

i−1

, v

i

)

∈ E dla i = 1, 2, . . . , k. Długość drogi jest

liczbą jej krawędzi. Droga zawiera wierzchołki i krawędzie. Droga jest prosta, jeśli wszystkie jej wierz-

chołki są różne. Poddroga drogi jest ciągiem kolejnych jej wierzchołków. W grafie skierowanym droga

113

background image

114

Analiza algorytmów - ćwiczenia

v

0

, v

1

, . . . , v

k

 tworzy cykl, jeśli v

0

= v

k

oraz droga zawiera co najmniej jedną krawędź. Cykl jest pro-

sty, jeśli wszystkie wierzchołki v

1

, . . . , v

k

są różne. Pętla jest cyklem o długości 1. Cykl

v

0

, v

1

, . . . , v

k



oraz

v

1

, v

2

, . . . , v

k

, v

0

 to ten sam cykl. Graf skierowany nie zawierający pętli nazywamy prostym. Graf

nie zawierający cykli nazywamy acyklicznym. Graf nieskierowany jest spójny, jeśli pomiędzy każdą parą

wierzchołków istnieje co najmniej jedna droga. Mówimy, że wierzchołek v jest osiągalny z u, jeśli z wierz-

chołka u istnieje droga do v. Składowe spójne grafu to klasy abstrakcji wierzchołków w relacji „ jest

osiągalny z”. Inaczej mówiąc, składowe spójne to takie podzbiory wierzchołków i krawędzi grafu, że po-

między każdą parą wierzchołków należących do tego samego podzbioru istnieje co najmniej jedna droga.

Graf skierowany jest silnie spójny, jeśli każde dwa wierzchołki są osiągalne jeden z drugiego. Silne spójne

składowe grafu skierowanego to klasy abstrakcji wierzchołków w relacji „są wzajemnie osiągalne”. Graf

skierowany jest silnie spójny, jeśli ma dokładnie jedną silnie spójną składową. Dwa grafy G = (V, E)

i G



= (V



, E



) są izomorficzne („ jednakowego kształtu”), jeśli istnieje bijekcja f : V

→ V



taka, że

(u, v)

∈ E wtedy i tylko wtedy, gdy (f(u), f(v)) ∈ E



. Czyli można przenumerować wierzchołki grafu G

tak, aby były wierzchołkami grafu G



zachowując odpowiadające krawędzie w G i G



. Graf G



= (V



, E



)

jest podgrafem grafu G = (V, E), jeśli V



⊆ V i E



⊆ E. Podgraf G



grafu G jest podgrafem utworzonym

z V



, jeśli wszystkie krawędzie łączące w G wierzchołki ze zbioru V



znajdują się również w zbiorze

E



. Acykliczny graf nieskierowany nazywa się lasem. Spójny, acykliczny graf nieskierowany nazywa się

drzewem. Graf nieskierowany jest dwudzielny, jeśli zbiór jego wierzchołków V może zostać podzielony na

dwa rozłączne podzbiory V

1

i V

2

takie, że wierzchołki każdej krawędzi tego grafu należą odpowiednio do

podzbioru V

1

i V

2

.

9.3.

Metody reprezentacji grafów w pamięci

Przyjęto dwa podstawowe sposoby reprezentacji grafu G = (V, E) w pamięci komputera: przez listy

sąsiedztwa oraz macierz sąsiedztwa. Listy sąsiedztwa zapisane są w tablicy odsyłaczy Adj. Każda lista

odpowiada pojedynczemu wierzchołkowi. Na liście znajdują się numery wierzchołków sąsiednich z tym

wierzchołkiem. Porządek wierzchołków na listach sąsiedztwa jest dowolny. Macierz sąsiedztwa jest ma-

cierzą A =

{a

ij

} o wymiarach |V | × |V | taką, że a

ij

= 1 jeśli w grafie istnieje krawędź (i, j), a 0 jeśli nie

istnieje. Rys. 9.1 przedstawia opisy grafu nieskierowanego, natomiast rys. 9.2 opisy grafu skierowanego.

9.4.

Algorytmy wyznaczania najkrótszych dróg w grafie ważonym

Funkcja wagowa w : E

→ R przyporządkowuje każdej krawędzi grafu wagę o wartości rzeczywistej. Graf

z taką funkcją jest określany jako graf ważony lub sieć. Wagą (długością) drogi p =

v

1

, v

2

, . . . , v

k

 jest

background image

Rozdział 9: Algorytmy grafowe

115

9.1. Dwie reprezentacje grafu nieskierowanego: (a) graf nieskierowany, (b) listy sąsiedztwa, (c) macierz

sąsiedztwa

9.2. Dwie reprezentacje grafu skierowanego: (a) graf skierowany, (b) listy sąsiedztwa, (c) macierz sąsiedz-

twa

suma wag tworzących ją krawędzi:

w(p) =

k



i=2

w(v

i−1

, v

i

)

Problem szukania najkrótszej drogi z wierzchołka i do j w grafie ważonym (skierowanym lub nie-

skierowanym) sprowadza się do znalezienia drogi prowadzącej z i do j o minimalnej wadze, nazywanej

również kosztem. Dróg takich może być więcej niż jedna.

Istnieją różne warianty algorytmów znajdujących najkrótsze drogi w grafie:



Najkrótsze drogi z jednym źródłem: wyróżniamy jeden wierzchołek s (nazywany źródłem, ang. sour-

ce) grafu G i szukamy najkrótszych dróg z tego wierzchołka do wszystkich pozostałych wierzchołków

grafu.



Najkrótsze drogi z jednym wierzchołkiem docelowym: wyróżniamy jeden wierzchołek docelowy t

(ang. target) grafu G i szukamy najkrótszych dróg prowadzących ze wszystkich pozostałych wierz-

chołków grafu do wierzchołka t. Zmieniając zwrot wszystkich łuków (w grafach skierowanych)

można ten problem sprowadzić do problemu najkrótszych dróg z jednym źródłem.



Najkrótsza droga między parą wierzchołków: szukamy najkrótszej drogi prowadzącej z wierzchołka

u do wierzchołka v. Rozwiązanie problemu najkrótszych dróg ze źródłem w u rozwiązuje również

ten problem. Okazuje się, że w pesymistycznym przypadku żaden algorytm znajdujący najkrótszą

background image

116

Analiza algorytmów - ćwiczenia

drogę między parą wierzchołków nie jest szybszy od najlepszego algorytmu dla problemu z jednym

źródłem.



Najkrótsze drogi między wszystkimi parami wierzchołków: szukamy najkrótszych dróg dla wszyskich

par wierzchołków grafu G. Rozwiązanie problemu z jednym źródłem dla wszystkich wierzchołków

grafu rozwiązuje również ten problem, jednak istnieją efektywniejsze algorytmy.

9.5.

Algorytm Floyda-Warshalla

Algorytm Floyda-Warshalla (FW) służy do znajdowania najkrótszych dróg między wszystkimi parami

wierzchołków danego grafu skierowanego G = (V, E). Stosowane jest tu programowanie dynamiczne.

Wykorzystywany jest fakt, że każda poddroga najkrótszej drogi łączącej dwa wierzchołki grafu jest

również najkrótszą drogą łączącą końce poddrogi.

Problem znajdowania najkrótszych dróg między wszystkimi parami wierzchołków formułuje się

następująco:

Dane:



G = (V, E, d) - sieć skierowana (lub nieskierowana),



V =

{1, 2, . . . , n} - zbiór wierzchołków,



E

⊆ V × V - zbiór krawędzi,



d : E

→ R - funkcja długości (wag) krawędzi; funkcja ta jest opisywana macierzą długości (wag)

krawędzi D =

{d

ij

}, i, j ∈ V . Dodatkowo zakłada się, że:

– Długości krawędzi d

ij

mogą być ujemne, ale w grafie nie może być cykli o ujemnej dłu-

gości. Gdyby były, nie istniałyby najkrótsze drogi między pewnymi parami wierzchołków,

ponieważ droga rozszerzona o taki cykl zmniejszyłaby swoją długość i operację taką można

byłoby powtarzać nieskończenie wiele razy.

– Jeśli w grafie G nie ma krawędzi (i, j), to przyjmujemy, że d

ij

=

∞.

– Dla każdego d zachodzi d +

∞ = ∞ oraz min(d, ∞) = d.

Szukane:

Szukamy macierzy D

=

{d

ij

}, gdzie d

ij

jest długością najkrótszej drogi prowadzącej od wierzchołka

i do wierzchołka j, i, j

∈ V .

Algorytm Floyda-Warshalla wyznacza ciąg macierzy

D

0

, D

1

, . . . , D

n

= D

background image

Rozdział 9: Algorytmy grafowe

117

takich, że D

n

jest macierzą długości najkrótszych dróg. Każda macierz D

k

, k = 1, 2, . . . , n jest wyzna-

czana następująco:

d

k

ij

= min(d

k−1

ij

, d

k−1

ik

+ d

k−1

kj

)

i, j

∈ V

dla k = 1, 2, . . . , n.

Macierz D

0

jest wyznaczana według zależności:

d

0

ij

=


0

gdy i = j,

d

ij

gdy i

= j.

Wartość d

k

ij

jest długością najkrótszej drogi od wierzchołka i do j nie przechodzącej przez żaden wierz-

chołek o numerze większym niż k, poza ewentualnie początkiem i końcem. Jest to krótsza z dwóch

następujących dróg:



najkrótszej takiej drogi nie przechodzącej przez żaden wierzchołek o numerze większym niż k

− 1,



najkrótszej drogi spośród dróg prowadzących od i do k, a następnie od k do j, nie przechodzących

między tymi wierzchołkami przez żaden wierzchołek o numerze większym niż k

− 1.

Algorytm przedstawia pseudokod 9.3.

Przykład:

Przykładowy graf skierowany i przebieg algorytmu FW przedstawiono na rysunku 9.4.

Złożoność algorytmu jest łatwa do wyznaczenia, wykonujemy n kroków dla macierzy n

× n, T (n) =

O(n

3

).

Powyższy algorytm można łatwo zmodyfikować tak, aby jednocześnie z szukaniem długości mini-

malnych dróg znajdował same drogi. Niech P =

{p

ij

} jest macierzą „następnych” wierzchołków drogi;

dokładniej: p

ij

jest drugim z kolei wierzchołkiem najkrótszej drogi prowadzącej z i do j.

Macierz P jest wyznaczana w następujący sposób:



Początkowo:

p

0

ij

=


j

gdy d

ij

= ∞,

0

gdy d

ij

=

∞.



W i-tej iteracji, jeśli

d

ik

+ d

kj

< d

ij

to przyjmujemy p

ij

:=p

ik

.

background image

118

Analiza algorytmów - ćwiczenia

1

procedure FW;

2

begin

3

f

Dane: macierz długości krawędzi D =

{d

ij

}

g

4

f

Szukane: macierz długości najkrótszych dróg D =

{d

ij

}

g

5

for i := 1 to n do

6

for j := 1 to n do

7

if i = j then

8

d[i,j] := 0;

9

end if;

10

end for;

11

end for;

12

for k := 1 to n do

13

for i := 1 to n do

14

if d[i,k]

= ∞ then

15

for j := 1 to n do

16

d[i,j] := min(d[i,j],d[i,k]+d[k,j]);

17

end for;

18

end if;

19

end for;

20

end for;

21

end.

9.3. Algorytm Floyda-Warshalla

background image

Rozdział 9: Algorytmy grafowe

119

d

1

2

3

1

2

8

5

2

3

∞ ∞

3

2

d

0

1

2

3

1

0

8

5

2

3

0

3

∞ 2

0

d

1

1

2

3

1

0

8

5

2

3

0

8

3

∞ 2 0

d

2

1

2

3

1

0

8

5

2

3

0

8

3

5

2

0

d

3

1

2

3

1

0

7

5

2

3

0

8

3

5

2

0

9.4. Przykładowy przebieg algorytmu FW

Przykład:

Przykładowy graf nieskierowany o jednostkowych wagach krawędzi i wynikowe macierze D oraz P

przedstawiono na rys. 9.5.

Zawartość macierzy P przedstawiona na rys. 9.5 pozwala np. znaleźć najkrótszą drogę z wierzchołka

1 do wierzchołka 7. Element p

17

= 2 wskazuje, że przechodząc z wierzchołka 1 do 7 należy najpierw

skierować się do wierzchołka 2. Po przejściu do wierzchołka 2 pozostaje jeszcze do przejścia droga (2, 7).

Element p

27

= 4, a więc kolejnym wierzchołkiem drogi jest 4. Tam znajdujemy p

47

= 6, dalej p

67

= 7,

co oznacza, że z wierzchołka 6 trafiamy już bezpośrednio do końca drogi, tj. wierzchołka 7.

9.6.

Algorytm Dijkstry

Algorytm Dijkstry rozwiązuje problem najkrótszych dróg z jednym źródłem w ważonym grafie skierowa-

nym G = (V, E) w przypadku, gdy wagi wszystkich krawędzi są nieujemne. Algorytm jest podobny do

algorytmu znajdującego minimalne drzewo rozpinające metodą dołączania do już znalezionego fragmen-

tu drzewa kolejnych krawędzi tak, aby w każdym kroku drzewo było rozszerzane o tę krawędź, której

waga wnosi najmniej do wagi całego drzewa. Tutaj zostanie zdefiniowany początkowo pusty zbiór S, do

którego będą dołączane kolejno te wierzchołki, dla których da się już wyznaczyć najkrótszą drogę ze

źródła do wierzchołka. Dla wszystkich wierzchołków należących na danym etapie do zbioru S długość

drogi jest już wyznaczona i nie zmieni się do końca wykonywania algorytmu.

Tablica d przechowuje bieżące oszacowanie wagi najkrótszej drogi prowadzącej ze źródła s do danego

wierzchołka (d[v] jest oszacowaniem wagi drogi ze źródła do v). Tablica p przechowuje poprzedników na

background image

120

Analiza algorytmów - ćwiczenia



















D

1

2

3

4

5

6

7

8

9

1

0

1

1

2

2

3

4

3

4

2

1

0

2

1

1

2

3

2

3

3

1

2

0

1

2

2

3

3

4

4

2

1

1

0

1

1

2

2

3

5

2

1

2

1

0

2

2

1

2

6

3

2

2

1

2

0

1

2

2

7

4

3

3

2

2

1

0

1

1

8

3

2

3

2

1

2

1

0

1

9

4

3

4

3

2

2

1

1

0

P

1

2

3

4

5

6

7

8

9

1

0

2

3

2

2

2

2

2

2

2

1

0

1

4

5

4

4

5

5

3

1

1

0

4

4

4

4

4

4

4

2

2

3

0

5

6

6

5

6

5

2

2

4

4

0

4

8

8

8

6

4

4

4

4

4

0

7

7

7

7

6

6

6

6

8

6

0

8

9

8

5

5

5

5

5

7

7

0

9

9

8

8

7

7

8

7

7

8

0

9.5. Graf nieskierowany o jednostkowych wagach krawędzi i wynikowe macierze D i P

drodze prowadzącej ze źródła do wierzchołków grafu (p[v] jest poprzednim w stosunku do v wierzchołkiem

na chwilowo najkrótszej drodze ze źródła do v).

W przedstawionym dalej algorytmie występuje operacja relaksacji, nazywana też osłabianiem ogra-

niczeń. Z każdym wierzchołkiem v

∈ V związany jest atrybut d[v], którego wartość jest zawsze górnym

ograniczeniem wagi najkrótszej drogi ze źródła s do wierzchołka v. Wielkość d[v] nazywana jest osza-

cowaniem wagi najkrótszej drogi. Relaksacja krawędzi (u, v) przy źródłowym wierzchołku s polega na

sprawdzeniu, czy przechodząc przez wierzchołek u, można znaleźć krótszą od dotychczasowej najkrót-

szej drogi z s do v i, jeśli taka możliwość istnieje, odpowiedniej aktualizacji d[v] oraz p[v]. Ewentualna

aktualizacja polega na zmniejszeniu oszacowania wagi najkrótszej drogi d[v] oraz zmianie poprzedni-

ka p[v]. Relaksacja krawędzi (u, v) jest realizowana przez procedurę relaksacja(u,v) przedstawioną jako

pseudokod 9.6..

Przykładowo, jeśli d[u] = 5, d[v] = 9 i krawędź (u, v) ma wagę 2, to po przeprowadzeniu relaksacji

relaksacja(u,v) zmniejszy się d[v] do 7, a p[v] stanie się równe u. Gdyby natomiast przed relaksacją d[v]

byłoby równe 6, to relaksacja niczego nie zmieni.

Początkowo przyjmujemy, że dla wszystkich wierzchołków v

∈ V poza źródłem d[v] = ∞, dla źródła

s ustalamy d[s] = 0. Dla wszystkich v

∈ V ustalamy p[v] = 0, co oznacza brak poprzednika (numer 0 nie

jest numerem żadnego wierzchołka ze zbioru V ).

W algorytmie Dijkstry pamiętany jest zbiór S zawierający te wierzchołki, dla których wagi naj-

background image

Rozdział 9: Algorytmy grafowe

121

1

procedure relax(u,v);

2

begin

3

f

u,v: nr wierzchołków

g

4

if d[u]+waga(u,v) < d[v] then

5

d[v] := d[u]+waga(u,v);

6

p[v] := u;

7

end if;

8

end.

krótszych dróg ze źródła s zostały już obliczone. Początkowo zbiór S jest pusty. Następnie wykonywane

są wielokrotnie następujące kroki:



wybór wierzchołka u nie należącego do S o minimalnym oszacowaniu wagi najkrótszej drogi,



dodanie wierzchołka u do S



wykonanie relaksacji dla wszystkich krawędzi o początku w wierzchołku u, czyli wychodzących z u.

Do pamiętania elementów zbioru V

− S można użyć kolejki priorytetowej, oznaczonej w przed-

stawianym algorytmie przez Q. Kluczami są tu wartości d. W implementacji zakłada się, że graf jest

reprezentowany w pamięci przez listy sąsiedztwa. Algorytm przedstawia pseudokod 9.6..

1

f

s: źródło

g

2

procedure dijkstra(V,E,s);

3

begin

4

d[v] :=

∞ dla v należących do V;

5

d[s] := 0;

6

p[v] := 0 dla v należących do V;

7

S := zbiór pusty;

8

Q := wszystkie wierzchołki ze zbioru V;

9

while kolejka Q nie jest pusta do

10

u := wierzchołek z Q o minimalnej wartości d;

11

S := S +

f

u

g

;

12

for lista wierzchołków v sąsiadujących z u

13

relax(u,v);

14

end for;

15

end while;

16

end.

Przykład:

background image

122

Analiza algorytmów - ćwiczenia

Przykładowy graf skierowany i przebieg algorytmu Dijkstry przedstawiono na rys. 9.6.

9.6. Przykładowy przebieg algorytmu Dijkstry. Źródłem jest wierzchołek s. Na rys. (a) tylko on ma

ustaloną wagę drogi, ale jeszcze nie należy do zbioru S. Jest wybierany jako wierzchołek o minimalnej

wadze spoza S i dołączany do S, co uwidoczniono na rys. (b). Wierzchołki należące do S oznaczono

na czarno. Teraz dokonywana jest relaksacja krawędzi wychodzących z s, a więc (s, u) oraz (s, x).

Pierwsza relaksacja powoduje przypisanie wierzchołkowi u wagi 10, ponieważ idąc do u poprzez s

krawędzią o koszcie 10 uzyskujemy łączny koszt 0 + 10 = 10, a 10 <

∞. Podobnie wierzchołek x

uzyskuje wagę 5. W kolejnym obiegu głównej iteracji znowu szukamy wierzchołka z minimalną wagą

spośród wierzchołków nie należących do S. Tym razem jest nim wierzchołek x o wadze 5. Dołączamy

go do zbioru S, co przedstawia rys. (c). Dokonujemy relaksacji krawędzi wychodzących z wierzchołka

x, w wyniku czego zmieniają się wagi wierzchołków u, v, y. W kolejnym obiegu wierzchołkiem o

minimalnej wadze, nie należącym do S jest y itd. Rys. (f) przedstawia ostateczny wynik działania

algorytmu, wszystkie wierzchołki należą do S.

Złożoność algorytmu

Oznaczmy liczbę wierzchołków w zbiorze V przez n, a liczbę krawędzi w zbiorze E przez e. I stotna

dla szybkości działania algorytmu Dijkstry jest implementacja kolejki priorytetowej Q. W najprost-

szym przypadku może ona być zaimplementowana jako tablica jednowymiarowa przeszukiwana liniowo.

Wtedy jednak każda operacja wyboru minimalnego elementu trwa O(n). Każdy z n wierzchołków gra-

fu przechodzi przez tę kolejkę, więc łącznie wybory zajmują czas O(n

2

). Każda z n list sąsiedztwa jest

analizowana dokładnie raz, jeden raz są też analizowane wszystkie krawędzie sąsiadujące z każdym wierz-

background image

Rozdział 9: Algorytmy grafowe

123

chołkiem. Czyli każda krawędź jest analizowana jeden raz, a więc ostatecznie algorytm działa w czasie

O(n

2

+ e) = O(n

2

), ponieważ w pesymistycznym przypadku liczba krawędzi jest rzędu O(n

2

)).

Dla grafów rzadkich efektywniejsze rozwiązanie uzyskuje się implementując kolejkę priorytetową

za pomocą kopca. Taki algorytm nazywany jest czasem zmodyfikowanym algorytmem Dijkstry. Kopiec

budowany jest w czasie O(n). Dalej następuje instrukcja iteracyjna wykonywana n razy (dla wszyst-

kich wierzchołków). Każda operacja wyboru minimalnego elementu zabiera czas O(log n), jest to czas

potrzebny na przywrócenie własności kopca. Łącznie operacje wyboru minimalnych elementów zajmują

czas O(n log n). Dla każdej krawędzi jest przeprowadzana operacja relaksacji, której wynikiem może być

zmiana pewnej wartości w kopcu. Przywrócenie własności kopca zajmuje czas O(log n). Krawędzi jest e,

więc ten etap zajmie czas O(e log n), a cały algorytm O(log n(n + e)).

Stosując tzw. kopiec Fibonacciego można uzyskać dla operacji przywracania własności kopca w

całym algorytmie czas O(1) i łączną złożoność O(n lg n + e).

Literatura

[1] Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Wprowadzenie do algorytmów, WNT,

Warszawa 1997

background image

124

Analiza algorytmów - ćwiczenia

background image

Rozdział 10

Funkcje mieszające

10.1.

Wprowadzenie

W wielu zastosowaniach wykorzystuje się struktury danych, na których są wykonywane tylko operacje

słownikowe: wstaw, usuń i szukaj. W kompilatorach języków programowania wykorzystuje się słowniki

symboli, w których kluczami są dowolne ciągi znaków odpowiadające identyfikatorom. Do reprezentacji

słowników doskonale nadają się tablice z mieszaniem będące uogólnieniem zwyczajnych tablic. Stoso-

wanie tablic z mieszaniem często jest określanie mianem: mieszania, rozpraszania lub hashowania (ang.

hashing). Mieszanie jest bardzo efektywną metodą, średni czas działania podstawowych operacji słowni-

kowych na tablicach z mieszaniem wynosi O(1)

10.2.

Tablice z adresowaniem bezpośrednim

Adresowanie bezpośrednie jest bardzo prostą i skuteczną metodą. Załóżmy, że wykorzystujemy słownik,

którego elementy mają klucze należące do uniwersum U =

{0, 1, 2, 3, ...m − 1}, gdzie m nie jest zbyt

duże. Przyjmijmy, że żadne dwa elementy nie mają identycznych kluczy. W takim przypadku słownik

możemy zrealizować za pomocą tablicy (z adresowaniem bezpośrednim) T [0..m

− 1], którą adresujemy

wartościami kluczy należącymi do uniwersum U .

Implementacja operacji słownikowych w takim przypadku jest trywialna:

125

background image

126

Analiza algorytmów - ćwiczenia

1

procedure wstaw adrbez(T, x);

2

begin

3

T [key(x)] := x;

4

end.

10.1. Algorytm wstawiania do tablicy z adresowaniem bezpośrednim

1

procedure usuń adrbez(T, x);

2

begin

3

T [key(x)] := nil;

4

end.

10.2. Algorytm usuwania z tablicy z adresowaniem bezpośrednim

10.3.

Tablice z mieszaniem

Adresowanie bezpośrednie ma poważną wadę: jeśli uniwersum kluczy U jest zbyt duże, to przechowy-

wanie w pamięci komputera tablicy T o rozmiarze

|U| może być nierealne. Dodatkowo zbiór K kluczy

elementów słownika może być tak mały, że większość pamięci zarezerwowanej dla tablicy T nie będzie

wykorzystana. Rozwiązaniem tego problemu jest zastosowanie tablicy z mieszaniem. W tablicy z adre-

sowaniem bezpośrednim element o kluczu k zostaje umieszczony na pozycji o indeksie k, natomiast w

tablicy z mieszaniem element ten zostaje umieszczony na pozycji o indeksie h(k). Funkcję h(k) nazywamy

funkcją mieszającą, która odwzorowuje uniwersum kluczy U na zbiór indeksów tablicy:

h : U

−→ {0, 1, 2, ...m − 1}

Ogromne oszczędności pamięci uzyskane dzięki zastosowaniu tablicy z mieszaniem okupione są

niebezpieczeństwem wystąpienia kolizji, tj. sytuacji, w której funkcja mieszająca przypisze dwu różnym

kluczom tę samą pozycję w tablicy. Oczywiście istnieją efektywne metody przezwyciężania kolizji, jednak

najlepiej byłoby w ogóle do kolizji nie dopuścić. Możemy to osiągnąć odpowiednio konstruując funkcję

mieszającą h(k).

Jeżeli zbiór K kluczy obiektów podlegających mieszaniu jest stały i z góry zadany, to można dla

takiego zbioru opracować doskonałą funkcję mieszającą h

d

(k) Funkcja h(k) jest doskonała jeśli jest

różnowartościowa:

1

procedure szukaj adrbez(T, k);

2

begin

3

return T [k];

4

end.

10.3. Algorytm szukania w tablicy z adresowaniem bezpośrednim

background image

Rozdział 10: Funkcje mieszające

127

k

1

,k

2

∈K

k

1

= k

2

=

⇒ h(k

1

)

= h(k

2

)

Przy użyciu takiej funkcji możliwe jest wyszukanie dowolnego obiektu w tablicy lub stwierdzenie,

że on w niej nie występuje w dokładnie jednej próbie. Doskonała funkcja mieszająca jest minimalna

jeżeli wszystkie wartości z

{0, 1, 2, ...m − 1} są wykorzystane, czyli:

|K| = n

Dla zbioru nazw funkcji standardowych języka BASIC: K =

{sin, cos, tan, atn, sqr, exp, log,

abs, int, sgn, rnd

} można metodą „prób i błędów” wyznaczyć minimalną doskonałą funkcję mieszającą

postaci:

a. h

d

(x

1

, x

2

, x

3

) = ˆ

x

1

+ ˆ

x

1

+ ˆ

x

2

b. h

d

(x

1

, x

2

, x

3

) = ˆ

x

1

+ ˆ

x

2

+ ˆ

x

2

c. h

d

(x

1

, x

2

, x

3

) = ˆ

x

2

+ ˆ

x

2

+ ˆ

x

3

d. h

d

(x

1

, x

2

, x

3

) = ˆ

x

2

+ ˆ

x

3

+ ˆ

x

3

gdzie x

1

, x

2

, x

3

oznaczają kolejne znaki słowa, natomiast ˆ

x

1

, ˆ

x

2

, ˆ

x

3

to kody znaków. Wyznaczenie funkcji

mieszającej oznacza przyporządkowanie znakom odpowiednich kodów. Zauważmy, że 0

 ˆx

i

 m−1 = 10

dla każdego znaku x

i

.

ad. a.h

d

(x

1

, x

2

, x

3

) = ˆ

x

1

+ ˆ

x

1

+ ˆ

x

2

Sprawdźmy jakie znaki występują na pozycjach 1 i 2:

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

1 2

1

1

1

1

1 3 1

2 1 1

1

1

2 2

1

1

1

Rozwiązaniem będzie tablica kodów znaków występujących na pozycji 1 i 2:

A B C E G I L N O Q R S T X

liczba wystąpień 3 1 1 1 1 2 1 2 2 1 1 3 2 1

kod

Tym znakom, które często występuja przypisujemy 0, na ich podstawie i tak trudno rozróżnić słowa,

a więc:

background image

128

Analiza algorytmów - ćwiczenia

A,I,N,O,S,T=0 T=1

N=3

C=2 Q=5 E=3,X=0 L=4 B=7 G=10 R=3

SIN

2S+I=0

0

COS 2C+O=?

4

4

TAN 2T+A=0(bląd) 2T+A=2

2

ATN

2A+T=1

1

SQR

2S+Q=?

5

5

EXP

2E+X=?

6

6

LOG

2L+O=?

8

8

ABS

2A+B=?

7

7

INT

2I+N=0(błąd) 2I+N=3

3

SGN

2S+G=?

10

10

RND

2R+N=?

9

9

Rozwiązaniem jest otrzymana tablica kodów znaków występujących na pozycji 1 i 2:

A B C E G I L N O Q R S T X

kod 0 7 2 3 10 0 4 3 0 5 3 0 1 0

ad. c.h

d

(x

1

, x

2

, x

3

) = ˆ

x

2

+ ˆ

x

2

+ ˆ

x

3

Sprawdźmy jakie znaki występują na pozycjach 2 i 3:

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

2 1 1

1

1

2 2

1

1

1

3

1

1

4

1

1 2 1

Liczba wystąpień znaków na pozycjach 2 i 3:

A B D G I N O P Q R S T X

liczba wystąpień 1 1 1 2 1 6 2 1 1 1 2 2 1

kod

Tym znakom, które często występuja przypisujemy 0, na ich podstawie i tak trudno rozróżnić słowa,

a więc:

G,N,O,S,T=0

N=3

G=2

I=3 A=1 Q=1,R=2 X=0,P=1 B=4 D=4

SIN

2I+N=?

9

9

COS 2O+S=0

0

TAN 2A+N=?

5

5

ATN 2T+N=0(błąd) 2T+N=3

3

SQR

2Q+R=?

4

4

EXP

2X+P=?

1

1

LOG

2O+G=0(błąd) 2O+G=2

2

ABS

2B+S=?

8

8

INT

2N+T=6

6

SGN

2G+N=7

7

RND

2N+D=?

10

10

background image

Rozdział 10: Funkcje mieszające

129

Rozwiązaniem jest otrzymana tablica kodów znaków występujących na pozycji 1 i 2:

A B D G I N O P Q R S T X

kod 1 4 4 2 3 3 0 1 1 2 0 0 0

b. h

d

(x

1

, x

2

, x

3

) = ˆ

x

1

+ ˆ

x

2

+ ˆ

x

2

Stosując metodę „prób i błędów” intuicyjnie postępowaliśmy według zasad wyszukiwania wyczerpujące-

go. Spróbujmy zastosować następującą wersję wyszukiwania wyczerpującego:

A S I N O T B C E G L Q R X S C T A S E L A I S R 1

3 3 2 2 2 2 1 1 1 1 1 1 1 1

I O A T Q X O B N G N 2

N S N N R P G S T N D 0

0 0 0 0

0

0

1 0 0

0

2

1

1 2

2

2 4

3 0

3 6

0

1

2

2 0

0

4

1 0 0

1

0

1

2

2

4

3

6

4 0

0

8

5 0

0

5

5 0

10

2

5 0

0

7

10

10

1 0

4 0

5 0

5 0

5 0

4

9

Podkreślenie oznacza wystąpienie kolizji.

Rozwiązaniem jest otrzymana tablica kodów znaków:

A S I N O T B C E G L Q R X

kod 0 0 0 1 0 2 3 3 1 4 5 5 5 4

Kolejność znaków, dla których wyszukujemy kody ma istotne znaczenie dla przebiegu wyszukiwania.

W powyższym przykładzie decyduje częstotliwość występowania znaków na znaczących pozycjach w

zbiorze słów. Inna metoda wyznaczenia kolejności znaków polega na przypisaniu znakom odpowiednich

wag:

waga znaku

=



współczynnik pozycji

× liczba wystąpień na pozycji

background image

130

Analiza algorytmów - ćwiczenia

dla funkcji postaci:

h

d

(x

1

, x

2

, x

3

) = ˆ

x

1

+ ˆ

x

1

+ ˆ

x

2

= 2

· ˆx

1

+ 1

· ˆx

2

+ 0

· ˆx

3

współczynnik znaku na pozycji pierwszej wynosi 2, na pozycji drugiej 1, trzeciej 0.

Obliczamy tabelę wag:

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

1

2

1

1

1

1

1 3 1

2

1 1

1

1

2 2

1

1

1

waga 5 1 2

2

1

3

2

2 2

1 2 6 3

1

Następnie sortujemy nie rosnąco:

S A I T C E L N O R B G Q X

waga 6 5 3 3 2 2 2 2 2 2 1 1 1 1

Następnie stosujemy wyszukiwanie wyczerpujące, tak jak w poprzednim przykładzie:

S A I T C E L N O R B G Q X S C T A S E L A I S R 2

6 5 3 3 2 2 2 2 2 2 1 1 1 1

I O A T Q X O B N G N 1

N S N N R P G S T N D 0

0 0 0 0

0

0

1 0 0 0 0

2 1

0

1

1

2

2

3 0

0

3

10

1 0

0

1

1

2

2

3 0

0

2

3

1

1

3

2

2

4

3

3

5

4 0

4

6

0

4 0

0

8

5 0

5

0

7 0

0

7

9 0

9 0

10

10

Otrzymujemy tablicę kodów znaków:

S A I T C E L N O R B G Q X

kod 0 0 0 1 0 0 1 3 4 4 5 7 9 10

Pewien wpływ na proces wyszukiwania ma kolejność słów, dla których sprawdzamy kolizje. Kolej-

background image

Rozdział 10: Funkcje mieszające

131

ność ta może zostać wyznaczona na podstawie wagi poszczególnych słów.

waga słowa =



współczynnik pozycji

× waga znaku

SIN

COS TAN ATN SQR EXP LOG ABS

INT

SGN RND

2

·6+3 2·2+2 2·3+5 2·5+3 2·6+1 2·2+1 2·2+1 2·5+1 2·3+2 2·6+1 2·2+1

15

6

11

13

13

5

6

11

8

13

6

Następnie stosujemy wyszukiwanie wyczerpujące, jak wyżej:

S A I T C E L N O R B G Q X S A S S T A I C L R E 2

6 5 3 3 2 2 2 2 2 2 1 1 1 1

I T Q G A B N O O N X 1

N N R N N S T S G D P 0

10.4.

Metody przezwyciężania kolizji

10.4.1.

Mieszanie otwarte - metoda „łańcuchowa”

Najprostszą metodą rozwiązywania kolizji jest metoda „łańcuchowa”. W metodzie łańcuchowej wszystkie

elementy, którym odpowiada ta sama pozycja w tablicy, umieszczone zostają na jednej liście. Początek

listy przechowywany jest w tablicy na wskazanej pozycji.

1

procedure wstaw miesz(T, x);

2

begin

f

wstaw x na początku listy T[h(key(x))]

g

3

end.

10.4. Algorytm wstawiania do tablicy z mieszaniem, z „łańcuchową” metodą przezwyciężania kolizji

1

procedure usuń miesz(T, x);

2

begin

f

usuń x z listy T[h(key(x))]

g

3

end.

10.5. Algorytm usuwania z tablicy z mieszaniem, z „łańcuchową” metodą przezwyciężania kolizji

Wstawianie elementów działa w czasie O(1) natomiast wyszukiwanie i usuwanie elementów działa

w czasie O(α), gdzie α nazywamy współczynnikiem zapełnienia:

α =

liczba elementów w tablicy

rozmiar tablicy

Jeżeli liczba elementów w tablicy z mieszaniem jest co najmniej proporcjonalna do rozmiaru tablicy,

to średni czas wyszukiwania i usuwania jest ograniczony przez stałą.

liczba element

ów w tablicy = O(rozmiar tablicy) =

⇒ α =

O(rozmiar tablicy)

rozmiar tablicy

= O(1)

background image

132

Analiza algorytmów - ćwiczenia

1

procedure szukaj miesz(T, k);

2

begin

f

wyszukaj element o kluczu k na liście T[h(k)]

g

3

end.

10.6. Algorytm szukania w tablicy z mieszaniem, z „łańcuchową” metodą przezwyciężania kolizji

10.4.2.

Mieszanie zamknięte

Kolejna metoda rozwiązywania kolizji określana jest mianem mieszania zamkniętego. W tej metodzie

wszystkie elementy przechowywane są wprost w tablicy. W odróżnieniu od metody „łańcuchowej” nie

ma żadnych dodatkowych list, żadne elementy nie są przechowywane poza tablicą. Wynika stąd, że dla

mieszania zamkniętego współczynnik zapełnienia nigdy nie przekroczy 1, α

 1 W mieszaniu zamkniętym

funkcja mieszająca zwraca ciąg wartości, permutację wszystkich pozycji w tablicy. W celu wstawienia

elementu do tablicy sprawdzamy kolejne pozycje z ciągu zwróconego przez funkcję mieszająca, aż do

znalezienia wolnej pozycji. Wyszukiwanie elementów polega na sprawdzeniu wszystkich pozycji tablicy

w kolejności określonej przez funkcję mieszającą. Zmodyfikowana funkcja mieszająca będzie teraz dwu-

argumentowa: h : U

× {0, 1, 2, ...m − 1} −→ {0, 1, 2, ...m − 1} h(k, i), gdzie k jest kluczem, a i określa

liczbę kolizji.

1

procedure wstaw zamk(T, x);

2

begin

3

i := 0;

4

repeat

5

j := h(key(x), i);

6

if T[j] = nil then

7

T[j] := k;

8

return;

9

end if;

10

i := i + 1;

11

until i=m;

f

błąd przepełnienie tablicy

g

12

end.

10.7. Algorytm wstawiania do tablicy z mieszaniem zamkniętym

Konstruując funkcję mieszającą dla mieszania zamkniętego dbamy aby, kolejność zwracanych po-

zycji zależała od wartości klucza. Zazwyczaj stosuje się jedną z trzech postaci funkcji mieszających:

background image

Rozdział 10: Funkcje mieszające

133

1

procedure usuń zamk(T, x);

2

begin

3

i := 0;

4

repeat

5

j := h(key(x), i);

6

if T[j] = x then

7

T[j] := nil;

8

return;

9

end if;

10

i := i + 1;

11

until i=m;

f

błąd brak elementu

g

12

end.

10.8. Algorytm usuwania z tablicy z mieszaniem zamkniętym

1

procedure szukaj zamk(T, k);

2

begin

3

i := 0;

4

repeat

5

j := h(k, i);

6

if key(T[j]) = k then

7

return T[j];

8

end if;

9

i := i + 1;

10

until i=m;

f

błąd brak elementu

g

11

end.

10.9. Algorytm szukania w tablicy z mieszaniem zamkniętym

background image

134

Analiza algorytmów - ćwiczenia



liniowa

h(k, i) = (h

1

(k) + i) mod m



kwadratowa

h(k, i) = (h

1

(k) + c

1

· i + c

2

· i

2

) mod m



dwukrotna

h(k, i) = (h

1

(k) + i

· h

2

(k)) mod m

Dla danej funkcji mieszającej h(k, i) = (k + i) mod m podaj postać tablicy mieszającej po wpro-

wadzeniu do niej następującej sekwencji kluczy: 11, 6, 88, 142, 23, 37, 96. Wyznaczyć liczbę prób, jaka

była potrzebna do wprowadzenia podanych kluczy do tablicy oraz spodziewaną liczbę prób wyznaczoną

teoretycznie.

próby

1

2

3

4

5

6

11

4

6

6

88

4

5

142

2

23

2

3

37

2

3

4

5

6

0

96

5

6

0

1

Wykonano 17 prób i otrzymano następujące wypełnienie tablicy:

0

37

1

96

2

142

3

23

4

11

5

88

6

6

Średnia liczba prób wstawienia i-ego klucza wynosi: E

śr

(i) =

1

1−α

=

1

1−

i−1

m

Średnia suma wykonanych prób:

7



i=1

E

śr

(i)

≈ 18, 15

background image

Rozdział 10: Funkcje mieszające

135

10.5.

Zadania

1. Dla danej funkcji mieszającej h(k, i) = (k + i + i

2

) mod m podaj postać tablicy mieszającej po

wprowadzeniu do niej następującej sekwencji kluczy: 44, 16, 39, 105, 23, 4, 80. Wyznaczyć liczbę

prób, jaka była potrzebna do wprowadzenia podanych kluczy do tablicy oraz spodziewaną liczbę

prób wyznaczoną teoretycznie.

Literatura

[1] Cormen, T.H., Leiserson C.E., Rivest R.L.: Wprowadzenie do algorytmów. Wydawnictwa Naukowo-

Techniczne, Warszawa 1997.

[2] Wirth, N.: Algorytmy + struktury danych = programy. Wydawnictwo Naukowo-Techniczne, War-

szawa 2000.

[3] Czech, Z.J.: Analiza algorytmów – materiały dydaktyczne, Gliwice, luty 1997.

Wersja 0.01 (2001-12-20) (AS)

background image

136

Analiza algorytmów - ćwiczenia

background image

Rozdział 11

Generowanie obiektów

kombinatorycznych

11.1.

Wprowadzenie

W wielu algorytmach zachodzi konieczność generowania i sprawdzenia wszystkich albo wybranych ele-

mentów pewnej klasy obiektów kombinatorycznych. W tym rozdziale przedstawiono metody generacji

permutacji, podzbiorów i kombinacji. Algorytmy generujące obiekty kombinatoryczne można podzielić na

trzy klasy:



generujące wszystkie obiekty danej klasy, każdy dokładnie jeden raz,



generujące losowy obiekt danej klasy w taki sposób, aby prawdopodobieństwo generowania każdego

obiektu było jednakowe,



generujące wybrany (k-ty) obiekt danej klasy.

Optymalny czas wykonania algorytmu generującego wszystkie obiekty danej klasy powinien być

rzędu liczby tych obiektów, tj. mocy zbioru tych obiektów. Taki algorytm nazywany jest liniowym. Czas

generacji pojedynczego obiektu nie powinien zależeć od jego numeru n. Wygenerowanie n-tego obiektu

jest zwykle bardziej kłopotliwe niż sukcesywne generowanie wszystkich obiektów.

Algoryty generujące wszystkie obiekty kombinatoryczne danej klasy składa się z reguy z trzech faz:



inicjalizacji, połączonej z wygenerowaniem pierwszego obiektu,



przetworzenia wygenerowanego obiektu,



o ile to możliwe, przekształcania bieżącego obiektu w następny.

137

background image

138

Analiza algorytmów - ćwiczenia

1

procedure generator;

2

begin

3

inicjalizacja(obiekt);

4

while not (ostatni element(obiekt)) do

5

przetwórz(obiekt);

f

wykonanie żądanych operacji na obiekcie

g

6

przekształć na następny(obiekt);

7

end while;

8

end.

Ogólną postać takich algorytmów przedstawia pseudokod 11.1..

Algorytmy generujące wszystkie obiekty mogą je generować w różnych uporządkowaniach. Jeśli

różnice między kolejnymi obiektami są minimalne, to algorytmy takie nazywane są algorytmami mini-

malnych zmian. Inne mogą generować obiekty uporządkowane leksykograficznie.

11.2.

Generowanie permutacji

Permutacje należą do często generowanych obiektów kombinatorycznych. Dowolną permutację elementów

zbioru

{a

1

, a

2

, . . . , a

n

} można zapisać jako ciąg indeksów tych elementów, czyli permutację zbioru liczb

naturalnych

{1, 2, . . . , n}. Dlatego w dalszym ciągu omówione zostaną algorytmy generujące właśnie

permutacje liczb naturalnych, co nie zmniejsza ogólności rozwiązania problemu.

11.2.1.

Permutacje w porządku minimalnych zmian

Minimalną zmianą prowadzącą do przekształcenia jednej permutacji w inną, jest pojedyncza transpozycja,

czyli zamiana pozycji pary elementów. Permutacje n elementów w takim porządku generuje się za pomocą

następującej metody rekurencyjnej:



Dla n = 1 permutacją jest (1).



Dla n > 1 tworzymy uporządkowaną listę permutacji n

− 1 elementów i oznaczamy je przez P

i

i = 1, . . . , (n

− 1)!.



Tworzymy uporządkowaną listę permutacji n elementów generując n nowych permutacji z każdej

permutacji P

i

przez wstawianie liczby n między elementy P

i

. Miejsc, w które można wstawić ten

nowy element jest n. Dla nieparzystych i element n wstawiamy od prawej do lewej strony P

i

, dla

parzystych od lewej do prawej.

background image

Rozdział 11: Generowanie obiektów kombinatorycznych

139

Na rys. 11.1 przedstawiono przykładowy przebieg algorytmu dla n = 4.

Taka procedura generuje permutacje w porządku minimalnych zmian metodą rekurencyjną, nie

znajdujc bezporednio tych zmian (tzn. wynikiem działania algorytmu są permutacje, a nie numery ele-

mentów do zamiany). Jest również możliwe iteracyjne generowanie permutacji.

11.2.2.

Permutacje w porządku leksykograficznym

Przyjmujemy, że permutacja (a

i

1

, a

i

2

, . . . , a

i

n

) poprzedza leksykograficznie permutację (a

j

1

, a

j

2

, . . . , a

j

n

)

wtedy i tylko wtedy, gdy dla pewnego k, 1

 k  n, i

p

= j

p

dla 1

 p < k i i

k

= j

k

. Na przykad w przy-

padku permutacji cyfr porządek leksykograficzny to taki porządek, w którym permutacje traktowane jako

reprezentacje liczb naturalnych uporządkowane są rosnąco. Dla trzech cyfr 1, 2, 3 będzie to porządek

123, 132, 213, 231, 312, 321.

Najistotniejszą częścią algorytmu generującego wszystkie permutacje jest sposób przekształcenia

bieżącej permutacji w następną. Można opisać ten sposób następująco:



Oznacz kolejne elementy bieżącej permutacji przez (a

1

, a

2

, . . . , a

n

).



Przeglądając elementy od prawej strony, tzn. od a

n

w kierunku a

1

, znajdź pierwszą pozycję, dla

której a

i

< a

i+1

.



Przeglądając elementy położone na prawo od a

i

znajdź element a

j

taki, że jest on najmniejszy

spośród elementów większych od a

i

.



Zamień miejscami element a

i

z a

j

.



Odwróć kolejność elementów (a

i+1

, . . . , a

n

). Elementy te zostaną uporządkowane rosnąco. (indeks

i nie ulega zamianie z j w poprzednim kroku).

Przykładowo, rozważmy permutację ośmiu elementów (1, 7, 8, 3, 6, 5, 4, 2). Przeglądamy elementy od

prawej do lewej i znajdujemy sytuację 3 < 6, czyli a

i

< a

i+1

dla i = 4. Następnie wśród elementów na

prawo od trójki szukamy większych od 3; znajdziemy 6, 5 i 4. Z tych elementów wybieramy najmniejszy.

Jest to czwórka znajdująca się na pozycji 7, czyli j = 7. Teraz zamieniamy j-ty element (czwórkę) z i-tym

(trójką), otrzymując (1, 7, 8, 4, 6, 5, 3, 2). Dalej odwracamy kolejność elementów leżących na prawo od i-

tego, czyli czwartego. Otrzymujemy permutację (1, 7, 8, 4, 2, 3, 5, 6), która jest szukaną kolejną permutacją

w porządku leksykograficznym następującą po (1, 7, 8, 3, 6, 5, 4, 2).

Generację permutacji w porządku leksykograficznym należy zakończyć wtedy, gdy po takim prze-

kształceniu otrzymamy permutację (n, n

− 1, . . . , 1), która jest ostatnią permutacją w tym porządku.

background image

140

Analiza algorytmów - ćwiczenia

11.2.3.

Generowanie n-tej permutacji

Czasem zachodzi konieczność wygenerowania pojedyczej permutacji o zadanym numerze bez generowania

permutacji ją poprzedzających. Numer jest indeksem permutacji w wybranym uporządkowaniu. Omówi-

my przykładową metodę stosująca poznane już wcześniej tablice inwersji. Tablica inwersji odpowiadająca

pewnej permutacji elementów 1, 2, . . . , n ma n elementów. Na i-tej pozycji tablicy znajduje się liczba

informująca, ile elementów permutacji znajdujących się na lewo od jej i-tego elementu jest większych

od tego elementu. Przykładowo, dla permutacji (4, 3, 5, 2, 1) tablica inwersji ma postać (0, 1, 0, 3, 4). Jeśli

numerujemy pozycje od 1 do n, to na i-tej pozycji tablicy inwersji mogą znaleźć się tylko całkowite

liczby nieujemne mniejsze od i. Łatwo policzyć, że różnych n-elementowych tablic inwersji jest dokładnie

n!, czyli tyle, ile jest permutacji n elementów. Istnieje wzajemnie jednoznaczne odwzorowanie zbioru

permutacji w zbiór tablic inwersji.

Wyznaczenie tablicy inwersji dla danej permuacji jest prostym zadaniem. Odwrotna operacja, wy-

znaczenie permutacji na podstawie tablicy inwersji, jest nieco trudniejsza. Elementy permutacji należy

uporządkować rosnąco: (1, 2, . . . , n). Szukaną permutację konstruuje się od ostatniego elementu. W ta-

blicy inwersji na ostatnim, n-tym miejscu znajduje się liczba t

n

= k informująca, ile elementów szukanej

permutacji na pozycjach od 1 do n

− 1 jest większych od n-tego elementu permutacji. Wobec tego na

n-tej pozycji permutacji musi znaleźć się liczba n

− k. Podobnie można znaleźć elementy n − 1, n − 2 itd.

aż do 1.

Pozostaje więc do rozwiązania wyznaczenie n-tej tablicy inwersji, czyli przekształcenie liczby n na

zawartość tablicy inwersji. Pomocne okazują się tu niejednorodne systemy pozycyjne. Przyzwyczajeni

jesteśmy do stosowania pozycyjnego systemu dziesiętnego, który jest systemem jednorodnym, tzn. wagi

kolejnych cyfr różnią się o stały czynnik, nazywany podstawą r (r > 1). Zapisując liczbę N jako ciąg

cyfr d

i

takich, że 0

 d

i

< r:

d

k

d

k−1

d

k−2

. . . d

0

,

d

k

= 0

wyznaczamy jej wartość następująco:

N = d

0

+ d

1

r + d

2

r

2

+ . . . + d

k

r

k

.

Dla systemu dziesiętnego r = 10.

W niejednorodnych systemach pozycyjnych zamiast r stosowany jest ciąg liczb naturalnych r

0

, r

1

, . . . , r

k−1

.

Wtedy podanemu wcześniej ciągowi odpowiada wartość

N = d

0

+ d

1

r

0

+ d

2

r

0

r

1

+ d

3

r

0

r

1

r

2

. . . + d

k

k−1



i=0

r

i

gdzie 0

 d

i

< r

i

, d

k

= 0 (za wyjątkiem przypadku N = 0, wtedy d

k

= 0). Przykładem takiego systemu

jest zapis czasu w formacie DHMS (dni, godziny, minuty, sekundy). Dla takiego formatu r

0

= 60, r

1

= 60,

r

2

= 24.

background image

Rozdział 11: Generowanie obiektów kombinatorycznych

141

W omawianym problemie wyznaczania tablic inwersji pomocny będzie tzw. silnia-system:

N = d

0

0! + d

1

1! + d

2

2! + . . . + d

k

k!

gdzie 0

 d

i

< i + 1, d

k

= 0 (za wyjątkiem przypadku N = 0), d

0

= 0 (zawsze); r

i

= i + 1. Cyfry tak

zapisanej liczby N są elementami N -tej tablicy inwersji. Algorytm przekształcania liczby N na liczbę w

niejednorodnym systemie pozycyjnym (w szczególności silnia-systemie) przedstawia pseudokod 11.2.3..

Tym sposobem mając dany numer permutacji n można najpierw znaleźć zapis n w silnia-systemie,

wypełnić tablicę inwersji „cyframi” tej liczby i na podstawie tablicy inwersji utworzyć permutację.

11.2.4.

Generowanie losowej permutacji

Znając metodę generowania k-tej permutacji, można wylosować liczbę z przedziału od 1 do n! (gdzie n jest

liczbą permutowanych elementów) i utworzyć k-tą permutację. Jeśli każdą liczbę generowaliśmy z takim

samym prawdopodobieństwem, to również każda permutacja jest wtedy jednakowo prawdopodobna.

Inny sposób to dokonanie n

− 1 transpozycji. Zaczynamy od permutacji o uporządkowanych rosną-

co elementach (1, 2, . . . , n). Zamieniamy n-ty element z losowo wybranym elementem spośród 1 do n.

Następnie element n

− 1 zamieniamy z jednym z elementów spośród 1 do n − 1 itd. Można wykazać, że

tutaj również wszystkie permutacje są jednakowo prawdopodobne.

11.3.

Generowanie podzbiorów

Podzbiorów zbioru n-elementowego jest 2

n

, licząc łącznie ze zbiorem pustym. Tworzenie podzbiorów

jest równoważne tworzeniu n-bitowych ciągów binarnych. Element a

i

należy do podzbioru wtedy i tylko

wtedy, gdy i-ty element ciągu jest jedynką.

11.3.1.

Wszystkie podzbiory

Zagadnienie generowania wszystkich podzbiorów sprowadza się do wygenerowania wszystkich n-bitowych

liczb dwójkowych.

11.3.2.

Losowe podzbiory

Generowanie losowego podzbioru to wylosowanie liczby z przedziału od 0 do 2

n

− 1 i zamiana tej liczby

na postać dwójkową.

Aby wygenerować k-elementowy, losowy podzbiór zbioru S =

{a

1

, a

2

, . . . , a

n

}, należy wyloso-

wać jeden z n elementów tego zbioru i uzupełnić losowym podzbiorem k

− 1 elementowym zbioru

background image

142

Analiza algorytmów - ćwiczenia

S pozbawionego wylosowanego elementu. Prawdopodobieństwo wylosowania k-elementowego podzbio-

ru

{a

i

1

, a

i

2

, . . . , a

i

k

} jest równe iloczynowi prawdopodobieństw zdarzeń:

Pr(pierwszy wybrany element jest jednym z

{a

i

1

, a

i

2

, . . . , a

i

k

}) =

k
n

Pr(drugi wybrany element jest jednym z pozostałych k

− 1 elementów) =

k−1
n−1

Pr(drugi wybrany element jest jednym z pozostałych k

− 2 elementów) =

k−2
n−2

. . .

Pr(k-ty wybrany element jest ostatnim pozostającym elementem) =

1

n−k+1

Iloczyn ten jest równy:

k

n

·

k

− 1

n

− 1

· . . . ·

1

n

− k + 1

=

k!(n

− k)!

n!

=

1

n
k

Ponieważ k-elementowych podzbiorów jest dokładnie

n
k

, więc każdy z nich zostanie wylosowany z jedna-

kowym prawdopodobieństwem. Metoda ta pozwala wygenerować k-elementowy podzbiór w czasie O(k).

11.4.

Generowanie kombinacji

Przez kombinacje rozumiemy podzbiory k-elementowe podzbiory zbioru n-elementowego. W sposób naiw-

ny można wygenerować wszystkie kombinacje generując wszystkie możliwe podzbiory (jak w poprzednim

punkcie) i wybierając spośród nich te, które mają dokładnie k elementów. Jednak w ten sposób zawsze

trzeba będzie przetworzyć 2

n

podzbiorów, tymczasem kombinacji jest tylko

n
k

.

11.4.1.

Porządek leksykograficzny

Przedstawimy metodę generowania kombinacji w porządku leksykograficznym. Kombinacja jest tu opi-

sywana przez ciąg numerów tych elementów zbioru, które należą do kombinacji. Na przykład, jeśli ele-

mentami zbioru będą liczby

{1, 2, 3, 4, 5, 6} i mają zostać wygenerowane trzyelementowe kombinacje, to

będą one miały następujący porządek:

123, 124, 125, 126,

134, 135, 136,

145, 146,

156,

234, 235, 236,

245, 246,

256,

345, 346,

356,

456.

background image

Rozdział 11: Generowanie obiektów kombinatorycznych

143

Poniższy algorytm pokazuje, jak przekształcić n-tą kombinację na (n + 1)-wszą:



Początkowa kombinacja to (1, 2, . . . , k).



Kolejną kombinację tworzymy z bieżącej w następujący sposób:

– Przeglądamy bieżącą kombinację od prawej do lewej strony. Znajdujemy element, który jeszcze

nie osiągnął maksymalnej wartości.

– Element ten zwiększamy o 1, a wszystkie elementy po jego prawej stronie otrzymują kolejno

najmniejsze z dopuszczalnych wartości.

Powracając do przykładu załóżmy, że bieżącą kombinacją jest 156. Aby wygenerować następną kombi-

nację, przeglądamy bieżącą kombinację od prawej strony, znajdując dopiero na pierwszej pozycji liczbę,

która nie osiągnęła jeszcze wartości maksymalnej, w tym przypadku 4. Zwiększamy ją o 1 uzyskując 2.

Elementy po prawej stronie, czyli na pozycjach 2 i 3, mają przyjąć najmniejsze możliwe wartości, czyli

3 i 4. Ostatecznie otrzymujemy kombinację 234.

11.4.2.

Porządek minimalnych zmian

Przy generowaniu kombinacji w porządku minimalnych zmian korzysta z kodu Gray’a. W zwykle stosowa-

nej odmianie jest to sekwencja ciągów binarnych, w której kolejne dwa ciągi binarne różnią się dokładnie

na jednej pozycji. Na przykład dla dwóch bitów może to być sekwencja:

00, 01, 11, 10

dla trzech bitów:

000, 001, 011, 010, 110, 111, 101, 100.

Zauważmy, że sekwencje takie można tworzyć rekurencyjnie: przy trzech bitach wystarczy przepisać ciąg

sekwencji dwubitowych najpierw w normalnym porządku dopisując na początku zera, potem w odwrot-

nym porządku dopisując jedynki. Możliwe jest też tworzenie sekwencji o długościach innych niż 2

n

, np.

przez znajdowanie zamkniętych dróg w tzw. tablicach Karnaugha.

Podczas generowania kombinacji korzysta się z nieco innego rodzaju kodu Gray’a. Kolejne cią-

gi różnią się na dokładnie dwóch bitach i mają zawsze tę samą liczbę jedynek, równą k. Koncepcja

generowania takich sekwencji jest podobna do przedstawionego wcześniej rekurencyjnego generowania

„zwykłych” kodów. Przez G(n, k) oznaczymy sekwencję ciągów n-bitowych o k jedynkach. Wtedy:

G(n, k) =


0

G(n

− 1, k)

1

G(n

− 1, k − 1)

R


 .

Zapis G(n

−1, k−1)

R

oznacza, że chodzi o sekwencję G(n

−1, k−1), ale w odwróconej kolejności. Zgodnie

z oznaczeniem, G(n, 0) jest (jednym) ciągiem n zer, natomiast G(n, n) (jednym) ciągiem n jedynek:

G(n, 0) = 0000 . . . 0,

background image

144

Analiza algorytmów - ćwiczenia

G(n, n) = 1111 . . . 1.

Zachęcamy Czytelnika do przeanalizowania tego wzoru rekurencyjnego na prostym przykładzie, np. dla

G(4, 2). Wynikową sekwencją jest:

0011, 0110, 0101, 1100, 1010, 1001.

Czytelnik zechce również sprawdzić, że kolejne ciągi binarne różnią się dokładnie na dwóch pozycjach.

11.4.3.

Generowanie i-tej kombinacji

Generowanie dowolnego podzbioru w czasie niezależnym od liczby wszystkich podzbiorów, a jedynie

od mocy podzbioru, jest przydatne zarówno przy generowaniu wszystkich podzbiorów, jak i losowego

podzbioru bądź pewnej liczby podzbiorów. Opisany dalej generator przekształca liczbę naturalną i



0,

n
k

− 1



na i-ty podzbiór w pewnym uporządkowaniu. Istnieje wiele metod generacji podzbiorów

w ten sposób, w tym rozdziale zostanie przedstawiona jedna z nich. Rys. 11.2 przedstawia tzw. trójkąt

Pascala. Potraktujmy elementy tego trójkąta jako wierzchołki grafu skierowanego. Łuki grafu łączą każdy

wierzchołek grafu tylko z bezpośrednio „nad nim” leżącymi wierzchołkami: dwoma, jednym lub żadnym

w przypadku najwyższego wierzchołka (łuki te nie zostały zaznaczone na rysunku). Nazwijmy najwyżej

leżący wierzchołek korzeniem. Zauważmy, że z wierzchołka znajdującego się na pozycji (n, k) istnieje

dokładnie

n
k

dróg prowadzących do korzenia, czyli tyle, ile wynosi wartość odpowiedniego elementu

trójkąta. Własność tę można udowodnić przez indukcję: jeśli była prawdziwa dla wierzchołków (n, k

− 1)

i (n, k), to będzie też prawdziwa dla wierzchołka (n + 1, k), ponieważ można z niego przejść do (n, k

− 1)

i dalej wykorzystać wszystkie ścieżki stamtąd do korzenia, albo do (n, k). Liczba dróg jest sumą liczb

dróg z tych dwóch wierzchołków i tyle jest też równa wartość elementu (n + 1, k):

n+1

k

=

n

k−1

+

n
k

dla k = 1, . . . , n.

Z każdą drogą prowadzącą z wierzchołka (n, k) do korzenia możemy związać dokładnie jeden k-

elementowy podzbiór zbioru n-elementowego. Aby ustalić, które spośród n elementów należą do pod-

zbioru, prześledźmy drogę z wierzchołka (n, k) do wierzchołka (0, 0). Przechodzimy więc z poziomu i = n

do poziomu i = 0. W każdym przypadku, kiedy przechodząc na poziom „wyższy” (z i do i

− 1) wybierze-

my lewy wierzchołek, przyjmiemy, że element i-ty należy do pozbioru. W przeciwnym razie nie należy.

Oczywiście „zakrętów w lewo” jest dokładnie tyle, ile jest równe k (rys. 11.2).

Pozostaje więc do rozwiązania numerowanie dróg w taki sposób, aby na podstawie numeru dało

się wygenerować drogę bez konieczności generowania wszystkich poprzednich dróg. Tutaj przyda się

poprzednia obserwacja: z wierzchołka (n, k) możemy przejść do wierzchołka (n

− 1, k − 1) lub (n − 1, k).

Przyjmiemy, że

n−1

k−1

początkowych numerów oznacza drogi skręcające do (n

−1, k−1), a pozostałe (czyli

n−1

k

numerów) drogi skręcające do (n

− 1, k). Mając numer drogi wystarczy więc porównać go z

n−1
k−1

i w zależności od wyniku porównania przejść do lewego lub prawego wierzchołka na wyższym poziomie.

Przy przechodzeniu do prawego wierzchołka należy numer drogi do dalszych obliczeń zmodyfikować tak,

background image

Rozdział 11: Generowanie obiektów kombinatorycznych

145

aby ścieżki wychodzące z tego wierzchołka były numerowane od jedynki (bądź zera, zależnie od założeń),

czyli odjąć od niego

n−1

k

. W ten sposób uzyskujemy algorytm przedstawiony jako pseudokod 11.4.3..

Zamiast wyznaczania

S

p

w każdym obiegu iteracji można również skorzystać z zależności:

n

− 1

k

=

n

k

·

n

− k

n

,

n

− 1

k

− 1

=

n

k

·

k

n

,

unikając w ten sposób wielokrotnego wyznaczania wartości

S

p

. Złożoność takiego algorytmu jest rzędu

O(n), czyli liczby elementów zbioru.

Literatura

[1] Edward M. Reingold, Jurg Nievergelt, Narsingh Deo, Alorytmy kombinatoryczne, Państwowe Wy-

dawnictwo Naukowe, Warszawa 1985

[2] Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Wprowadzenie do algorytmów, WNT,

Warszawa 1997

background image

146

Analiza algorytmów - ćwiczenia

1


1 2


1 2 3


1 2 3 4

1 2 4 3

1 4 2 3

4 1 2 3

1 3 2


4 1 3 2

1 4 3 2

1 3 4 2

1 3 2 4

3 1 2


3 1 2 4

3 1 4 2

3 4 1 2

4 3 1 2

2 1


3 2 1


4 3 2 1

3 4 2 1

3 2 4 1

3 2 1 4

2 3 1


2 3 1 4

2 3 4 1

2 4 3 1

4 2 3 1

2 1 3


4 2 1 3

2 4 1 3

2 1 4 3

2 1 3 4

11.1. Generowanie permutacji w porządku minimalnych zmian. Podkreślone zostały elementy wstawiane

w kolejnych krokach. Zależnie od parzystości numeru iteracji, elementy wstawiane są od prawej do

lewej bądź od lewej do prawej strony poprzedniej permutacji.

background image

Rozdział 11: Generowanie obiektów kombinatorycznych

147

1

procedure SilniaSystem;

2

begin

3

f

Dane wejściowe: liczba N

g

4

f

Dane wyjściowe: ciąg cyfr d

0

, d

1

, . . . , d

k

będący zapisem N w silnia-systemie

g

5

d

0

:= 0;

f

najstarsza cyfra zawsze równa 0

g

6

q := N ;

7

k := 0;

8

while q

= 0 do

9

d

k

:= q mod r

k

;

f

kolejna cyfra

g

10

q := q div r

k

;

11

k := k + 1;

12

end while;

13

if k

= 0 then

14

k := k

− 1;

15

end if;

16

end.

11.2. Generowanie kombinacji na podstawie trójkąta Pascala

background image

148

Analiza algorytmów - ćwiczenia

1

procedure gen komb(n, S, p: integer);

2

begin

3

f

n: nr kombinacji od 0, S: liczba elementów zbioru, p: liczba elementów podzb.

g

4

f

v, lewy r, prawy r, i: zmienne cakowite

g

5

n := n + 1;

f

...teraz kombinacje są numerowane od 1

g

6

i := 0;

f

nr pierwszego elementu zbioru

g

7

while S > 0 do

8

v :=

S

p

;

f

wartość bieżącego elementu trójkąta

g

9

prawy r :=

S−1

p

;

f

„prawy rodzic”

g

10

if p < S then

11

lewy r := v

− prawy r;

12

else

13

lewy r := 1;

f

„lewy rodzic”

g

14

end if;

15

S := S

− 1;

f

„piętro” w górę

g

16

if n > lewy r then

f

idziemy w prawo do góry

g

17

n := n

− lewy r;

18

writeln(’element ’,i, ’nie należy do podzbioru’);

19

else

f

idziemy w lewo do góry

g

20

writeln(’element ’,i, ’należy do podzbioru’);

21

p := p

− 1;

f

zmniejszamy liczbę elementów podzbioru („k”)

g

22

end if;

23

i := i + 1;

f

nr kolejnego elementu zbioru

g

24

end while;

25

end.

background image

Rozdział 12

Algorytmy geometryczne: wypukła

otoczka

12.1.

Wprowadzenie

Wypukłą otoczką zbioru punktów Q nazywamy najmniejszy wielokąt wypukły P taki, że każdy punkt

ze zbioru Q leży na brzegu P albo w jego wnętrzu. Rys. 12.1 przedstawia przykładowy zbiór punktów i

jego otoczkę. Wypukłą otoczkę zbioru Q oznaczamy przez CH(Q) (ang. convex hull).

12.1. Zbiór punktów Q wraz z zaznaczoną swoją wypukłą otoczką CH(Q). Źródło: [1].

Dalej zostaną przedstawione dwa algorytmy wyznaczające wierzchołki wypukłej otoczki w kolejności

przeciwnej do ruchu wskazówek zegara: algorytm Grahama działający w czasie O(n lg n) i algorytm

Jarvisa działający w czasie O(nh), gdzie n jest liczbą punktów zbioru Q, a h jest liczbą punktów na

otoczce.

W obu algorytmach stosuje się metodę zwaną „zamiataniem obrotowym”, polegającą na przeglą-

149

background image

150

Analiza algorytmów - ćwiczenia

daniu wierzchołków w kolejności wzrastających kątów, jaki tworzy ustalona półprosta z półprostą o

tym samym początku, przechodząca przez analizowany wierzchołek. Istnieją też inne metody, również

działające w czasie O(n lg n).

12.2.

Algorytm Grahama

Algorytm Grahama korzysta ze stosu S, zawierającego kandydatów na wierzchołki otoczki. Każdy

punkt zbioru Q jest raz wkładany na stos. Punkty nie będące wierzchołkami CH(Q) są podczas reali-

zacji algorytmu zdejmowane ze stosu. Po zakończeniu działania algorytmu na stosie S pozostaną tylko

wierzchołki CH(Q).

Algorytm działa dla co najmniej trzyelementowych zbiorów Q,

|Q|  3. Poza stadnardowymi funk-

cjami operującymi na stosie, Push(S) i Pop(S), używane są jeszcze funkcja Top(S) zwracająca element

na szczycie stosu S bez zdejmowania go ze stosu oraz funkcja Next to top(S), zwracająca element leżący

bezpośrednio pod szczytowym elementem stosu, również bez modyfikowania stanu S. Po zakończeniu

działania algorytmu stos S będzie zawierał (od dołu do góry) wszystkie wierzchołki CH(Q) w kolejności

przeciwnej do ruchu wskazówek zegara. Algorytm przedstawia pseudokod 12.2..

1

procedure Graham Scan(Q);

2

begin

3

znajdź w zbiorze Q punkt p

0

o minimalnej współrzędnej y;

4

jeśli jest więcej punktów o tej współrzędnej, wybierz punkt o najmniejszej współrzędnej x;

5

pozostałe punkty Q posortuj rosnąco ze względu na współrzędną kątową w biegunowym układzie współrzędnych

6

oznacz kolejno przez p

1

, p

2

, . . . , p

n−1

;

7

wyzeruj stos S;

8

Push(S, p

0

);

9

Push(S, p

1

);

10

Push(S, p

2

);

11

for i := 3 to n

− 1 do

12

while kąt (Next to top(S), Top(S), p

i

) nie oznacza skrętu w lewo do

13

Pop(S);

14

end while;

15

Push(S, p

i

);

16

end for;

17

return S;

f

w S jest szukana otoczka

g

18

end.

W jaki sposób sprawdzić, czy przechodząc z punktu A przez B do C skręcamy w lewo, opisano

background image

Rozdział 12: Algorytmy geometryczne: wypukła otoczka

151

w poprzedniej części ćwiczeń

1

. Działanie algorytmu Graham Scan dla przykładowego zbioru Q można

prześledzić na rysunkach 12.2 i 12.3.

Bieżący wynik, tzn. fragment otoczki opisany ciągiem punktów znajdujących się na stosie S, jest

zaznaczony na kolejnych rysunkach linią łamaną. Rysunek (a) przedstawia najniżej położony punkt

oznaczony p

0

i pozostałe, ponumerowane w kolejności rosnących kątów w biegunowym układzie współ-

rzędnych o początku w p

0

. Zaznaczone są też trzy początkowe punkty p

0

, p

1

i p

2

odłożone na stos S.

Rysunek (b) przestawia sytuację po próbie odłożenia na S kolejnego punktu, p

3

. Przed jego odłożeniem

sprawdzono, czy przechodząc z p

1

przez p

2

do p

3

skręcamy w lewo. Okazało się, że nie. Wobec tego ze

stosu został zdjęty jeden punkt (p

2

). Ponownie sprawdzono, czy przechodząc z przedostatniego punktu

na stosie (p

0

) przez ostatni (p

1

) do p

3

skręcamy w lewo. Tym razem okazało się, że tak. Dlatego punkt

p

2

został odłożony na stos. Kolejne rysunki (b-l) przedstawiają następne kroki algorytmu. Za każdym

razem, kiedy skręt nie jest skrętem w lewo (co powoduje usuwanie punktów ze stosu), zaznaczono odpo-

wiedni kąt przerywaną linią. Ostatni rysunek (l) przedstawia wynik działania algorytmu, czyli znalezioną

wypukłą otoczkę CH(Q).

Złożoność algorytmu Graham Scan

można wyznaczyć badając czas wykonania kolejnych eta-

pów algorytmu. Punkt o minimalnej współrzędnej y można znaleźć w czasie O(n). Sortowanie n punktów

wymaga czasu O(n lg n). Każdy punkt jest dokładnie raz odkładany na stos i najwyżej raz z niego zdej-

mowany. Związane z tym operacje, m.in. sprawdzanie kątów, łącznie zajmują czas O(n) (analizujemy

tu tzw. koszt zamortyzowany). Najwyższy rząd złożoności ma więc sortowanie, czyli rząd złożoności

całego algorytmu jest także równy O(n lg n).

12.3.

Algorytm Jarvisa

Zauważmy, że do znalezienia dwóch początkowych punktów CH(Q), tj. punktów p

0

i p

1

, wystarczyłoby

odnalezienie p

0

i wybór punktu o minimalnym kącie w biegunowym układzie współrzędnych (bez sorto-

wania). Algorytm Jarvisa w podobny sposób, nazywany owijaniem, znajduje kolejne punkty wypukłej

otoczki. Po znalezieniu punktu p

1

przesuwa do niego początek układu współrzędnych i ponownie wy-

szukuje wśród pozostałych punktów Q taki, który ma minimalną współrzędną biegunową. Znaleziony

zostanie p

2

. Tym sposobem znajdziemy prawą część CH(Q), tzn. część wypukłej otoczki łączącą „le-

woskrętnie” najniżej z najwyżej położonym punktem. Tę część nazwiemy prawym łańcuchem. Drugą

część CH(Q), lewy łańcuch, znajdziemy w podobny sposób, zaczynając od najwyżej położonego punk-

tu (oznaczmy go przez p

k

). Tym razem jednak kąty będziemy odmierzać od ujemnej części osi x. Możliwe

jest też połączenie obu etapów przyjmując, że ciąt kątów związanych z bokami otoczki musi być ściśle

rosnący (od 0 do 2π).

1

której tu jeszcze nie ma

background image

152

Analiza algorytmów - ćwiczenia

Przykładową realizację algorytmu Jarvisa przestawia rysunek 12.4.

Złożoność algorytmu Jarvisa jest rzędu O(nh), gdzie h jest liczbą punktów otoczki. Dla każdego

punktu otoczki znajdujemy wierzchołek o minimalnej współrzędnej kątowej, co wymaga czasu O(n).

Łącznie dla h punktów mamy więc O(nh).

Literatura

[1] Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Wprowadzenie do algorytmów, WNT,

Warszawa 1997

background image

Rozdział 12: Algorytmy geometryczne: wypukła otoczka

153

12.2. Działanie algorytmu Graham Scan, cz. 1. Źródło: [1].

background image

154

Analiza algorytmów - ćwiczenia

12.3. Działanie algorytmu Graham Scan, cz. 2. Źródło: [1].

background image

Rozdział 12: Algorytmy geometryczne: wypukła otoczka

155

12.4. Przykładowa realizacja algorytmu Jarvisa. Źródło: [1].

background image

156

Analiza algorytmów - ćwiczenia

background image

Spis rysunków

1.1

Świątynia Pura Ulu Danau, Bali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

10

1.2

Wieże w Hanoi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

10

1.3

Szacowanie od dołu wartości H

n

: ln n < H

n

. . . . . . . . . . . . . . . . . . . . . . . . . .

20

1.4

Szacowanie od góry wartości H

n

: H

n

< ln n + 1 . . . . . . . . . . . . . . . . . . . . . . . .

20

2.1

Algorytm znajdowania elementu minimalnego i maksymalnego (minmax1) . . . . . . . . .

26

2.2

Algorytm znajdowania elementu minimalnego i maksymalnego (minmax2) . . . . . . . . .

27

2.3

Algorytm znajdowania elementu minimalnego i maksymalnego (minmax3) . . . . . . . . .

29

2.4

Prosty algorytm sortowania . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

32

3.1

Algorytm sortowania przez proste wybieranie . . . . . . . . . . . . . . . . . . . . . . . . .

36

3.2

Ilustracja działania algorytmu sortowania przez proste wybieranie (pionowa kreska od-

dziela fragment tablicy, który jest już posortowany) . . . . . . . . . . . . . . . . . . . . . .

37

3.3

Ilustracja działania algorytmu sortowania przez proste wstawianie

. . . . . . . . . . . . .

38

3.4

Szkic algorytmu sortowania przez proste wstawianie

. . . . . . . . . . . . . . . . . . . . .

38

3.5

Algorytm sortowania przez proste wstawianie bez wartownika . . . . . . . . . . . . . . . .

39

3.6

Algorytm sortowania przez proste wstawianie z wartownikiem . . . . . . . . . . . . . . . .

41

3.7

I lustracja działania algorytmu sortowania Shella

. . . . . . . . . . . . . . . . . . . . . . .

43

3.8

Algorytm sortowania Shella . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

44

3.9

Algorytm sortowania bąbelkowego . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

47

3.10 Algorytm wyszukiwania binarnego . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

48

4.1

Ogólna wersja algorytmu sortowania szybkiego . . . . . . . . . . . . . . . . . . . . . . . .

52

4.2

I lustracja działania algorytmu sortowania szybkiego

. . . . . . . . . . . . . . . . . . . . .

52

4.3

Algorytm sortowania szybkiego . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

53

4.4

Algorytm wyboru k-tego co do wielkości elementu w oczekiwanym czasie liniowym . . . .

60

5.1

Rekurencyjny algorytm obliczania liczb Fibonacciego . . . . . . . . . . . . . . . . . . . . .

66

5.2

Drzewo wywołań procedury Fib1 dla parametru 6 . . . . . . . . . . . . . . . . . . . . . . .

67

157

background image

158

Analiza algorytmów - ćwiczenia

5.3

Nierekurencyjny algorytm wyznaczania liczb Fibonacciego . . . . . . . . . . . . . . . . . .

68

5.4

Algorytm wyznaczania długości najdłuższego wspólnego podciągu (NWP) . . . . . . . . .

70

5.5

I lustracja działania algorytmu wyznaczania NWP . . . . . . . . . . . . . . . . . . . . . . .

71

5.6

Algorytm drukowania najdłuższego wspólnego podciągu . . . . . . . . . . . . . . . . . . .

72

6.1

Przykładowe drzewo binarne. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

77

6.2

Przykładowe drzewo poszukiwań binarnych . . . . . . . . . . . . . . . . . . . . . . . . . .

78

6.3

Wyszukiwanie elementu o podanym kluczu w drzewie poszukiwań binarnych . . . . . . . .

79

6.4

Wyszukiwanie minimalnego elementu w drzewie poszukiwań binarnych . . . . . . . . . . .

79

6.5

Wyszukiwanie maksymalnego elementu w drzewie poszukiwań binarnych . . . . . . . . . .

80

6.6

Wyszukiwanie następnika elementu w drzewie przeszukiwań binarnych . . . . . . . . . . .

80

6.7

Wstawianie nowego elementu do drzewa poszukiwań binarnych. . . . . . . . . . . . . . . .

81

6.8

Niezrównoważone drzewo poszukiwań binarnych . . . . . . . . . . . . . . . . . . . . . . . .

82

6.9

Usuwanie klucza z drzewa poszukiwań binarnych. We wszystkich przypadkach usuwany

element jest zaznaczony jasno szarym kolorem, a element faktycznie wycinany (przypadek

c) jest zaznaczony ciemnoszarym kolorem. . . . . . . . . . . . . . . . . . . . . . . . . . . .

83

6.10 Usuwanie elementu z drzewa poszukiwań binarnych . . . . . . . . . . . . . . . . . . . . . .

84

6.11 Przykładowe drzewo czerwono-czarne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

86

6.12 Zamiany wykonywane przez rotacje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

86

6.13 Przykład działania operacji rotacji . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

87

6.14 Drzewo statystyk pozycyjnych . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

88

6.15 Operacja wyszukiwania elementu o zadanej randze . . . . . . . . . . . . . . . . . . . . . .

89

6.16 Wyznaczanie rangi elementu w dynamicznej statystyce pozycyjnej . . . . . . . . . . . . .

89

6.17 Zmiany wykonywane w trakcie rotacji. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

90

7.1

Szukanie wyjścia z labiryntu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

94

7.2

Ilustracja możliwości obcinania gałęzi drzewa wyszukiwania. Bez obcinania przeanalizowa-

ne musiałyby być wszystkie węzły. Obcięcia znacznie redukują liczbę węzłów, tym bardziej,

im wyżej (bliżej korzenia) są dokonywane. . . . . . . . . . . . . . . . . . . . . . . . . . . .

96

7.3

Przykładowe drzewo z odciętymi gałęziami i kolejność przeszukiwania węzłów drzewa przez

algorytm wyszukiwania wyczerpującego, zaznaczona przerywaną linią. . . . . . . . . . . .

97

7.4

Labirynt ilustrujący możliwość łączenia gałęzi drzewa wyszukiwania . . . . . . . . . . . . 100

7.5

Reorganizacja drzewa. Obydwa drzewa mają jednakową liczbę liści, ale drzewo (a) można

efektywniej przeszukać niż (b), ponieważ odcięcie gałęzi wyeliminuje naraz więcej liści. . . 101

7.6

Szacowanie wielkości drzewa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102

7.7

Fragmenty drzewa gry w kółko i krzyżyk. Źródło: [1]. . . . . . . . . . . . . . . . . . . . . . 104

7.8

Ilustracja α–β obcięcia. Źródło: [1]. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106

background image

Rozdział 12: Algorytmy geometryczne: wypukła otoczka

159

9.1

Dwie reprezentacje grafu nieskierowanego: (a) graf nieskierowany, (b) listy sąsiedztwa, (c)

macierz sąsiedztwa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115

9.2

Dwie reprezentacje grafu skierowanego: (a) graf skierowany, (b) listy sąsiedztwa, (c) ma-

cierz sąsiedztwa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115

9.3

Algorytm Floyda-Warshalla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118

9.4

Przykładowy przebieg algorytmu FW

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119

9.5

Graf nieskierowany o jednostkowych wagach krawędzi i wynikowe macierze D i P . . . . . 120

9.6

Przykładowy przebieg algorytmu Dijkstry. Źródłem jest wierzchołek s. Na rys. (a) tylko on

ma ustaloną wagę drogi, ale jeszcze nie należy do zbioru S. Jest wybierany jako wierzchołek

o minimalnej wadze spoza S i dołączany do S, co uwidoczniono na rys. (b). Wierzchołki

należące do S oznaczono na czarno. Teraz dokonywana jest relaksacja krawędzi wychodzą-

cych z s, a więc (s, u) oraz (s, x). Pierwsza relaksacja powoduje przypisanie wierzchołkowi

u wagi 10, ponieważ idąc do u poprzez s krawędzią o koszcie 10 uzyskujemy łączny koszt

0 + 10 = 10, a 10 <

∞. Podobnie wierzchołek x uzyskuje wagę 5. W kolejnym obiegu

głównej iteracji znowu szukamy wierzchołka z minimalną wagą spośród wierzchołków nie

należących do S. Tym razem jest nim wierzchołek x o wadze 5. Dołączamy go do zbioru S,

co przedstawia rys. (c). Dokonujemy relaksacji krawędzi wychodzących z wierzchołka x, w

wyniku czego zmieniają się wagi wierzchołków u, v, y. W kolejnym obiegu wierzchołkiem

o minimalnej wadze, nie należącym do S jest y itd. Rys. (f) przedstawia ostateczny wynik

działania algorytmu, wszystkie wierzchołki należą do S. . . . . . . . . . . . . . . . . . . . 122

10.1 Algorytm wstawiania do tablicy z adresowaniem bezpośrednim . . . . . . . . . . . . . . . 126

10.2 Algorytm usuwania z tablicy z adresowaniem bezpośrednim . . . . . . . . . . . . . . . . . 126

10.3 Algorytm szukania w tablicy z adresowaniem bezpośrednim . . . . . . . . . . . . . . . . . 126

10.4 Algorytm wstawiania do tablicy z mieszaniem, z „łańcuchową” metodą przezwyciężania

kolizji . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131

10.5 Algorytm usuwania z tablicy z mieszaniem, z „łańcuchową” metodą przezwyciężania kolizji131

10.6 Algorytm szukania w tablicy z mieszaniem, z „łańcuchową” metodą przezwyciężania kolizji132

10.7 Algorytm wstawiania do tablicy z mieszaniem zamkniętym

. . . . . . . . . . . . . . . . . 132

10.8 Algorytm usuwania z tablicy z mieszaniem zamkniętym . . . . . . . . . . . . . . . . . . . 133

10.9 Algorytm szukania w tablicy z mieszaniem zamkniętym . . . . . . . . . . . . . . . . . . . 133

11.1 Generowanie permutacji w porządku minimalnych zmian. Podkreślone zostały elementy

wstawiane w kolejnych krokach. Zależnie od parzystości numeru iteracji, elementy wsta-

wiane są od prawej do lewej bądź od lewej do prawej strony poprzedniej permutacji. . . . 146

11.2 Generowanie kombinacji na podstawie trójkąta Pascala . . . . . . . . . . . . . . . . . . . . 147

background image

160

Analiza algorytmów - ćwiczenia

12.1 Zbiór punktów Q wraz z zaznaczoną swoją wypukłą otoczką CH(Q). Źródło: [1]. . . . . . 149

12.2 Działanie algorytmu Graham Scan, cz. 1. Źródło: [1]. . . . . . . . . . . . . . . . . . . . . . 153

12.3 Działanie algorytmu Graham Scan, cz. 2. Źródło: [1]. . . . . . . . . . . . . . . . . . . . . . 154

12.4 Przykładowa realizacja algorytmu Jarvisa. Źródło: [1]. . . . . . . . . . . . . . . . . . . . . 155

background image

Spis tabel

2.1

Porównanie czasów działania algorytmów wyszukiwania minimum i maksimum (czasy sta-

nowią średnią ze 100 wykonań) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

31

3.1

Porównanie własności prostych algorytmów sortowania . . . . . . . . . . . . . . . . . . . .

45

3.2

Porównanie czasów działania prostych algorytmów sortowania (czasy stanowią średnią ze

100 wykonań) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

46

3.3

Porównanie czasów działania algorytmu sortowania Shella dla różnych ciągów przyrostów

46

4.1

Porównanie metod wyboru klucza osiowego dla algorytmu quick sort . . . . . . . . . . .

58

4.2

Porównanie wydajności algorytmów sortowania (czasy stanowią średnią ze 100 wykonań) .

58

161

background image

Skorowidz

algorytm

sortowania

stabilny, 35

zachowanie naturalne, 35

czynnik sumacyjny, 55

klucz osiowy, 51

sortowanie, 35

tablica inwersji, 27

162


Wyszukiwarka

Podobne podstrony:
Analiza algorytmow ukrywania w Nieznany
A V Aho, J E Hopcroft,J D Ullman Algorytmy Projektowanie I Analiza Algorytmow Komputerowych
Algorytmy ćwiczenie
Projektowanie i analiza algorytmow
Analizy - Gleba cwiczenia, OZNACZENIE SKŁADU GRANULOMETRYCZNEGO GLEBY
Analiza rynku cwiczenia I
Zestawienie tematów objętych zakresem egzaminu z budowy i analizy algorytmów
Analiza instrumentalna ćwiczenia, Zootechnika, Analiza instrumentalna
Analiza Algorytmów Genetycznych jako Ukladow Dynamicznych 08 Kotowski PhD p72
Analiza finansowa ćwiczenia
analiza algorytmow
Analiza żywnosci ćwiczenia 2
Wzory na kolokwium, Studia - Gospodarka Przestrzenna UEP, I stopień, V semestr, Analiza finansowa -
01. Próba statyczna rozciągania metali - sprawozdanie, Budownictwo - PG, IV semestr, Met. dośw. w an
ANALIZA ANIONÓW - ćwiczenia z 13.11, ANALIZA ANIONÓW
Analiza finansowa cwiczenia 2, Rachunek wynikow

więcej podobnych podstron