rev4: Bardzo wymowny komentarz użytkownika Sammaron zauważył, że być może ta odpowiedź była wcześniej mylona z góry na dół i z dołu do góry. Chociaż pierwotnie ta odpowiedź (rev3) i inne odpowiedzi mówiły, że „oddolne jest zapamiętywaniem” („załóżmy podproblemy”), może być odwrotne (to znaczy „odgórne” może oznaczać „zakładanie podproblemów” i „ oddolnie „może być„ składać podproblemy ”). Wcześniej czytałem o zapamiętywaniu będącym innym rodzajem programowania dynamicznego w przeciwieństwie do podtypu programowania dynamicznego. Cytowałem ten punkt widzenia, mimo że go nie podzielałem. Przepisałem tę odpowiedź tak, aby była agnostyczna w stosunku do terminologii, dopóki w literaturze nie będzie można znaleźć odpowiednich odniesień. Przekonwertowałem również tę odpowiedź na wiki społeczności. Proszę preferować źródła akademickie. Lista referencji:} {Literatura: 5 }
Podsumować
Programowanie dynamiczne polega na porządkowaniu obliczeń w taki sposób, aby uniknąć ponownego obliczania powielonych prac. Masz główny problem (korzeń twojego drzewa podproblemów) i podproblemy (poddrzewa). Podproblemy zwykle się powtarzają i nakładają .
Weźmy na przykład pod uwagę swój ulubiony przykład Fibonnaci. To jest pełne drzewo podproblemów, gdybyśmy wykonali naiwne wywołanie rekurencyjne:
TOP of the tree
fib(4)
fib(3)...................... + fib(2)
fib(2)......... + fib(1) fib(1)........... + fib(0)
fib(1) + fib(0) fib(1) fib(1) fib(0)
fib(1) fib(0)
BOTTOM of the tree
(W niektórych innych rzadkich problemach drzewo to może być nieskończone w niektórych gałęziach, reprezentując brak zakończenia, a zatem jego spód może być nieskończenie duży. Ponadto w niektórych problemach możesz nie wiedzieć, jak wygląda pełne drzewo przed Dlatego możesz potrzebować strategii / algorytmu, aby zdecydować, które podproblemy ujawnić).
Memoization, Tabulation
Istnieją co najmniej dwie główne techniki programowania dynamicznego, które nie wykluczają się wzajemnie:
Zapamiętywanie - jest to podejście laissez-faire: zakładasz, że obliczyłeś już wszystkie podproblemy i nie masz pojęcia, jaka jest optymalna kolejność oceny. Zazwyczaj wykonywałbyś wywołanie rekurencyjne (lub jakiś iteracyjny odpowiednik) z korzenia i albo miałbyś nadzieję, że zbliżysz się do optymalnej kolejności oceny, albo uzyskasz dowód, że pomożesz ci dojść do optymalnej kolejności oceny. Zapewniłbyś, że rekurencyjne wywołanie nigdy nie przelicza podproblemu, ponieważ buforujesz wyniki, a zatem zduplikowane poddrzewa nie są ponownie obliczane.
- Przykład: Jeśli obliczasz ciąg Fibonacciego
fib(100)
, po prostu wywołałbyś to, a on zadzwoniłby fib(100)=fib(99)+fib(98)
, który wywołałby fib(99)=fib(98)+fib(97)
, ... itd ..., który zadzwoni fib(2)=fib(1)+fib(0)=1+0=1
. W końcu to rozwiązałoby się fib(3)=fib(2)+fib(1)
, ale nie trzeba go ponownie obliczać fib(2)
, ponieważ zapisaliśmy go w pamięci podręcznej.
- Rozpoczyna się na szczycie drzewa i ocenia podproblemy od liści / poddrzewów z powrotem do korzenia.
Tabulacja - Możesz również myśleć o programowaniu dynamicznym jako o algorytmie "wypełniania tabeli" (choć zwykle jest to wielowymiarowe, ta 'tabela' może w bardzo rzadkich przypadkach mieć geometrię nieeuklidesową *). Jest to podobne do zapamiętywania, ale bardziej aktywne i obejmuje jeden dodatkowy krok: musisz z wyprzedzeniem wybrać dokładną kolejność wykonywania obliczeń. Nie powinno to oznaczać, że kolejność musi być statyczna, ale masz znacznie większą elastyczność niż zapamiętywanie.
- Przykład: Jeśli wykonujesz Fibonacciego, można wybrać do obliczania liczby w tej kolejności:
fib(2)
, fib(3)
, fib(4)
... buforowanie każdą wartość, dzięki czemu można łatwiej obliczyć kolejnych. Możesz również myśleć o tym jako o zapełnianiu tabeli (inna forma buforowania).
- Osobiście nie słyszę często słowa „tabulacja”, ale jest to bardzo przyzwoity termin. Niektórzy uważają to za „programowanie dynamiczne”.
- Przed uruchomieniem algorytmu programista bierze pod uwagę całe drzewo, a następnie pisze algorytm do oceny podproblemów w określonej kolejności w kierunku korzenia, generalnie wypełniając tabelę.
- * przypis: Czasami „tabela” nie jest tabelą prostokątną z połączeniami podobnymi do siatki per se. Może raczej mieć bardziej skomplikowaną strukturę, taką jak drzewo lub strukturę specyficzną dla domeny problemowej (np. Miasta w niewielkiej odległości na mapie), a nawet diagram kratowy, który, choć podobny do siatki, nie ma struktura połączeń góra-dół-lewa-prawa itd. Na przykład user3290797 połączył przykład programowania dynamicznego polegający na znalezieniu maksymalnego niezależnego zbioru w drzewie , co odpowiada wypełnieniu pustych miejsc w drzewie.
(Najogólniej mówiąc, w paradygmacie „programowania dynamicznego” powiedziałbym, że programista bierze pod uwagę całe drzewo, a następniepisze algorytm, który implementuje strategię oceny podproblemów, która może zoptymalizować dowolne właściwości, jakie chcesz (zwykle połączenie złożoności czasowej i złożoności przestrzennej). Twoja strategia musi się gdzieś zacząć, od jakiegoś konkretnego podproblemu i być może może się dostosować w oparciu o wyniki tych ocen. W ogólnym sensie „programowania dynamicznego”, możesz spróbować buforować te podproblemy, a bardziej ogólnie, unikać powracania do podproblemów z subtelnym rozróżnieniem, być może w przypadku wykresów w różnych strukturach danych. Bardzo często te struktury danych są rdzeniem, podobnie jak tablice lub tabele. Rozwiązania podproblemów można wyrzucić, jeśli już ich nie potrzebujemy).
[Wcześniej ta odpowiedź zawierała stwierdzenie dotyczące terminologii odgórnej i oddolnej; wyraźnie istnieją dwa główne podejścia zwane memoization i tabulation, które mogą być sprzeczne z tymi terminami (choć nie do końca). Ogólnym terminem używanym przez większość ludzi jest nadal „programowanie dynamiczne”, a niektórzy ludzie mówią „zapamiętanie” w odniesieniu do tego konkretnego podtypu „programowania dynamicznego”. Ta odpowiedź nie mówi, która jest odgórna i oddolna, dopóki społeczność nie znajdzie odpowiednich odniesień w artykułach naukowych. Ostatecznie ważne jest, aby zrozumieć rozróżnienie, a nie terminologię.]
Plusy i minusy
Łatwość kodowania
Memoizacja jest bardzo łatwa do zakodowania (generalnie możesz * napisać adnotację "memoizer" lub funkcję wrapper, która zrobi to automatycznie za ciebie) i powinna być twoją pierwszą linią podejścia. Wadą tabeli jest to, że musisz wymyślić porządek.
* (w rzeczywistości jest to łatwe tylko wtedy, gdy sam piszesz funkcję i / lub kodujesz w nieczystym / niefunkcjonalnym języku programowania ... na przykład, jeśli ktoś już napisał prekompilowaną fib
funkcję, koniecznie wykonuje ona rekursywne wywołania do siebie i nie możesz magicznie zapamiętać funkcji bez upewnienia się, że te rekurencyjne wywołania wywołują twoją nową zapamiętaną funkcję (a nie oryginalną niezapamiętywaną funkcję))
Rekurencyjność
Zwróć uwagę, że zarówno od góry do dołu, jak i od dołu do góry można zaimplementować za pomocą rekursji lub iteracyjnego wypełniania tabeli, chociaż może to nie być naturalne.
Praktyczne obawy
Przy zapamiętywaniu, jeśli drzewo jest bardzo głębokie (np. fib(10^6)
), Zabraknie Ci miejsca na stosie, ponieważ każde opóźnione obliczenie musi zostać umieszczone na stosie, a będziesz miał ich 10 ^ 6.
Optymalność
Każde podejście może nie być optymalne czasowo, jeśli kolejność odwiedzania podproblemów nie jest optymalna, szczególnie jeśli istnieje więcej niż jeden sposób obliczenia podproblemu (zwykle buforowanie rozwiązałoby ten problem, ale teoretycznie jest możliwe, że buforowanie może nie w niektórych egzotycznych przypadkach). Memoizacja zwykle dodaje złożoność czasową do złożoności przestrzeni (np. W przypadku tworzenia tabel masz większą swobodę w odrzucaniu obliczeń, na przykład używanie tabelowania z Fib pozwala na użycie przestrzeni O (1), ale zapamiętywanie z Fib wykorzystuje O (N) miejsce na stosie).
Zaawansowane optymalizacje
Jeśli również wykonujesz niezwykle skomplikowane problemy, możesz nie mieć innego wyjścia, jak tylko dokonać zestawienia (lub przynajmniej wziąć bardziej aktywną rolę w kierowaniu zapamiętywaniem tam, gdzie chcesz). Również jeśli jesteś w sytuacji, w której optymalizacja jest absolutnie krytyczna i musisz ją zoptymalizować, tabulacja pozwoli ci dokonać optymalizacji, których zapamiętywanie nie pozwoliłoby ci zrobić w rozsądny sposób. Moim skromnym zdaniem w normalnej inżynierii oprogramowania żaden z tych dwóch przypadków nigdy się nie pojawia, więc użyłbym po prostu zapamiętywania („funkcji przechowującej odpowiedzi w pamięci podręcznej”), chyba że coś (na przykład miejsce na stosie) sprawia, że konieczne jest tworzenie tabel ... chociaż technicznie, aby uniknąć przepalenia stosu, możesz 1) zwiększyć limit rozmiaru stosu w językach, które na to pozwalają, lub 2) zużyć stałą dodatkową pracę w celu wirtualizacji stosu (ick),
Bardziej skomplikowane przykłady
Podajemy tutaj szczególnie interesujące przykłady, które nie są tylko ogólnymi problemami DP, ale ciekawie rozróżniają zapamiętywanie i tabelowanie. Na przykład jedna formuła może być znacznie łatwiejsza niż druga lub może istnieć optymalizacja, która zasadniczo wymaga tabelowania:
- algorytm obliczania odległości edycji [ 4 ], interesujący jako nietrywialny przykład dwuwymiarowego algorytmu wypełniania tabel