Lançado · em melhoria
Guia de Programação dinâmica · 1/6
Por enquanto, este capítulo está disponível apenas em inglês.
Dynamic programming (DP) is a way of solving a problem by breaking it into smaller subproblems, solving each subproblem once, storing the answer, and reusing it whenever the same subproblem comes up again. The name comes from Richard Bellman, who developed the method in the 1950s; "programming" here means planning with a table, not writing code. This chapter explains when DP applies, the two properties that make it work, and the vocabulary used in the rest of the guide.
A problem is a good fit for DP when it has both of these properties.
Divide and conquer also splits a problem into subproblems, but in merge sort the halves never overlap, so there is nothing to reuse. Greedy algorithms also rely on optimal substructure, but they commit to one locally best choice at each step; DP instead considers every choice and keeps the best one, which is why it works on problems where greedy fails.
The Fibonacci numbers are defined by F(0) = 0, F(1) = 1 and F(n) = F(n-1) + F(n-2). Translating the definition directly into code works, but it is extremely slow because the same values are recomputed again and again.
calls = 0
def fib(n: int) -> int:
global calls
calls += 1
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(25), calls) # 75025 242785Computing F(25) takes almost a quarter of a million calls, and the count grows roughly like 1.6^n. Yet there are only 26 distinct subproblems, F(0) through F(25). That gap between "calls made" and "distinct subproblems" is exactly what DP removes.
Memoization (top-down) keeps the recursive structure and adds a cache. The first call for a given argument computes the result; later calls return the stored value. In Python, functools.cache does this with one decorator.
from functools import cache
@cache
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(90)) # 2880067194370816120
print(fib.cache_info()) # CacheInfo(hits=88, misses=91, maxsize=None, currsize=91)Tabulation (bottom-up) removes recursion entirely. You decide an order in which every subproblem's dependencies are already solved, then fill a table in that order with a loop.
def fib(n: int) -> int:
table = [0] * (n + 1)
if n > 0:
table[1] = 1
for i in range(2, n + 1):
table[i] = table[i - 1] + table[i - 2]
return table[n]
print(fib(90)) # 2880067194370816120Both versions compute each of the n + 1 values once, so both run in O(n) time. Memoization is usually quicker to write and only visits states that are actually reachable. Tabulation avoids recursion depth limits and function call overhead, and it makes space optimizations easier to see.
| Term | Meaning | Fibonacci example |
|---|---|---|
| State | The parameters that identify one subproblem | n |
| Recurrence (transition) | How a state's answer is built from smaller states | F(n) = F(n-1) + F(n-2) |
| Base case | States answered directly without recursion | F(0) = 0, F(1) = 1 |
| Table / memo | Where answers are stored | list table or the @cache dictionary |
| Order of evaluation | The order in which bottom-up fills the table | increasing n |
| Answer | The state (or combination of states) you return | F(n) |
Look for DP when a problem asks for one of these and the input is small enough for a table of states to fit in memory.
A strong hint is a solution that makes a sequence of choices, where after each choice the rest of the problem looks like a smaller copy of the original. If two different sequences of choices can lead to the same remaining problem, you have overlapping subproblems.
0 comentários
Fazer login · Faça login para deixar um comentário.
Seja o primeiro a comentar.