Programowanie dynamiczne, a problemy optymalizacyjne

Podobne dokumenty
Podstawowe struktury danych

Programowanie dynamiczne

Porządek symetryczny: right(x)

Programowanie dynamiczne

Ogólne wiadomości o grafach

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

Wykład 10 Grafy, algorytmy grafowe

Wysokość drzewa Głębokość węzła

Wykład X. Programowanie. dr inż. Janusz Słupik. Gliwice, Wydział Matematyki Stosowanej Politechniki Śląskiej. c Copyright 2016 Janusz Słupik

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

Podejście zachłanne, a programowanie dynamiczne

Poprawność semantyczna

Wykład 6. Drzewa poszukiwań binarnych (BST)

Drzewa poszukiwań binarnych

Wykład 2. Drzewa poszukiwań binarnych (BST)

Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych. Algorytmy i struktury danych Laboratorium 7. 2 Drzewa poszukiwań binarnych

Jeśli czas działania algorytmu zależy nie tylko od rozmiaru danych wejściowych i przyjmuje różne wartości dla różnych danych o tym samym rozmiarze,

Zadanie 1 Przygotuj algorytm programu - sortowanie przez wstawianie.

< K (2) = ( Adams, John ), P (2) = adres bloku 2 > < K (1) = ( Aaron, Ed ), P (1) = adres bloku 1 >

ZASADY PROGRAMOWANIA KOMPUTERÓW ZAP zima 2014/2015. Drzewa BST c.d., równoważenie drzew, kopce.

Programowanie dynamiczne i algorytmy zachłanne

Programowanie dynamiczne cz. 2

Drzewa binarne. Drzewo binarne to dowolny obiekt powstały zgodnie z regułami: jest drzewem binarnym Jeśli T 0. jest drzewem binarnym Np.

Wykład 2. Drzewa zbalansowane AVL i 2-3-4

Metody teorii gier. ALP520 - Wykład z Algorytmów Probabilistycznych p.2

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

Algorytmy i struktury danych. Co dziś? Tytułem przypomnienia metoda dziel i zwyciężaj. Wykład VIII Elementarne techniki algorytmiczne

Algorytmy i str ruktury danych. Metody algorytmiczne. Bartman Jacek

Techniki konstruowania algorytmów. Metoda dziel i zwyciężaj

Drzewo. Drzewo uporządkowane ma ponumerowanych (oznaczonych) następników. Drzewo uporządkowane składa się z węzłów, które zawierają następujące pola:

Strategia "dziel i zwyciężaj"

Drzewa spinające MST dla grafów ważonych Maksymalne drzewo spinające Drzewo Steinera. Wykład 6. Drzewa cz. II

EGZAMIN - Wersja A. ALGORYTMY I STRUKTURY DANYCH Lisek89 opracowanie kartki od Pani dr E. Koszelew

ALGORYTMY I STRUKTURY DANYCH

Drzewa BST i AVL. Drzewa poszukiwań binarnych (BST)

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

Modele i narzędzia optymalizacji w systemach informatycznych zarządzania

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

Algorytmy i Struktury Danych, 9. ćwiczenia

Drzewa poszukiwań binarnych

Algorytmy i struktury danych. Drzewa: BST, kopce. Letnie Warsztaty Matematyczno-Informatyczne

Algorytmika Problemów Trudnych

Algorytmy i Struktury Danych

Twój wynik: 4 punktów na 6 możliwych do uzyskania (66,67 %).

Tadeusz Pankowski

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

Podstawy Informatyki. Wykład 6. Struktury danych

Algorytmy i Struktury Danych

Wykład 8. Drzewo rozpinające (minimum spanning tree)

Teoretyczne podstawy informatyki

Podstawy Informatyki. Metody dostępu do danych

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

Drzewa wyszukiwań binarnych (BST)

Programowanie dynamiczne (optymalizacja dynamiczna).

PLAN WYKŁADU BAZY DANYCH INDEKSY - DEFINICJE. Indeksy jednopoziomowe Indeksy wielopoziomowe Indeksy z użyciem B-drzew i B + -drzew

Matematyczne Podstawy Informatyki

Matematyka dyskretna - 7.Drzewa

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

MACIERZE. Sobiesiak Łukasz Wilczyńska Małgorzata

ALGORYTMY I STRUKTURY DANYCH

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

LABORATORIUM 3 ALGORYTMY OBLICZENIOWE W ELEKTRONICE I TELEKOMUNIKACJI. Wprowadzenie do środowiska Matlab

Zaawansowane algorytmy i struktury danych

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

Wykład 8. Drzewa AVL i 2-3-4

Algorytmy i złożoności Wykład 5. Haszowanie (hashowanie, mieszanie)

prowadzący dr ADRIAN HORZYK /~horzyk tel.: Konsultacje paw. D-13/325

Grafy (3): drzewa. Wykłady z matematyki dyskretnej dla informatyków i teleinformatyków. UTP Bydgoszcz

Każdy węzeł w drzewie posiada 3 pola: klucz, adres prawego potomka i adres lewego potomka. Pola zawierające adresy mogą być puste.

Uwaga: Funkcja zamień(a[j],a[j+s]) zamienia miejscami wartości A[j] oraz A[j+s].

Algorytmy grafowe. Wykład 1 Podstawy teorii grafów Reprezentacje grafów. Tomasz Tyksiński CDV

ANALIZA ALGORYTMÓW. Analiza algorytmów polega między innymi na odpowiedzi na pytania:

Wyszukiwanie w BST Minimalny i maksymalny klucz. Wyszukiwanie w BST Minimalny klucz. Wyszukiwanie w BST - minimalny klucz Wersja rekurencyjna

Język ludzki kod maszynowy

Przypomnij sobie krótki wstęp do teorii grafów przedstawiony na początku semestru.

Analiza algorytmów zadania podstawowe

Obliczenia iteracyjne

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

Struktury danych i złozoność obliczeniowa. Prof. dr hab. inż. Jan Magott

Złożoność obliczeniowa zadania, zestaw 2

Instrukcje dla zawodników

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

WSTĘP DO INFORMATYKI. Drzewa i struktury drzewiaste

Sortowanie bąbelkowe

Podstawy Informatyki. Sprawność algorytmów

Egzaminy i inne zadania. Semestr II.

Algorytmy i Struktury Danych.

. Podstawy Programowania 2. Drzewa bst - część druga. Arkadiusz Chrobot. 12 maja 2019

Układy równań i nierówności liniowych

do instrukcja while (wyrażenie);

Teoretyczne podstawy informatyki

Programowanie liniowe

ZAGADNIENIE TRANSPORTOWE

Projektowanie i analiza algorytmów

Wykład 1_2 Algorytmy sortowania tablic Sortowanie bąbelkowe

Algorytmy i Struktury Danych.

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

Programowanie i struktury danych

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

TEORETYCZNE PODSTAWY INFORMATYKI

Transkrypt:

Programowanie dynamiczne, a problemy optymalizacyjne W procesie opracowywania algorytmu programowania dynamicznego dla problemu optymalizacji wyróżniamy etapy: Etap1. Określenie właściwości rekurencyjnej, która daje rozwiązanie optymalne dla realizacji problemu. Etap2. Obliczenie wartości rozwiązania optymalnego w porządku wstępującym. Etap3. Skonstruowanie rozwiązania optymalnego w porządku wstępującym Nie jest prawdą, że problem optymalizacji może zawsze zostać rozwiązany przy użyciu programowania dynamicznego. Aby tak było w problemie musi mieć zastosowanie zasada optymalności. Zasada optymalności. Zasada optymalności ma zastosowanie w problemie wówczas, gdy rozwiązanie optymalne realizacji problemu zawsze zawiera rozwiązania optymalne dla wszystkich podrealizacji. Zasada optymalności w problemie najkrótszej drogi: jeżeli ν k jest wierzchołkiem należącym do drogi optymalnej z ν i do ν j, to poddrogi z ν i do ν k oraz z ν k do ν j również muszą być optymalne. Optymalne rozwiązanie realizacji zawiera rozwiązania optymalne wszystkich podrealizacji. Jeżeli zasada optymalności ma zastosowanie w przypadku danego problemu, to można określić właściwość rekurencyjną, która będzie dawać optymalne rozwiązanie realizacji w kontekście optymalnych rozwiązań podrealizacji.

W praktyce zanim jeszcze założy się, że rozwiązanie optymalne może zostać otrzymane dzięki programowaniu dynamicznemu trzeba wykazać, że zasada optymalności ma zastosowanie. Przykład. Rozważmy problem znalezienia najdłuższych! prostych dróg wiodących od każdego wierzchołka do wszystkich innych wierzchołków. Ograniczamy się do prostych dróg, ponieważ w przypadku cykli zawsze możemy utworzyć dowolnie długą poprzez powtórne przechodzenie przez cykl. Optymalna (najdłuższa) prosta droga z ν 1 do ν 4 to [ν 1, ν 3, ν 2, ν 4 ]. Poddroga [ν 1, ν 3 ] nie jest optymalną drogą z ν 1 do ν 3 ponieważ długość[ν 1, ν 3 ] = 1 < długość[ν 1, ν 2,ν 3 ] = 4 Zatem zasada optymalności nie ma zastosowania. Wynika to z faktu, że optymalne drogi ν 1 do ν 3 oraz z ν 3 do ν 4 nie mogą zostać powiązane, aby utworzyć optymalną drogę z ν 1 do ν 4. W ten sposób utworzymy cykl, nie optymalną drogę.

Łańcuchowe mnożenie macierzy Załóżmy, że chcemy pomnożyć macierze o wymiarach 2x3 i 3x4 w następujący sposób: 1 2 3 7 8 9 1 29 35 41 38 4 5 6 x 2 3 4 5 = 74 89 104 83 = C(2,4) 6 7 8 9 Macierz wynikowa ma rozmiary 2x4. Każdy element można uzyskać poprzez 3 mnożenia, przykładowo C(1,1) = 1x7 + 2x2 + 3x6 W iloczynie macierzy występuje 2x4=8 pozycji wiec całkowita liczba elementarnych operacji mnożenia wynosi 2x4x3 = 24. Ogólnie w celu pomnożenia macierzy o wymiarach i x j przez macierz o wymiarach j x k standardowo musimy wykonać i x j x k elementarnych operacji mnożenia. Weźmy mnożenie: A x B x C x D 20x2 2x30 30x12 12x8 Mnożenie macierzy jest łączne. Może być realizowane przykładowo: Ax(Bx(CxD)) lub (AxB)x(CxD). Istnieje 5 różnych kolejności w których można pomnożyć 4 macierze : Ax(Bx(CxD)) = 30x12x8 + 2x30x8 +20x2x8 = 3680 (AxB)x(CxD) = 20x2x30 + 30x12x8 +20x30x8 = 8880 Ax((BxC)xD) = 2x30x12 + 2x12x8 + 20x2x8 = 1232 ((AxB)xC)xD = 20x2x30 + 20x30x12 + 20x12x8 = 10320 (Ax(BxC))xD = 2x30x12 + 20x2x12 + 20x12x8 = 3120.

Każde mnożenie 4 macierzy wymaga innej liczby elementarnych operacji mnożenia. Trzecia kolejność jest optymalna. Zadanie: Opracować algorytm określający optymalną kolejność mnożenia n macierzy. Kolejność mnożenia macierzy zależy tylko od rozmiarów macierzy. Dane: Ilość macierzy n oraz rozmiary macierzy. Algorytm metodą siłową - rozważenie wszystkich kolejności i wybranie minimum. Czas wykonania algorytmu siłowego. Niech t n będzie liczbą różnych kolejności mnożenia n macierzy: A 1,A 2,,A n. Weźmy podzbiór kolejności dla których macierz A 1 jest ostatnią mnożoną macierzą. W podzbiorze tym mnożymy macierze od A 2 do A n, liczba różnych kolejności w tym podzbiorze wynosi t n-1 : A 1 x (A 2 A n ) _ t n-1 różnych możliwości Drugi podzbiór jest zbiorem kolejności, w przypadkach w których macierz A n jest ostatnią mnożoną macierzą. Liczba kolejności w tym podzbiorze również wynosi t n-1. Zatem dla n macierzy: t n t n-1 + t n-1 = 2 t n-1 Natomiast dla 2 macierzy: t 2 = 1 Korzystając z rozwiązania równania rekurencyjnego: t n 2 n-2

Dla tego problemu ma zastosowanie zasada optymalności, tzn. optymalna kolejność mnożenia n macierzy zawiera optymalną kolejność mnożenia dowolnego podzbioru zbioru n macierzy. Przykładowo, jeżeli optymalna kolejność mnożenia 6 macierzy jest: to A 1 ((((A 2 A 3 )A 4 )A 5 )A 6 ) (A 2 A 3 )A 4 Musi być optymalną kolejnością mnożenia macierzy od A 2 do A 4. Ponieważ mnożymy (k-1)-szą macierz, A k-1, przez k-tą macierz, A k, liczba kolumn w A k-1 musi być równa liczbie wierszy w A k. Przyjmując, że d 0 jest liczbą wierszy w A 1, zaś d k jest liczbą kolumn w A k dla 1 k n, to wymiary A k będą wynosić d k-1 x d k. Do rozwiązania problemu wykorzystamy sekwencje tablic dla 1 i j n : M[i][j] = minimalna liczba mnożeń wymaganych do pomnożenia macierzy od A i do A j, jeżeli i < j M[i][i] = 0

Przykład (6 macierzy): A 1 x A 2 x A 3 x A 4 x A 5 x A 6 5x2 2x3 3x4 4x6 6x7 7x8 d 0 d 1 d 1 d 2 d 2 d 3 d 3 d 4 d 4 d 5 d 5 d 6 Dla pomnożenia macierzy A 4, A 5, A 6 możemy określić dwie kolejności oraz liczby elementarnych operacji mnożenia: (A 4 A 5 )A 6 Liczba operacji mnożenia = d 3 x d 4 x d 5 + d 3 x d 5 x d 6 = 4 x 6 x 7 + 4 x 7 x 8 = 392 A 4 (A 5 A 6 ) Liczba operacji mnożenia = d 4 x d 5 x d 6 + d 3 x d 4 x d 6 = 6 x 7 x 8 + 4 x 6 x 8 = 528 Stąd: M[4][6] = minimum(392,528) = 392 Optymalna kolejność mnożenia 6 macierzy musi mieć jeden z rozkładów: 1. A 1 (A 2 A 3 A 4 A 5 A 6 ) 2. (A 1 A 2 )(A 3 A 4 A 5 A 6 ) 3. (A 1 A 2 A 3 )(A 4 A 5 A 6 ) 4. (A 1 A 2 A 3 A 4 )(A 5 A 6 ) 5. (A 1 A 2 A 3 A 4 A 5 )A 6 gdzie iloczyn w nawiasie jest uzyskiwany zgodnie z optymalna kolejnością. Liczba operacji mnożenia dla k-tego rozkładu jest minimalną liczba potrzebną do otrzymania każdego czynnika, powiększoną o liczbę potrzebną do pomnożenia dwóch czynników:

Zatem: M[1][k] + M[k+1][6] + d 0 d k d 6 M[1][6] = minimum(m[1][k] + M[k+1][6] + d 0 d k d 6 ) 1 k 5 Uogólniając ten rezultat w celu uzyskania właściwości rekurencyjnej, związanej z mnożeniem macierzy dostajemy (dla 1 i j n ) : M[i][j] = minimum(m[i][k] + M[k+1][j] + d i-1 d k d j ) i k j-1 M[i][i] = 0 Algorytm typu dziel i zwyciężaj oparty na tej właściwości jest wykonywany w czasie wykładniczym. Można jednak przedstawić wydajniejszy algorytm dynamiczny liczący M[i][j] w kolejnych etapach. Używamy siatkę podobną do trójkąta Pascala. Element M[i][j] jest obliczany: - na podstawie wszystkich wpisów ze swojego wiersza znajdujących się po jego lewej stronie - wpisów ze swojej kolumny, znajdujących się poniżej niego Algorytm: - ustawiamy wartość elementów na głównej przekątnej na 0 - obliczamy wszystkie elementy na przekątnej powyżej (przekątna 1) - obliczamy wszystkie wartości na przekątnej 2 - kontynuujemy obliczenia aż do uzyskania jedynej wartości na przekątnej 5, która jest odpowiedzią końcową M[1][6]

Przykład (6 macierzy) Obliczamy przekątną 0: M[i][i] = 0 dla 1 i 6 Obliczamy przekątną 1: M[1][2] = minimum( M[1][k] + M[k+1][2] + d 0 d k d 2 ) 1 k 1 = M[1][1] + M[2][2] + d 0 d 1 d 2 = 0 + 0 + 5 x 2 x 3 = 30 Wartości M[2][3], M[3][4], M[4][5], M[5][6] liczymy podobnie.

Obliczamy przekątną 2: M[1][3] = minimum( M[1][k] + M[k+1][3] + d 0 d k d 3 ) 1 k 2 = minimum( M[1][1] + M[2][3] + d 0 d 1 d 3, M[1][2] + M[3][3] + d 0 d 2 d 3 ) =minimum( 0 + 24 + 5 x 2 x 4, 30 + 0 + 5 x 3 x 4 )=64 Wartości M[2][4], M[3][5], M[4][6] liczymy podobnie. Obliczamy przekątną 3: M[1][4] = minimum( M[1][k] + M[k+1][4] + d 0 d k d 4 ) 1 k 3 = minimum( M[1][1] + M[2][4] + d 0 d 1 d 4, M[1][2] + M[3][4] + d 0 d 2 d 4, M[1][3] + M[4][4] + d 0 d 3 d 4 ) = minimum( 0 + 72 + 5 x 2 x 6, 30 + 72 + 5 x 3 x 6, 64 + 0 + 5 x 4 x 6 ) = 132 Wartości M[2][5], M[3][6] liczymy podobnie. Przekątną 4 liczymy podobnie : M[1][5] = 226 Przekątną 5 liczymy podobnie : M[1][6] = 348 Algorytm: minimalna liczba operacji mnożenia Problem : określić minimalną liczbę elementarnych operacji mnożenia, wymaganych w celu pomnożenia n macierzy oraz kolejność wykonywania mnożeń, która zapewnia minimalną liczbę operacji. Dane: liczba macierzy n oraz tablica liczb całkowitych d, indeksowana od 0 do n, gdzie d[i-1] x d[i] jest rozmiarem i-tej macierzy.

Wynik : minmult minimalna liczba elementarnych operacji mnożenia, wymaganych w celu pomnożenia n macierzy; dwuwymiarowa tablica P, na podstawie której można określić optymalną kolejność. P[i][j] jest punktem, w którym macierze od i do j zostaną rozdzielone w kolejności optymalnej dla mnożenia macierzy. int minmult(int n, const int d[], index P[][]) { index i,j,k,diagonal; int M[1..n][1..n]; for (i=1; i n; i++) M[i][i]=0; for (diagonal = 1; diagonal n-1; diagonal++) for (i=1; i n - diagonal; i++) { j=i+diagonal; M[i][j]= minimum (M[i][k]+M[k+1][j]+ i k j-1 d[i-1]*d[k]*d[j] ) P[i][j] = wartość k, która dała minimum } return M[1][n]; } Złożoność czasowa minimalna liczba operacji mnożenia. Operacją podstawową są instrukcje wykonywane dla każdej wartości k, w tym sprawdzenie czy wartość jest minimalna. Rozmiar danych: n- liczba macierzy do pomnożenia. Mamy do czynienia z pętlą w pętli. Ponieważ j = i + diagonal dla danych diagonal oraz i. Liczba przebiegów pętli k wynosi

j 1 i + 1 = i + diagonal 1 i + 1 = diagonal Dla danej wartości diagonal liczba przebiegów pętli for-i wynosi n- diagonal. Diagonal może przyjmować wartości od 1 do n - 1, całkowita liczba powtórzeń operacji podstawowej wynosi n-1 Σ [(n diagonal) x diagonal]= n(n-1)(n+1)/6 Θ(n 3 ) diagonal=1 Przykład: P[2][5] = 4 oznacza optymalną kolejność mnożenia (A 2 A 3 A 4 ) A 5 Punkt 4 jest punktem rozdzielenia macierzy w celu otrzymania czynników. Mając tablicę P: 1 2 3 4 5 6 1 1 1 1 1 1 2 2 3 4 5 3 3 4 5 4 4 5 5 5 możemy odczytać: P[1][6] = 1 -> A 1 (A 2 A 3 A 4 A 5 A 6 ) P[2][6] = 5 -> A 1 ((A 2 A 3 A 4 A 5 ) A 6 ) P[2][5] = 4 -> A 1 (((A 2 A 3 A 4 ) A 5 ) A 6 ) P[2][4] = 3 -> A 1 ((((A 2 A 3 ) A 4 ) A 5 ) A 6 )

Algorytm: wyświetlanie optymalnej kolejności Problem: wyświetlić optymalną kolejność dla mnożenia n macierzy. Dane: dodatnia liczba całkowita n oraz tablica P Wynik: optymalna kolejność mnożenia macierzy void order(index i, index j) { if (i==j) cout << A << i; else { k = P[i][j]; cout << ( ; order(i,k); order(k+1,j); cout << ) ; } }

Optymalne drzewa wyszukiwania binarnego Opracowujemy algorytm określania optymalnego sposobu zorganizowania zbioru elementów w postaci drzewa wyszukiwania binarnego. Dla każdego wierzchołka w drzewie binarnym poddrzewo, którego korzeniem jest lewy (prawy) potomek tego wierzchołka, nosi nazwę lewego (prawego) poddrzewa wierzchołka. Lewe (prawe) poddrzewo korzenia drzewa nazywamy lewym (prawym) poddrzewem drzewa. Drzewo wyszukiwania binarnego. Drzewo wyszukiwania binarnego jest binarnym drzewem elementów (nazywanych kluczami) pochodzących ze zbioru uporządkowanego. Musi spełniać warunki: 1. Każdy wierzchołek zawiera jeden klucz. 2. Każdy klucz w lewym poddrzewie danego wierzchołka jest mniejszy lub równy kluczowi tego wierzchołka. 3. Klucze znajdujące się w prawym poddrzewie danego wierzchołka są większe lub równe kluczowi tego wierzchołka.

Przykład. Dwa drzewa o tych samych kluczach. W lewym drzewie prawe poddrzewo wierzchołka Rudolf zawiera klucze (imiona) Tomasz, Urszula, Waldemar wszystkie większe od Rudolf zgodnie z porządkiem alfabetycznym. Zakładamy, że klucze są unikatowe. Głębokość wierzchołka w drzewie jest liczbą krawędzi w unikatowej drodze, wiodącej od korzenia do tego wierzchołka, inaczej zwana poziomem wierzchołka w drzewie. Głębokość drzewa to maksymalna głębokość wszystkich wierzchołków (w przykładzie - drzewo po lewej głębokość 3, po prawej głębokość 2) Drzewo nazywane jest zrównoważonym, jeżeli głębokość dwóch poddrzew każdego wierzchołka nigdy nie różni się o więcej niż 1 (w przykładzie lewe drzewo nie jest zrównoważone, prawe jest zrównoważone).

Zwykle drzewo wyszukiwania binarnego zawiera pozycje, które są pobierane zgodnie z wartościami kluczy. Celem jest takie zorganizowanie kluczy w drzewie wyszukiwania binarnego, aby średni czas zlokalizowania klucza był minimalny. Drzewo zorganizowane w ten sposób jest nazywane optymalnym. Jeżeli wszystkie klucze charakteryzuje to samo prawdopodobieństwo zostania kluczem wyszukiwania, to drzewo z przykładu (prawe) jest optymalne. Weźmy przypadek, w którym wiadomo, że klucz wyszukiwania występuje w drzewie. Aby zminimalizować średni czas wyszukiwania musimy określić złożoność czasową operacji lokalizowania klucza. Algorytm wyszukiwania klucza w drzewie wyszukiwania binarnego Wykorzystujemy strukturę danych: struct nodetype { keytype key; nodetype* left; nodetype* right; }; typedef nodetype* node_pointer; Zmienna typu node_pointer jest wskaźnikiem do rekordu typu nodetype. Problem: określić wierzchołek zawierający klucz w drzewie wyszukiwania binarnego, zakładając że taki występuje w drzewie. Dane: wskaźnik tree do drzewa wyszukiwania binarnego oraz klucz keyin. Wynik: wskaźnik p do wierzchołka zawierającego klucz.

void search(node_pointer tree, keytype keyin, node_pointer& p) { bool found; } p = tree; found = false; while (!found) if (p->key == keyin) found = true; else if (keyin < p->key) p = p->left; else p = p->right; Liczbę porównań wykonywanych przez procedurę search w celu zlokalizowania klucza możemy nazwać czasem wyszukiwania. Chcemy znaleźć drzewo, dla którego średni czas wyszukiwania jest najmniejszy. Zakładając, że w każdym przebiegu pętli while wykonywane jest tylko jedno porównanie możemy napisać : czas wyszukiwania = głębokość(key) + 1 Przykładowo (lewe poddrzewo): czas wyszukiwania = głębokość(urszula) + 1 = 2+1 = 3 Niech Key 1, Key 2,, Key n będą n uporządkowanymi kluczami oraz p i będzie prawdopodobieństwem tego, że Key i jest kluczem wyszukiwania. Jeżeli c i oznacza liczbę porównań koniecznych do znalezienia klucza Key i w danym drzewie, to: średni czas wyszukiwania = Σc i p i i=1 Jest to wartość która trzeba zminimalizować. n

Przykład. Mamy 5 różnych drzew dla n = 3. Wartości kluczy nie są istotne. Jeżeli: p 1 = 0.7, p 2 = 0.2 oraz p 3 = 0.1 to średnie czasy wyszukiwania dla drzew wynoszą : 1. 3(0.7) + 2(0.2) + 1(0.1) = 2.6 2. 2(0.7) + 3(0.2) + 1(0.1) = 2.1 3. 2(0.7) + 1(0.2) + 2(0.1) = 1.8 4. 1(0.7) + 3(0.2) + 2(0.1) = 1.5 5. 1(0.7) + 2(0.2) + 3(0.1) = 1.4 Piąte drzewo jest optymalne.

Oczywiście znalezienie optymalnego drzewa wyszukiwania binarnego poprzez rozpatrzenie wszystkich drzew wiąże się z ilością drzew co najmniej wykładniczą w stosunku do n. W drzewie o głębokości n-1 wierzchołek na każdym z n-1 poziomów (oprócz korzenia) może się znajdować na prawo lub lewo. Zatem liczba różnych drzew o głębokości n-1 wynosi 2 n-1 Załóżmy, że klucze od Key i do Key j są ułożone w drzewie, które minimalizuje wielkość: j Σ c m p m m=i gdzie c m jest liczbą porównań wymaganych do zlokalizowania klucza Key m w drzewie. Drzewo to nazywamy optymalnym. Wartość optymalną oznaczymy jako A[i][j] oraz A[i][i]=p i (jeden klucz wymaga jednego porównania). Korzystając z przykładu można pokazać, że w problemie tym zachowana jest zasada optymalności. Możemy sobie wyobrazić n różnych drzew optymalnych: drzewo 1 w którym Key 1 jest w korzeniu, drzewo 2 w którym Key 2 jest w korzeniu,, drzewo n w którym Key n jest w korzeniu. Dla 1 k n poddrzewa drzewa k muszą być optymalne, więc czasy wyszukiwania w tych poddrzewach można opisać:

Dla każdego m k wymagana jest o jeden większa liczba porównań w celu zlokalizowania klucza Key m w drzewie k niż w celu zlokalizowania tego klucza w poddrzewie w którym się znajduje. Dodatkowe porównanie jest związane z korzeniem i daje 1 x p m do średniego czasu wyszukiwania. Średni czas wyszukiwania dla drzewa k wynosi lub inaczej n A[1][k-1] + A[k+1][n] + Σ p m m=1 Jedno z k drzew musi być optymalne więc średni czas wyszukiwania optymalnego drzewa określa zależność: A[1][n] = minimum(a[1][k-1] + A[k+1][n]) + Σ p m m=1 gdzie A[1][0] i A[n+1][n] są z definicji równe 0. Uogólniamy definicje na klucze od Key i do Key j, gdzie i < j i otrzymujemy: n

A[i][j] = minimum(a[i][k-1] + A[k+1][j]) + Σ p m i < j i k j m=i A[i][i] = p i A[i][i-1] oraz A[j+1][j] są z definicji równe 0. Wyliczenia prowadzimy podobnie jak w algorytmie łańcuchowego mnożenia macierzy. j Algorytm znajdowania optymalnego drzewa przeszukiwania binarnego. Problem: określenie optymalnego drzewa wyszukiwania binarnego dla zbioru kluczy, z których każdy posiada przypisane prawdopodobieństwo zostania kluczem wyszukiwania. Dane: n-liczba kluczy oraz tablica liczb rzeczywistych p indeksowana od 1 do n, gdzie p[i] jest prawdopodobieństwem wyszukiwania i-tego klucza Wyniki: zmienna minavg, której wartością jest średni czas wyszukiwania optymalnego drzewa wyszukiwania binarnego oraz tablica R, z której można skonstruować drzewo optymalne. R[i][j] jest indeksem klucza znajdującego się w korzeniu drzewa optymalnego, zawierającego klucze od i-tego do j-tego.

void optsearch(int n, const float p[], float& minavg, index R[][]) { index i, j, k, diagonal; float A[1..n+1][0..n]; } for (i=1; i <= n; i++) { A[i][i-1] = 0; A[i][i] = p[i]; R[i][i] = i; R[i][i-1] = 0; } A[n+1][n] = 0; for(diagonal = 1; diagonal <= n-1; diagonal++) for(i = 1; i <= n - diagonal; i++) //Przekatna 1 { //tuz nad glowna przek j = i + diagonal; A[i][j]=minimum(A[i][k-1]+A[k+1][j] + Σ p m ; i k j m=i R[i][j]= wartość k, która dała minimum; } minavg = A[1][n]; j Złożoność czasową można określić podobnie jak dla mnożenia łańcuchowego macierzy: T(n) = n(n-1)(n+1)/6 Θ( n 3 ) Algorytm budowania optymalnego drzewa przeszukiwania binarnego. Problem: zbudować optymalne drzewo wyszukiwania binarnego. Dane: n liczba kluczy, tablica Key zawierająca n uporządkowanych kluczy oraz tablica R, utworzona w poprzednim algorytmie. R[i][j] jest indeksem klucza w korzeniu drzewa optymalnego, zawierającego klucze od i-tego do j-tego

Wynik: wskaźnik tree do optymalnego drzewa wyszukiwania binarnego, zawierającego n kluczy. node_pointer tree(index i,j) { index k; node_pointer p; } k = R[i][j]; if(k == 0) return NULL; else { p = new nodetype; p->key = Key[k]; p->left = tree(i,k-1); p->right = tree(k+1,j); return p; } Przykład. Załóżmy, że mamy następujące wartości w tablicy Key: Damian Izabela Rudolf Waldemar Key[1] Key[2] Key[3] Key[4] oraz p 1 = 3/8 p 2 = 3/8 p 3 = 1/8 p 4 = 1/8 Tablice A i R będą wówczas wyglądać: 0 1 2 3 4 0 1 2 3 4 1 0 3/8 9/8 11/8 7/4 1 0 1 1 2 2 2 0 3/8 5/8 1 2 0 2 2 2 3 0 1/8 3/8 3 0 3 3 4 0 1/8 4 0 4 5 0 5 0 A R

Problem komiwojażera. Komiwojażer planuje podróż, która uwzględnia odwiedzenie 20 miast. Każde miasto jest połączone z niektórymi innymi miastami. Chcemy zminimalizować czas czyli musimy określić najkrótszą trasę, która rozpoczyna się w mieście początkowym, przebiega przez wszystkie miasta i kończy w punkcie startu. Problem określania najkrótszej trasy nosi nazwę problemu komiwojażera. Problem może być reprezentowany przez graf ważony, z wierzchołkami-miastami. Trasa (droga Hamiltona) w grafie skierowanym jest drogą wiodącą z wierzchołka do niego samego, przechodzącą przez wszystkie wierzcholki dokładnie raz. Optymalna trasa w ważonym grafie skierownym jest taką drogą, która posiada najmniejszą długość. Problem polega na na znalezieniu optymalnej trasy w ważonym grafie skierowanym, kiedy instnieje przynajmniej jedna trasa. Wierzchołek początkowy to ν 1.

Możemy przykładowo opisać trzy trasy: length[ν 1, ν 2, ν 3, ν 4, ν 1 ] = 22 length[ν 1, ν 3, ν 2, ν 4, ν 1 ] = 26 length[ν 1, ν 3, ν 4, ν 2, ν 1 ] = 21 Ostatnia trasa jest optymalna. Najprostsza realizacja polega na rozważeniu wszystkich tras. W ogólnym przypadku może istnieć krawędź łącząca każdy wierzchołek z każdym innym wierzchołkiem. Drugi wierzchołek na trasie może być jednym z n-1 wierzchołków, trzeci wierzchołek jednym spośród n-2 wierzchołków, n-ty wierzchołek ostatnim wierzchołkiem. Zatem całkowita liczba tras wynosi - (n-1)(n-2) 1 = (n-1)! co oznacza wartość gorszą od wykładniczej. Czy można zastosować programowanie dynamiczne? Jeżeli ν k jest pierwszym wierzchołkiem po ν 1 na trasie optymalnej, to droga podrzędna tej trasy z ν k do ν 1 musi być drogą najkrótszą, przechodzącą przez wszystkie pozostałe wierzchołki dokładnie raz. Zatem zasada optymalności działa i można stosować programowanie dynamiczne. Graf reprezentuje macierz przyległości W: 1 2 3 4 1 0 2 9 2 1 0 6 4 3 7 0 8 4 6 3 0

W rozwiązaniu: V = zbiór wszystkich wierzchołków A = podzbiór zbioru V D[ν i ][A] = długość najkrótszej drogi z ν i do ν 1 przechodzącej przez każdy wierzchołek A dokładnie raz Zatem w przykładzie: V = {ν 1, ν 2, ν 3, ν 4 } reprezentuje zbiór, [ ] reprezentuje drogę Jeżeli A = {ν 3 }, to Jeżeli A = {ν 3, ν 4 }, to D[ν 2 ][A] = length[ν 2, ν 3, ν 1 ] = D[ν 2 ][A] = minimum(length[ν 2, ν 3, ν 4, ν 1 ],length[ν 2, ν 4, ν 3, ν 1 ]) = minimum(20, ) = 20 Zbiór V {ν 1, ν j } zawiera wszystkie wierzchołki oprócz ν 1 oraz ν j i ma zastosowanie zasada optymalności, możemy stwierdzić: Długość trasy minimalnej = minimum(w[1][j]+d[ν j ][V-{ν 1, ν j }]) 2 j n i ogólnie dla i 1oraz ν i nie należącego do A D[ν i ][A] = minimum(w[i][j]+d[ν j ][A-{ν j }]) jeżeli A j: νj A D[ν i ][ ] = W[i][1] Określmy optymalną trasę dla grafu z przykładu. Dla zbioru pustego: D[ν 2 ][ ] = 1 D[ν 3 ][ ] = D[ν 4 ][ ] = 6

Teraz rozważamy wszystkie zbiory zawierające jeden element: Podobnie: D[ν 3 ][{ν 2 }] = minimum(w[3][j] +D[ν j ][{ν 2 }-{ν j }]) = W[3][2] + D[ν 2 ][ ] = 7 + 1 = 8 D[ν 4 ][{ν 2 }] = 3 + 1 = 4 D[ν 2 ][{ν 3 }] = 6 + = D[ν 4 ][{ν 3 }] = + = D[ν 2 ][{ν 4 }] = 6 + 4 = 10 D[ν 3 ][{ν 4 }] = 8 + 6 = 14 Teraz rozważamy wszystkie zbiory zawierające dwa elementy: D[ν 4 ][{ν 2, ν 3 }] = minimum(w[4][j] +D[ν j ][{ν 2,ν 3 } - {ν j }]) j:νj {ν2, ν3 } Podobnie: = minimum(w[4][2]+d[ν 2 ][{ν 3 }], W[4][3]+D[ν 3 ][{ν 2 }]) = minimum(3+, +8) = D[ν 3 ][{ν 2, ν 4 }] = minimum(7+10, 8+4) = 12 D[ν 2 ][{ν 3, ν 4 }] = minimum(6+14, 4+ ) = 20 Na końcu liczymy długość optymalnej trasy: D[ν 1 ][{ν 2, ν 3, ν 4 }] = minimum(w[1][j] +D[ν j ][{ν 2,ν 3, ν 4 } - {ν j }]) = minimum(w[1][2] +D[ν 2 ][{ν 3,ν 4 }], W[1][3] +D[ν 3 ][{ν 2,ν 4 }], W[1][4] +D[ν 4 ][{ν 2,ν 3 }]) = minimum(2+20, 9+12, 26/ ) = 21

Algorytm programowania dynamicznego dla problemu komiwojażera Problem: określić optymalną trasę w ważonym grafie skierowanym. Wagi są liczbami nieujemnymi. Dane wejściowe: ważony graf skierowany oraz n, liczba wierzchołków w grafie. Graf reprezentujemy macierzą przyległości W W[i][j] reprezentuje wagę krawędzi od wierzchołka i-tego do j-tego. Wynik: zmienna minlength, której wartością jest długość optymalnej trasy oraz macierz P, na podstawie której konstruujemy optymalną trasę. Wiersze tablicy P są indeksowane od 1 do n, zaś jej kolumny są indeksowane przez wszystkie podzbiory zbioru V-{ν 1 }. Element P[i][A] jest indeksem pierwszego wierzchołka, znajdującego się po {ν i } na najkrótszej drodze z ν i do ν 1, która przechodzi przez wszystkie wierzchołki A dokładnie raz. void komiwojazer(int n, const number W[][], index P[][], number& minlength) { index i,j,k; number D[1..n][podzbior zbioru V-{ν 1 }]; for(i=2;i<=n;i++) D[i][ ] = W[i][1]; for(k=1;k<=n-2;k++) for(wszystkie podzbiory A V-{ν 1 } z k wierzch) for(i,takie ze i 1 oraz ν i nie należy do A){ D[i][A]=minimum(W[i][j]+D[j][A-{ν j }]); j: νj A P[i][A] = wartość j, która daje minimum; }

D[1][V-{ν 1 }] = minimum(w[1][j]+d[j][v-{ν 1,ν j ]); 2 j n P[1][V-{ν 1 }] = wartość j, która daje minimum; minlength = D[1][V-{ν 1 }]; } Elementy tablicy P, wymagane do określenia optymalnej trasy dla grafu z przykładu to: Optymalną trasę można uzyskać: P[1,{ν 2, ν 3, ν 4 }] P[3,{ν 2, ν 4 }] P[4,{ν 2 }] Indeks pierwszego wierzchołka = P[1][{ν 2, ν 3, ν 4 }] = 3 Indeks drugiego wierzchołka = P[3][{ν 2, ν 4 }] = 4 Indeks trzeciego wierzchołka = P[4][{ν 2 }] = 2 Optymalna trasa ma postać: {ν 1, ν 3, ν 4, ν 2, ν 1 } Dotychczas nie opracowano algorytmu dla problemu komiwojażera, którego złożoność w najgorszym przypadku byłaby lepsza niż wykładnicza.