Quando posso usar a programação dinâmica para reduzir a complexidade de tempo do meu algoritmo recursivo?

13

A programação dinâmica pode reduzir o tempo necessário para executar um algoritmo recursivo. Eu sei que a programação dinâmica pode ajudar a reduzir a complexidade do tempo dos algoritmos. As condições gerais são tais que, se atendidas por um algoritmo recursivo, implicam que o uso de programação dinâmica reduza a complexidade de tempo do algoritmo? Quando devo usar a programação dinâmica?

Anônimo
fonte

Respostas:

9

A programação dinâmica é útil, pois seu algoritmo recursivo chega às mesmas situações (parâmetros de entrada) muitas vezes. Há uma transformação geral de algoritmos recursivos em programação dinâmica conhecida como memorização , na qual existe uma tabela que armazena todos os resultados já calculados pelo seu procedimento recursivo. Quando o procedimento recursivo é chamado em um conjunto de entradas que já foram usadas, os resultados são apenas buscados na tabela. Isso reduz Fibonacci recursivo a Fibonacci iterativo.

A programação dinâmica pode ser ainda mais inteligente, aplicando otimizações mais específicas. Por exemplo, às vezes não há necessidade de armazenar a tabela inteira na memória a qualquer momento.

Yuval Filmus
fonte
O contador seria que, sempre que a complexidade do espaço da memorização for maior que os dados de entrada (talvez apenas> O (N)), é provável que a programação dinâmica não ajude. Ou seja, quando você raramente encontra a mesma situação.
EdA-qa mort-ora-y
1
Memoisation! = Programação dinâmica!
Raphael
1
Acho que não estamos dizendo isso, mas a pergunta indica reduzir a complexidade do tempo. A programação dinâmica por si só simplesmente particiona o problema. Programação dinâmica + memorização é uma maneira genérica de melhorar a complexidade do tempo sempre que possível .
EdA-qa mort-ora-y
@ edA-qamort-ora-y: Certo. Eu acho importante salientar isso claramente, pois aparentemente o OP confunde / mistura os conceitos.
Raphael
8

Se você apenas procura acelerar seu algoritmo recursivo, a memória pode ser suficiente. Essa é a técnica de armazenar resultados de chamadas de função, para que futuras chamadas com os mesmos parâmetros possam apenas reutilizar o resultado. Isso é aplicável se (e somente se) sua função

  • não tem efeitos colaterais e
  • depende apenas de seus parâmetros (ou seja, não de algum estado).

Você economizará tempo se (e somente se) a função for chamada com os mesmos parâmetros repetidamente. Exemplos populares incluem a definição recursiva dos números de Fibonacci, ou seja,

f(0 0)=0 0f(1)=1f(n+2)=f(n+1)+f(n), n0 0

ff(n)f(n+1)

Observe que, ao contrário, a memória é quase inútil para algoritmos como a classificação por mesclagem: geralmente poucas listas parciais (se houver) são idênticas e as verificações de igualdade são caras (a classificação é apenas um pouco mais cara!).

Em implementações práticas, como você armazena os resultados é de grande importância para o desempenho. Usar tabelas de hash pode ser a escolha óbvia, mas pode quebrar a localidade. Se seus parâmetros forem números inteiros não negativos, as matrizes são uma escolha natural, mas podem causar uma enorme sobrecarga de memória se você usar apenas algumas entradas. Portanto, memoisation é uma troca entre efeito e custo; se vale a pena depende do seu cenário específico.


A programação dinâmica é uma fera completamente diferente. É aplicável a problemas com a propriedade que

  • pode ser particionado em subproblemas (provavelmente de mais de uma maneira),
  • esses subproblemas podem ser resolvidos independentemente,
  • soluções (ótimas) desses subproblemas podem ser combinadas com soluções (ótimas) do problema original e
  • subproblemas têm a mesma propriedade (ou são triviais).

Isso geralmente está implícito quando as pessoas invocam o Princípio de Optimalidade de Bellman .

Agora, isso descreve apenas uma classe de problemas que podem ser expressos por um certo tipo de recursão. A avaliação daquelas é (geralmente) eficiente porque a memória pode ser aplicada com grande efeito (veja acima); geralmente, subproblemas menores ocorrem como partes de muitos problemas maiores. Exemplos populares incluem a distância de edição e o algoritmo Bellman-Ford .

Rafael
fonte
Você está dizendo que há casos em que a programação dinâmica leva a uma melhor complexidade de tempo, mas a memorização não ajuda (ou pelo menos não tanto)? Você tem algum exemplo? Ou você está apenas dizendo que a programação dinâmica é útil apenas para um subconjunto de problemas em que está a memorização?
Svick
@svick: A programação dinâmica não acelera nada em si, apenas se a recursão do DP for avaliada com memórias (o que geralmente é (!)). Novamente: DP é uma maneira de modelar problemas em termos de recursão, memoisation é uma técnica para acelerar algoritmos recursivos adequados (não importa se DP). Não faz sentido comparar os dois diretamente. É claro que você tenta modelar um problema como PD, porque espera aplicar a memória e, portanto, resolvê-lo mais rapidamente do que as abordagens ingênuas (r). Mas um ponto de vista de DP nem sempre leva ao algoritmo mais eficiente.
Raphael
Se você tiver vários processadores disponíveis, a programação dinâmica melhora muito o desempenho no mundo real, pois você pode paralelizar as peças. Na verdade, isso não muda a complexidade do tempo.
EdA-qa mort-ora-y
@ edA-qamort-ora-y: Isso vale para qualquer recursão. No entanto, não está claro que isso crie uma boa aceleração, porque a memória é menos eficiente que os limites do processador.
Raphael
Correção: a avaliação de recorrências de DP ingenuamente ainda pode ser (muito) mais rápida que a força bruta; cf. aqui .
Raphael