Krok 1
Transkrypt
Krok 1
Programowanie dynamiczne Technika konstrukcji algorytmów Własności: • stosowana najczęściej do problemów optymalizacyjnych lub znajdowania strategii w grze • dla zadanego wejścia jest wiele rozwiązań dopuszczalnych, każde posiada wartość (koszt) • celem jest znalezienie rozwiązania optymalnego, o minimalnym lub maksymalnym koszcie • stosowana gdy podproblemy posiadają wspólne (pod)podproblemy; metoda "dziel i zwyciężaj" rozwiązywałaby te same (pod)podproblemy wielokrotnie • aby uniknąć powtórnego rozwiązywania podproblemów korzysta z tablicy, umieszczając w niej wyniki już rozwiązanych podproblemów 1 Konstruowanie algorytmu metodą programowania dynamicznego: 1. Scharakteryzuj strukturę rozwiązania optymalnego, w szczególności jak powstaje z optymalnych rozwiązań podproblemów; upewnij się że zachodzi własność optymalnej podstruktury 2. Podaj rekurencyjny przepis na koszt rozwiązania optymalnego dla (pod)zadania 3. Zapisz algorytm znajdujący koszt rozwiązania optymalnego metodą wstępującą ("bottom-up" - od najmniejszych podzadań do największych) 4. Podaj metodę znajdowania rozwiązania optymalnego (niepotrzebne jeśli obliczamy tylko koszt optymalny). Przykład 1: Liczby Fibonacciego (problem nieoptymalizacyjny) 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, . . . F(0)=0, F(1)=1, F(n)=F(n–1)+F(n–2), n>1 oblicz F(n), dla zadanego n. Krok 1: definicja liczb F. Krok 2: zależność rekurencyjna (czyli w zasadzie to samo co krok 1) F(n)= n, jeśli n<=1 F(n-1)+F(n-2), wpp. 2 W zapisie algorytmicznym: Fib(n) if (n<2) return n; return Fib(n–1)+Fib(n–2); Czas wykonania: T(n) = T(n–1) + T(n–2) + O(1) Drzewo wywołań rekurencyjnych Fib(n) / Fib(n–1) / \ Fib(n–2) Fib(n–3) / \ Fib(n–3) Fib(n–4) \ Fib(n–2) / \ Fib(n–3) Fib(n–4) Głębokość lewego skrajnego liścia: Głębokość prawego skrajnego liścia: liczba węzłów V(n): 2n/2≤ V(n) ≤ 2n n n/2 zatem złożoność T(n): T(n) = Ω(2n/2), T(n) = O(2n). Przyczyna złożoności wykładniczej: wielokrotne obliczanie tego samego podzadania. 3 Krok 3: bottom-up, z tablicowaniem wyników: Fib2(n) F[0] = 0 F[1] = 1 for (i=2; i<=n; i++) F[i]=F[i–1]=F[i–2] return F[n] Złożoność czasowa i pamięciowa: Θ(n). (Oczywiście można lepiej) Krok 4: nie ma (koszt rozwiązania jest tożsamy z rozwiązaniem). Czy w ten sposób można usprawnić np. mergesort? Nie, bo w drzewie wywołań rekurencyjnych każdy podproblem pojawia się tylko raz. 4 Przykład 2: Podzielić liczbę naturalną n>0 na k>0 składników naturalnych, tak by iloczyn składników był największy: n=n1+...+nk , n1⋅n2⋅...⋅nk maksymalny, k<n Rekurencja po k: k>1: Fk(n) = max{nk⋅Fk-1(n-nk), 0<nk<n}, dla n>1 0, dla n=1 k=1: F1(n) = n, n≥1 Obliczenie wstępujące: tablicuj kolejno funkcje F1, F2, ..., Fk-1, i na końcu oblicz Fk(n) F1[i] = i, i=1,...,n F2[i] = max{j⋅F1(i-j), 0<j<i}, i=1,...,n F3[i] = max{j⋅F2(i-j), 0<j<i}, i=1,...,n ... Fk-1[i] = max{j⋅Fk-2(i-j), 0<j<i}, i=1,...,n Fk[n] = max{j⋅Fk-1(n-j), 0<j<n}. Pamięć: Θ(n) dwie tablice długości n, na aktualnie obliczane Fk oraz potrzebne do tego Fk-1. Czas: Θ(n2k) Uwaga: rozmiar danych jest rzędu lg n + lg k, zatem czas jest funkcją wykładniczą od rozmiaru danych – formalnie algorytm nie ma złożoności wielomianowej. 5 Problem optymalnego mnożenia ciągu macierzy dane: • ciąg macierzy liczbowych A1, A2, ..., An, • Ak jest rozmiaru pk-1 x pk, k=1,...,n obliczyć: • najmniejszą możliwą liczbę operacji mnożenia liczb elementów macierzy potrzebną do obliczenia iloczynu A1A2... An, czyli: • optymalne nawiasowanie tego iloczynu Założenie: Jeśli A∈Rp x q, B∈Rq x r, to iloczyn C = AB jest obliczany "z definicji" Cij = Σk=1,...,q Aik Bkj koszt obliczania iloczynu C=AB to liczba mnożeń elementów macierzy: p*q*r Koszt mnożenia ciągu macierzy zależy od nawiasowania. Np. dla rozmiarów A: 5 x 10, B: 10 x 3, C: 3 x 7, D: 7 x 5 iloczyn ABCD może być obliczony na 5 sposobów (5 różnych nawiasowań) 1. ((AB)C)D wymaga mnożeń: 5*10*3=150 dla AB, rozmiar wyniku 5 x 3 5*3*7= 105 dla (AB)C, rozmiar 5 x 7 5*7*5=175 dla ((AB)C)D, rozmiar 5 x 5 razem: 430. 6 2. (AB)(CD): 330 mnożeń 3. (A(BC))D: 735 4. A((BC)D): 810 5. A(B(CD)): 505 Intuicja: zacznij od eliminacji największego rozmiaru (tutaj: 10, czyli najpierw iloczyn AB) jest błędna. Możliwa liczba nawiasowań wyrażenia n-argumentowego: liczba Catalana C(n) C(1) = 1 C(n) = Σk=1,...,n-1 C(k)C(n-k), n>1 bo: k jest numerem ostatniego mnożenia, zatem • k = liczba argumentów do pomnożenia (rekurencyjnie) w lewej części ciągu, • n-k = liczba argumentów do pomnożenia w prawej części ciągu 7 Krok 1: Jak otrzymujemy rozwiązanie optymalne z rozwiązań podzadań ? Obserwacja: dla zadanego układu nawiasów, ostatnie mnożenie dzieli ciąg na dwa podzadania: (A1A2···Ak)(Ak+1Ak+2···An) dla pewnego 1<=k<=n–1 Więc A1, A2,..., Ak oraz Ak+1, Ak+2,..., An są mnożone niezależnie a następnie mnożone są wyniki (A1A2···Ak) i (Ak+1Ak+2···An). Pomysł: sprawdź wszystkie możliwe punkty podziału k i wybierz najlepszy. Własność optymalnej podstruktury Jeśli optymalne nawiasowanie Nij dla ciągu macierzy AiAi+1···Aj dzieli ciąg w pewnym miejscu k, i<=k<=j-1, to to nawiasowanie zawiera w sobie optymalne nawiasowanie Nik dla podciągu AiAi+1···Ak i optymalne nawiasowanie Nk+1,j Ak+1Ak+2···Aj. Dlaczego? jeśliby Nij nie zawierało optymalnego nawiasowania N'ik dla podciągu AiAi+1···Ak, to zastępując Nik w Nij przez N'ik otrzymalibyśmy nawiasowanie N'ij dla AiAi+1···Aj o mniejszym koszcie niż Nij – sprzeczne z optymalnością Nij. Analogicznie dla prawej strony ciągu. 8 Krok 2: rozwiązanie rekurencyjne Zdefiniujmy m[i, j] , dla 1≤i≤j≤n: m[i,j] = najmniejsza liczba mnożeń potrzebna do obliczenia iloczynu AiAi+1...Aj Potrzebujemy m[1,n], obliczamy wszystkie wartości m[i,j]. dla i=j, m[i,j] = 0 (jedna macierz) Niech k – punkt podziału dla AiAi+1···Aj, i≤k≤j–1. Wówczas: m[i, k] = najmniejsza liczba mnożeń dla AiAi+1···Ak m[k+1, j] = najmniejsza liczba mnożeń dla Ak+1Ak+2···Aj AiAi+1···Ak jest rozmiaru pi–1 x pk Ak+1Ak+2···Aj jest rozmiaru pk x pj, zatem liczba mnożeń dla pomnożenia obu wyników wynosi pi–1pkpj. 9 Ostatecznie: Jeśli dzielimy w punkcie k to liczba mnożeń wynosi m[i, k] + m[k+1, j] + pi–1pkpj szukamy k które zminimalizuje tę sumę, otrzymujemy rekurencję: m[i,j] = 0, dla i=j m[i,j] = mini≤k≤j-1{m[i,k] + m[k+1,j] + pi–1pkpj}, dla i<j która prowadzi do algorytmu rekurencyjnego: RMCO(p, i, j) // recursive matrix chain order if (i=j) return 0 else min=∞ for (k=i;k<j;k++) q=RMCO(p,i,k)+RMCO(p,k+1,j)+p[i–1]p[k]p[j] if (q<min) min=q return min Main call: RMCO(p,1,n). Obserwacja: • wiele podproblemów rozwiązywanych wielokrotnie • złożoność wykładnicza Krok 3: obliczenie metodą wstępującą z zapamiętywaniem wyników dla podproblemów w tablicy trójkątnej górnej: m[i, j] , dla 1≤i≤j≤n: gdzie m[i, j] = 0 dla i = j, zatem przekątna jest zerowa 10 Przykład podany wcześniej: p[0]=5, p[1]=10, p[2]=3, p[3]=7, p[4]=5. rozmiar 1: m[1, 1] = m[2, 2] = m[3, 3] = m[4, 4] = 0 rozmiar 2: m[1, 2] = p[0]p[1]p[2] = 5*10*3 = 150 m[2, 3] = p[1]p[2]p[3] = 10*3*7 = 210 m[3, 4] = p[2]p[3]p[4] = 3*7*5 = 105 rozmiar 3: m[1, 3] = min { /* k=1 */ m[1, 1] + m[2, 3] + p[0]p[1]p[3], /* k=2 */ m[1, 2] + m[3, 3] + p[0]p[2]p[3] } = min {560, 255} = 255 m[2, 4] = min { /* k=2 */ m[2, 2] + m[3, 4] + p[1]p[2]p[4], /* k=3 */ m[2, 3] + m[4, 4] + p[1]p[3]p[4] } = min {255, 560} = 255 rozmiar 4: m[1, 4] = min { /* k=1 */ m[1, 1] + m[2, 4] + p[0]p[1]p[4], /* k=2 */ m[1, 2] + m[3, 4] + p[0]p[2]p[4], /* k=3 */ m[1, 3] + m[4, 4] + p[0]p[3]p[4] } = min {505, 330, 430} = 330 Wynik – tablica m[]: 0 x x x 150 0 x x 255 210 0 x 330 255 105 0 11 Algorytm wstępujący aby zrealizować krok 4 – generowanie optymalnego nawiasowania: • obliczając m[i, j], najlepszą wartość k zapamiętujemy w s[i,j] • po zakończeniu, s[i, j]=k oznacza, że m[i, j] jest obliczane z m[i, k] oraz m[k+1, j]. MCO(p, n) for (i=1; i<=n; i++) m[i,i]=0 for (d=2; d<=n; d++) for (i=1; i<=n–d+1; j=i+d–1 // [i,j] // matrix chain order // sub-problem size i++) – subproblem boundaries m[i,j]=∞ // compute m[i,j] for (k=i; k<j; k++) q=m[i,k]+m[k+1,j]+p[i–1]p[k]p[j] if (m[i,j]>q) m[i,j]=q s[i,j]=k return m[1,n] end. Złożoność O(n3) oczywista. Dokładniej: liczba wykonań instrukcji q = m[.... wynosi: T(n) = Σd=2,...,n Σi=1,...,n-d+1 d = Σd=2,...,n (n-d+1)d proste obliczenia: T(n) = 1/6 n3 + O(n2) = Θ(n3) 12 Krok 4: obliczanie optymalnego rozwiązania Tablica s[]: 0 x x x 1 0 x x 2 2 0 x 2 2 3 0 np. s[1, 3] = 2 bo k=2 to najlepsze miejsce podziału dla m[1, 3] optymalne nawiasowanie dla A1A2A3A4: s[1, 4] = 2, zatem (A1A2)(A3A4). Inny przykład: jeśliby s[] miała wartości np.: 0 x x x 1 0 x x 1 2 0 x 3 3 3 0 to nawiasowanie byłoby następujące: A1A2A3A4 = (A1A2A3)A4, (A1A2A3) = (A1(A2A3)) czyli całość: A1A2A3A4 = (A1(A2A3))A4. Ogólnie: na podstawie tablicy s[] wygenerować drzewo nawiasowania • korzeń = nr ostatniego mnożenia = s[1,n] • korzeń lewego poddrzewa: rekurencja, nr ostatniego mnożenia w lewym podzadaniu • korzeń prawego poddrzewa: rekurencja, nr ostatniego mnożenia w prawym podzadaniu Złożoność algorytmu konstrukcji drzewa? 13 Spamiętywanie (memoization) • programowanie dynamiczne "top-down" – czyli w postaci rekurencyjnej • jak poprzednio, z tablicą na rozwiązania podzadań, • bez wielokrotnego rozwiązywania tych samych podzadań Na początku każdego rekurencyjnego wywołania sprawdź, czy tablica zawiera wynik tego podzadania • jeśli tak, użyj tego wyniku i zakończ to wywołanie • jeśli nie, to oblicz wynik a potem zapamiętaj w tablicy MMCO(p, i, j) //memo matrix chain order if (m[i,j] ≠ ∞ ) return m[i,j] if (i = j) m[i,j] = 0 else for (k=i; k<=j–1; k++) q=MMCO(p,i,k)+MMCO(p,k+1,j)+p[i–1]p[k]p[j] if (m[i,j] > q) m[i,j] = q return m[i,j] Memo-Matrix-Chain(p, n) // Main program: for (i=1; i<=n; i++) // initialize table for (j=i; j<=n; j++) m[i,j] = ∞ result = MMCO(p,1,n) // call MMCO 14 Złożoność: • • • Liczba podzadań: Θ(n2) Koszt dla pojedynczego podzadania: O(n) Każde zadanie liczone tylko raz. Wniosek: T(n) = O(n3) jest nieco pochopny, bo: ile jest wszystkich wywołań procedury? na pewno więcej niż wywołań w których się oblicza podzadanie koszt czasowy obliczyliśmy dla wywołań, które obliczają podzadanie (wywołanie I typu). Ile jest wywołań II typu – trafiających na już obliczone podzadanie? Ale: • • • • • każde wywołanie typu II jest wykonywane podczas obliczania jakiegoś podzadania – a więc wywołania typu I wywołań typu I jest Θ(n2) każde wywołanie typu I powoduje co najwyżej 2n następnych wywołań, a więc powoduje co najwyżej 2n wywołań typu II zatem liczba wywołań typu II jest O(n2*2n)=O(n3) każde wywołanie typu II kosztuje O(1) Zatem wywołania typu II też kosztują O(n3), zatem rzeczywiście: T(n) = O(n3) 15 Problem plecakowy Klasyczny problem z optymalizacji kombinatorycznej rozwiązywany często metodą programowania dynamicznego. Wersja 0-1: Dane: • n elementów, vi, wi to odpowiednio wartość oraz waga i-tego elementu • W – pojemność plecaka Wynik: • podzbiór elementów taki, że suma wag nie przekracza W oraz suma wartości jest możliwie największa. Wersja ułamkowa: Różni się tylko tym, że można dobierać ułamki elementów (nie większe niż 1). Własność optymalnej podstruktury wersji 0-1: Twierdzenie: Rozważmy optymalny dobór elementów do plecaka. Jeśli usuniemy z niego element j-ty, to pozostałe elementy tworzą optymalny dobór dla plecaka o pojemności W – wj, spośród n–1 elementów (bez elementu j-tego). Dowód: dość oczywisty. 16 Rozwiązanie wersji 0-1 metodą programowania dynamicznego: Podproblem P(j, k) powstaje przez: • ograniczenie zbioru elementów do początkowych j elementów (0 ≤ j ≤ n) • ograniczenie wagi plecaka do k (0 ≤ k≤ W) Definiujemy tablicę: S[j, k] := wartość optymalnego rozwiązania dla P(j,k) Dla j=0 nie ma żadnych elementów, więc wartość optymalnego plecaka o dowolnej pojemności jest 0. Dla j>0, k≥0 optymalna wartość plecaka S[j,k] obliczana jest przez rozpatrzenie następujących dwóch możliwości: • element j-ty nie należy do optymalnego upakowania S[j,k] = S[j-1,k] czyli wszystkie elementy wybrano spośród 1,...,j–1 • element j-ty należy do optymalnego upakowania pozostałą przestrzeń k–wj wypełniono optymalnie elementami spośród 1,...,j–1, wtedy S[j,k] = S[j-1,k–wj]+ vj optymalna wartość S[j,k] to większa z dwóch powyższych Tablica S[] można wypełnić wierszami, w wierszu od lewej do prawej. 17 tablica S[0..n, 0..W] // wiersz 0 w tablicy S for (k=0; k<=W; k++) S[0,k] = 0 ; for (j=1; j<=n; j++) // wypełnij wiersz j-ty for (k=0; k<=W; k++) if (k-w[j] < 0) S[j,k] = S[j-1,k] ; //j-ty elem nie mieści się w plecaku else S[j,k]=max( S[j-1,k], S[j-1,k-w[j]]+v[j] ) return S[n,W]. Złożoność: Θ(nW) , (czas i pamięć) gdyż tyle jest elementów do wypełnienia, a obliczanie każdego zajmuje czas stały. Nie jest to złożoność wielomianowa, zależy liniowo od wartości liczby W, a nie od długości jej zapisu (jest to złożoność pseudowielomianowa). Rekonstrukcja optymalnej zawartości plecaka: wystarcza tablica S[], nie trzeba podczas jej obliczania zapamiętywać jakiego wyboru dokonano ustalając wartość S[j,k]. (ćwiczenie) O(n) Przelicz przykład: n=6, pojemność plecaka W=19, wagi elementów: 4, 7, 3, 5, 6, 7, ich wartości odpowiednio 3, 5, 4, 5, 5, 8. 18 Gdy obliczamy tylko optymalną wartość plecaka – bez generowania optymalnego upakowania: Spostrzeżenie 1: wystarczy pamiętać tylko wiersz poprzedni i aktualny tablicy S, bo wartości w kolejnym wierszu zależą tylko od poprzedniego wiersza, a nie od wcześniejszych. Czas ten sam - Θ(nW), pamięć Θ(W) Spostrzeżenie 2: jeśli wypełniać aktualnie obliczany wiersz od prawej, to można obliczane wartości wpisywać w miejsce wartości poprzedniego wiersza, a więc wystarcza przechowywać tylko 1 wiersz tablicy S. Metoda 2 (również programowanie dynamiczne) Istnieje inny algorytm pseudowielomianowy: Złożoność: Θ(n∑i=1,...,n vi). Oznaczając V = max {vj} otrzymujemy O(n2 V) Szczegóły – ćwiczenie. 19 Jeszcze jedno typowe zadanie na programowanie dynamiczne: Problem najdłuższego wspólnego podciągu (LCS) Dla zadanych dwóch ciągów X = <x1, . . . . , xm> Y = <y1, . . . . , yn> znajdź wspólny podciąg o największej długości Np. X = <A, B, A, C, > Y = <A, C, A, B, C> LCS: <A,A,C> (nie unikalne) Zastosowanie: Miara podobieństwa łańcuchów DNA – słów nad alfabetem {A, C, G, T} : Im dłuższy wspólny podciąg tym bardziej podobne łańcuchy. Metoda "brute force": • generuj wszystkie możliwe podciągi ciągu X • sprawdzaj czy jest to również podciąg ciągu Y • zapamiętaj najdłuższy o tej własności Liczba różnych podciągów ciągu n-elementowego: tyle ile podzbiorów, czyli 2m. 20 Rozwiązanie metodą programowania dynamicznego: Podproblem: para prefiksów ciągów X i Y Xi = <x1, . . . . , xi> Yj = <y1, . . . . , yj> Tw. (o optymalnej podstrukturze) Dane są ciągi znaków: X = <x1, . . . . , xm> Y = <y1, . . . . , yn> Z = <z1, . . . . , zk> takie, że Z jest dowolnym LCS-em dla X i Y. Wówczas: 1. Jeśli xm = yn , to zk = xm = yn oraz Zk-1 jest LCS-em dla Xm-1 i Yn-1 2. Jeśli xm ≠ yn , to (a) jeśli zk ≠ xm , to Z jest LCS-em dla Xm-1 i Y (b) jeśli zk ≠ yn , to Z jest LCS-em dla X and Yn-1 Uwaga: twierdzenie odnosi się do optymalnego rozwiązania, ale nie mówi jak go znaleźć. Dowód: cut-and-paste, ćw. Wniosek: LCS dwóch ciągów zawiera LCS-y prefiksów tych ciągów – zatem problem LCS ma własność optymalnej podstruktury. 21 Defnicja rekurencyjna rozwiązania: Jeśli xm = yn to 1 podproblem: znajdź LCS dla Xm-1 i Yn-1 dołącz do niego xm Jeśli xm ≠ yn to 2 podproblemy: znajdź LCS dla Xm-1 i Yn znajdź LCS dla Xm i Yn-1 weź lepszy z nich Niech c[i,j] = długość LCS dla Xi i Yj. c[i,j] = 0, c[i-1, j-1] + 1 max(c[i, j-1], c[i-1, j]) i=0 lub j=0 i,j > 0, xi = yj, i,j > 0, xi ≠ yj Obserwacja: c[i,j] określone dla 0 <=i<=m, 0<=j<=n, zatem: liczba podproblemów wynosi (m+1)(n+1) = Θ(mn) 22 Obliczenie wstępujące: Tablica c[i,j], (m+1) x (n+1) optymalne wartości rozwiązań podproblemów c[i,j] zależy od: c[i-1, j-1] c[i-1, j] c[i, j-1] czyli elementów tablicy powyżej lub po lewej Zatem obliczenie tablicy c[]: • wierszami, • w wierszu od lewej do prawej kolumna zerowa i wiersz zerowy: c[i, 0] = 0, dla każdego i, c[0, j] = 0, dla każdego j. 23 Algorytm LCS-Length(X, Y) //m, n – długości słów X i Y for (i=1; i<=m; i++) c[i,0]=0 for (j=0; j<=n; j++) c[0,j]=0 for (i=1; i<=m; i++) for (j=1; j<=n; j++) if (X[i]=Y[j]) c[i,j] = c[i-1,j-1]+1 else if (c[i,j-1] >= c[i-1,j]) c[i,j] = c[i,j-1] else c[i,j] = c[i-1,j] return c end. // column 0 // row 0 Złożoność: 3 pętle: Θ(m) + Θ(n) + Θ(mn) = Θ(mn) 24 Przykład: X = <A, B, A, C, > Y = <A, C, A, B, C> Inicjalizacja: 0 0 1 A 2 B 3 A 4 C 1 2 3 4 5 A C A B C ------------------------| 0 | 0 | 0 | 0 | 0 | 0 | ------------------------| 0 | | | | | | ------------------------| 0 | | | | | | ------------------------| 0 | | | | | | ------------------------| 0 | | | | | | ------------------------- Wynik: 0 0 1 A 2 B 3 A 4 C 1 2 3 4 5 A C A B C ------------------------| 0 | 0 | 0 | 0 | 0 | 0 | ------------------------| 0 | 1 | 1 | 1 | 1 | 1 | ------------------------| 0 | 1 | 1 | 1 | 2 | 2 | ------------------------| 0 | 1 | 1 | 2 | 2 | 2 | ------------------------| 0 | 1 | 2 | 2 | 2 | 3 | ------------------------- 25 Rekonstrukcja optymalnego rozwiązania: b[i, j] – tablica z informacją który spośród c[i-1,j-1], c[,j-1], [i-1,j] został wybrany do obliczenia c[i, j] '\' jeśli c[i, j] = 1 + c[i–1, j–1] '<' jeśli c[i, j] = c[i, j–1] '^' jeśli c[i, j] = c[i–1, j] for (i=1; i<=m; i++) for (j=1; j<=n; j++) if (X[i]=Y[j]) c[i,j] = c[i-1,j-1]+1 b[i,j] '\' else if (c[i,j-1] >= c[i-1,j]) c[i,j] = c[i,j-1] b[i,j] '<' else c[i,j] = c[i-1,j] b[i,j] '^' return c end. wynik – tablica b[]: 0 0 1 A 2 B 3 A 4 C 1 2 3 4 5 A C A B C ------------------------| 0 | 0 | 0 | 0 | 0 | 0 | ------------------------| 0 | \ | < | \ | < | < | ------------------------| 0 | ^ | < | < | \ | < | ------------------------| 0 | \ | < | \ | < | < | ------------------------| 0 | ^ | \ | < | < | \ | ------------------------26 Obliczanie LCS – w kolejności od końca: • • • • zacznij w b[m, n] przesuwaj się zgodnie ze strzałkami dodawaj aktualny element gdy strzałka '\' stop gdy osiągniesz wiersz lub kolumnę 0 Θ(m+n). Uwagi: 1. Łatwe: jeśli obliczamy tylko długość najdłuższego wspólnego podciągu, to wystarcza pamięć O(min{m,n}), dokładniej: wystarcza pamięć na jeden wiersz tablicy a. 2. Łatwe: jeśli obliczamy najdłuższy wspólny podciąg, to do jego rekonstrukcji nie jest potrzebna tablica b; kierunek ruchu podczas rekonstrukcji podciągu od końca do początku można wyznaczyć z tablicy a, mając dostęp do X i Y. 3. Trudne: można obliczyć najdłuższy wspólny podciąg w takim samym czasie Θ(mn) i w pamięci Θ(m+n). 27