Técnica Top-Down e Bottom-Up na programação dinâmica
Abstract
Programação dinâmica é uma técnica que resolve problemas de otimização ao dividir o problema em subproblemas menores, armazenando soluções intermediárias para evitar cálculos redundantes. Suas abordagens principais são as estratégias bottom-up (iterativa) e top-down (recursiva com memoização).
Introdução
Muitos problemas computacionais envolvem a solução de subproblemas repetidos. Para lidar com isso de maneira eficiente, programação dinâmica (PD) utiliza uma abordagem estruturada: ela resolve cada subproblema uma única vez, armazenando seus resultados para evitar recalcular.
As duas principais técnicas de implementação em programação dinâmica são:
- Top-Down: Uma abordagem recursiva com memoização, que resolve subproblemas conforme necessário, armazenando seus resultados.
 - Bottom-Up: Uma abordagem iterativa que resolve subproblemas menores antes de resolver subproblemas maiores, construindo uma solução de maneira incremental.
 
Ambas as técnicas compartilham a mesma ideia central, mas diferem em como o problema é processado e estruturado.
Definição
Programação dinâmica é uma técnica de otimização que utiliza a sobreposição de subproblemas e a subestrutura ótima.
- Sobreposição de Subproblemas: Os mesmos subproblemas aparecem repetidamente durante a solução do problema.
 - Subestrutura Ótima: A solução ótima para um problema pode ser construída a partir das soluções ótimas de seus subproblemas.
 
Seja um problema descrito por uma função de estado 
onde 
Estratégias
- 
Top-Down:
- Começa com o problema maior e divide recursivamente em subproblemas menores.
 - Usa memoização para armazenar os resultados já calculados.
 - Evita cálculos redundantes durante a recursão.
 
 - 
Bottom-Up:
- Resolve os subproblemas menores primeiro, armazenando os resultados.
 - Utiliza tabelas para organizar as soluções intermediárias.
 - É implementada iterativamente, evitando o overhead de chamadas recursivas.
 
 
Detalhamento das Técnicas
Top-Down: Recursão com Memoização
Na estratégia top-down, a função é chamada recursivamente para resolver subproblemas conforme necessário. Sempre que um subproblema é resolvido, seu resultado é armazenado (memoizado).
- Vantagem: Mais fácil de implementar e entender devido à estrutura recursiva.
 - Desvantagem: Pode enfrentar problemas de profundidade de pilha em casos de entrada grande.
 
Exemplo de Implementação (Fibonacci):
def fib(n, memo={}):
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = fib(n-1, memo) + fib(n-2, memo)
    return memo[n]
 
print(fib(10))  # Resultado: 55Bottom-Up: Abordagem Iterativa
Na abordagem bottom-up, a solução começa pelos subproblemas menores e progride iterativamente até resolver o problema maior.
- Vantagem: Não sofre problemas de profundidade de pilha e frequentemente usa menos memória que a abordagem top-down.
 - Desvantagem: Requer planejamento explícito para definir a ordem de resolução dos subproblemas.
 
Exemplo de Implementação (Fibonacci):
def fib(n):
    if n <= 1:
        return n
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]
 
print(fib(10))  # Resultado: 55Comparação entre Top-Down e Bottom-Up
| Característica | Top-Down | Bottom-Up | 
|---|---|---|
| Estrutura | Recursiva | Iterativa | 
| Armazenamento | Memoização (estrutura dinâmica) | Tabela (estrutura fixa) | 
| Facilidade de Implementação | Mais simples (para problemas recursivos) | Pode ser mais desafiador | 
| Eficiência | Pode ser menos eficiente (overhead de recursão) | Geralmente mais eficiente | 
Exemplo
Problema: Mochila Fracionária
Considere um problema de maximizar o valor total de itens que podem ser colocados em uma mochila com capacidade fixa. Suponha que temos itens com pesos e valores conhecidos.
Abordagem Bottom-Up:
Definimos 
onde 
Exemplo de Implementação:
def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        for w in range(capacity + 1):
            if weights[i-1] <= w:
                dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
            else:
                dp[i][w] = dp[i-1][w]
    return dp[n][capacity]
 
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 5
print(knapsack(weights, values, capacity))  # Resultado: 7