Programowanie dynamiczne a podobieństwa typu „dziel i zwyciężaj”
Jak na razie to widzę, mogę powiedzieć, że programowanie dynamiczne jest rozszerzeniem paradygmatu dziel i rządź .
Nie traktowałbym ich jako czegoś zupełnie innego. Ponieważ oba działają na zasadzie rekurencyjnego podziału problemu na dwa lub więcej podproblemów tego samego lub pokrewnego typu, dopóki nie staną się one na tyle proste, aby można je było rozwiązać bezpośrednio. Rozwiązania podproblemów są następnie łączone, aby dać rozwiązanie pierwotnego problemu.
Dlaczego więc nadal mamy różne nazwy paradygmatów i dlaczego nazwałem programowanie dynamiczne rozszerzeniem. Dzieje się tak, ponieważ dynamiczne podejście do programowania można zastosować do problemu tylko wtedy, gdy problem ma pewne ograniczenia lub warunki wstępne . Po tym dynamicznym programowaniu rozszerza się podejście dziel i zwyciężaj z zapamiętywaniem lub techniką tabelaryczną .
Przejdźmy krok po kroku…
Wymagania wstępne / ograniczenia dotyczące programowania dynamicznego
Jak właśnie odkryliśmy, istnieją dwa kluczowe atrybuty, które muszą posiadać problem, który dzieli i pokonuje, aby programowanie dynamiczne mogło mieć zastosowanie:
Optymalna podkonstrukcja - optymalne rozwiązanie można zbudować z optymalnych rozwiązań jej podproblemów
Nakładające się podproblemy - problem można rozbić na podproblemy, które są wykorzystywane wielokrotnie lub rekurencyjny algorytm dla problemu rozwiązuje ten sam podproblem w kółko, zamiast zawsze generować nowe podproblemy
Gdy te dwa warunki zostaną spełnione, możemy powiedzieć, że ten problem „dziel i rządź” można rozwiązać za pomocą metody programowania dynamicznego.
Dynamiczne rozszerzenie programowania dla dziel i rządź
Dynamiczne podejście do programowania rozszerza podejście dziel i zwyciężaj za pomocą dwóch technik ( zapamiętywania i tworzenia tabel ), które mają na celu przechowywanie i ponowne wykorzystanie rozwiązań podproblemów, które mogą drastycznie poprawić wydajność. Na przykład naiwna rekurencyjna implementacja funkcji Fibonacciego ma złożoność czasową, w O(2^n)
której rozwiązanie DP robi to samo tylko z O(n)
czasem.
Memoizacja (wypełnianie pamięci podręcznej z góry na dół) odnosi się do techniki buforowania i ponownego wykorzystywania wcześniej obliczonych wyników. Zapamiętana fib
funkcja wyglądałaby więc następująco:
memFib(n) {
if (mem[n] is undefined)
if (n < 2) result = n
else result = memFib(n-2) + memFib(n-1)
mem[n] = result
return mem[n]
}
Tabulacja (wypełnianie pamięci podręcznej od dołu do góry) jest podobna, ale koncentruje się na wypełnianiu wpisów w pamięci podręcznej. Obliczanie wartości w pamięci podręcznej jest najłatwiejsze do wykonania iteracyjnie. Wersja tabelaryczna programu fib
wyglądałaby następująco:
tabFib(n) {
mem[0] = 0
mem[1] = 1
for i = 2...n
mem[i] = mem[i-2] + mem[i-1]
return mem[n]
}
Możesz przeczytać więcej o zapamiętywaniu i porównaniu tabelarycznym tutaj .
Główną ideą, którą powinieneś tu zrozumieć, jest to, że ponieważ nasz problem dziel i rządź ma nakładające się podproblemy, możliwe staje się buforowanie rozwiązań podproblemów, a zatem zapamiętywanie / tabulacja wkraczają na scenę.
Więc jaka jest różnica między DP a DC
Ponieważ jesteśmy teraz zaznajomieni z warunkami wstępnymi DP i jej metodologiami, jesteśmy gotowi umieścić wszystko, o czym wspomniano powyżej, w jednym obrazku.
Jeśli chcesz zobaczyć przykłady kodu, możesz przyjrzeć się bardziej szczegółowemu wyjaśnieniu tutaj, w którym znajdziesz dwa przykłady algorytmów: wyszukiwanie binarne i minimalna odległość edycji (odległość Levenshteina), które ilustrują różnicę między DP i DC.