Programowanie dynamiczne Patryk Żywica 5 maja 2008 1
Spis treści 1 Problem wydawania reszty 3 1.1 Sformułowanie problemu...................... 3 1.2 Algorytm.............................. 3 1.2.1 Prosty algorytm zachłanny................. 3 1.2.2 Algorytm dynamiczny................... 3 2 Problem znajdowania najdłuższego niemalejacego podciagu 6 2.1 Sformułowanie problemu...................... 6 2.2 Algorytm.............................. 6 2
1 Problem wydawania reszty 1.1 Sformułowanie problemu W najprostszy sposób problem ten definiuje się następująco: mamy do dyspozycji nieskończenie wiele monet o nominałach c 1, c 2,..., c n. Chcemy wypłacić kwotę k tak, aby ilość użytych monet była jak najmniejsza. W dalszej części zakładam, że poszczególne nominały przechowywane są w tablicy c o rozmiarze n+1 tak, że c[0] = 0, c[1] = c 1... c[n] = c n oraz, że wszystkie tablice indeksowane są od zera. 1.2 Algorytm 1.2.1 Prosty algorytm zachłanny Stosujemy go praktycznie codziennie, zawsze gdy musimy za coś zapłacić. Polega on na wypłacaniu danej kwoty największym nominałem tak długo jak kwota nie jest mniejsza od jego wartości, następnie powtarzamy postępowanie dla mniejszego nominału. Pseudokod tego rozwiązania wygląda następująco: 1. dla wszystkich nominałów c i w kolejności malejącej 2. while k >= c i 3. i i c i Jeśli zmienna k po zakończeniu algorytmu będzie większa od zera, to znaczy, że danej kwoty nie da się wypłacić. 1.2.2 Algorytm dynamiczny Jednak algorytm przedstawiony powyżej nie wystarcza. Można odpowiednio dobrać nominały, np: dla monet 1, 4, 9 algorytm zachłanny nie zawsze zwróci poprawny wynik. Zatem widać, że problem ten w ogólnym przypadku nie jest taki prosty jak sie wydawało. Z pomocą przychodzi tu technika programowania dynamicznego. Wcześniej jednak przydałoby się bardziej sprecyzować użyteczność algorytmu zachłannego. Wiemy już, że jest on poprawny dla nominałów 1, 2, 5. Z tego też 3
powodu są to nominały używane w większości krajów na świecie. Jednak nie jest to jedyny przypadek. Można udowodnić, że algorytm zachłanny wystarcza również dla nominałow spełniających następujące założenie: dla dowolnego c N oraz k N i nominałach postaci c 0, c 1... c k Wracając do algorytmu. Zdefiniujmy tablicę dwuwymiarową T o rozmiarze k na n. W komórce o indeksie T[i][j] znajduje sie najmniejsza liczba monet potrzebna do wypłacenia kwoty i używając pierwszych j monet. W naszym przykładzie będą to kolejno zbiory dostępnych monet: {0}, {1}, {1, 4}, {1, 4, 9}. Monetę o nominale 0 możemy bez straty ogólności dołożyć do dostępnego zbioru, ponieważ żadna kwota nie może zostać wypłacona przy jej użyciu stąd wartość dla wszystkich kwot większych od 0. W każdym kroku algorytmu musimy zdecydować, który sposób wypłaty kwoty i przy użyciu pierwszych j monet, jest optymalny. Wiersze przetwarzamy kolejno po sobie, w taki sposób, aby wszystkie wiersze o kwocie mniejszej od i były przetworzone przed wierszem i. Wiersze przetwarzamy zgodnie ze wzrostem mnogości zbioru nominałów. Łatwo zauważyć, że dzieki takiej kolejności przetwarzania, mamy tylko dwie możliwości w każdym kroku. Dodanie nowego nominału może nie wpłynąć na ilość monet potrzebnych do wypłacenia danej kwoty, wtedy T [i][j] = T [i][j 1]. Drugi przypadek jest przeciwny, czyli dodanie nowego nominału c j wpływa na ilość monet, wtedy wynikiem jest rozwiązanie optymalne dla kwoty i c j (używąjac tego samego zbioru monet) powiększone o jedną monetę nominału c j. Wartość w tablicy T to T [i][j] = T [i c j ][j] + 1. Pozostaje tylko kwestia zdecydowania, który przypadek zachodzi dla danego pola. Jest to bardzo łatwe, wystarczy sprawdzić, która z tych wartości jest mniejsza. Poniżej przedstawione jest działanie tego algorytmu dla nominałów 1, 4, 9 i kwot od 0 do 17. 4
Kwota 0 1 4 9 0 0 0 0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 algorytm Kwota 0 1 4 9 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 4 4 1 1 5 5 2 2 6 6 3 3 7 7 4 4 8 8 2 2 9 9 3 1 10 10 4 2 11 11 5 3 12 12 3 3 13 13 4 2 14 14 5 3 15 15 6 4 16 16 4 4 17 17 5 3 Stan tablicy T przed i po wykonaniu algorytmu dla kwoty 17 oraz nominałów 1, 4 oraz 9. Pogrubione liczby w wierszach 12 i 16 pokazują miejsca, w których algorytm zachłanny zwróciłby nieoptymalny wynik. Strzałki w tabelach wykorzystywane są do odtworzenia wyboru monet dającego optymalne rozwiązanie. Zdefiniujmy tablicę B o rozmiarze k na n. B[i][j] przyjmuje jedną z dwóch wartości lub w zależności od tego czy wypłacając kwotę i bieżemy monetę o nominale c j (wartość ), lub wypłacamy tę kwotę nie używając monety c j (wartość ). Odtworzenia wyboru monet dokonujemy zaczynając od pola T[k][n] idąc zgodnie z kierunkiem strzałek do początku tablicy. Jeśli poruszamy się w górę, to znaczy, że moneta o numerze aktualnej kolumny została wybrana. 5
Oto pseudokod dynamicznego algorytmu wydawania reszty. 1. for i 0 to n 2. do T[0][i] 0 3. for i 1 to k 4. do T[i][0] 5. for i 1 to k 6. do for j 1 to n 7. do if i<c[j] 8. then T[i][j] min(t [i c[j]][j] + 1, T [i][j 1]) 9. B[i][j] 10. else T[i][j] T[i][j-1] 11. B[i][j] T[k][n] zawiera ilość monet potrzebnych do optymalnego wypłacenia kwoty k, jeśli jest to możliwe, lub jeśli nie jest to możliwe. Złożoność czasowa i pamięciowa algorytmu to Θ(nk) 2 Problem znajdowania najdłuższego niemalejacego podciagu 2.1 Sformułowanie problemu Jest dany ciag a 1, a 2,..., a n liczb rzeczywistych. Chcemy wyszukać najdłuższy podciąg b 1, b 2,..., b m tego ciągu, taki aby spełniony był warunek: b 1 b 2... b n Problem oczywiście można przeformułować na znajdowanie najdłuższego podciągu nierosnącego, malejącego lub rosnącego. 2.2 Algorytm Algorytm z wykorzystaniem programowania dynamicznego działa w czasie O(n lg n). Jego idea polega na pamiętaniu w tablicy T[i] największego elementu w podciągu niemalejącym o długości i. 6
W czasie przetwarzania kolejnych wyrazów ciągu (a n ) aktualizujemy tablicę T wstawiając wartość a i pod najmniejszm takim indeksem i tablicy T, aby aktualna wartość pod tym indeksem była najmniejszą wartością wiekszą lub równą od a i. Poniżej przedstawiam przykład działania, a następnie pseudokod algorytmu. a) T[i] b) T[i] 1 c) T[i] 1 7 d) T[i] 1 2 e) T[i] 1 2 5 f) T[i] 1 2 4 g) T[i] 1 2 3 h) T[i] 1 2 3 4 i) T[i] 1 2 3 4 10 (a) przedstawia początkowe ustawienie tablicy T. (b) tablica T po wstawieniu pierwszego elementu. (c)-(i) tablica T po wstawieniu elementu pogrubionego. Z tablicy (i) możemy odczytać, że najdłuższy podciąg niemalający ma długość 5. Aby móc odczytać, które wyrazy ciągu (a n ) tworzą najdłuższy niemalejący podciąg trzeba wprowadzić dodatkową tablicę, w której dla każdego a i będziemy przechowywali indeks wyrazu go poprzedzającego w najdłuższym podciągu. 7
Kluczowym spostrzeżeniem, dzięki któremu algorytm ten jest szybki jest to, że zawartość tablicy T jest zawsze niemalejąca. Można więc użyć wyszukania binarnego w celu odnalezienia miejsca, w które należy wstawić dany element. 1. for i 1 to n 2. do T[i] 3. T[0] 4. for i 1 to n 5. do p WYSZUKAJ-BINARNIE(T, a i ) 6. T[p] a i Procedura WYSZUKAJ-BINARNIE(T, a i ) zwraca najmniejszy indeks najmniejszego elementu wiekszego lub równego a i. Długość podciągu odczytujemy jako najmniejszy indeks tablicy T, którego wartość jest mniejsza od. 8