Oi
Recentemente, resolvi problemas do arquivo
Timus Online Judge e me deparei com uma seção chamada
tarefas de programação dinâmica . Esse tipo de tarefa é de particular interesse para mim, porque muitas vezes essa abordagem garante a velocidade e a elegância da solução. O que é programação dinâmica?
A programação dinâmica é uma abordagem para resolver problemas em que há uma divisão em subtarefas que são "mais simples" em comparação com a original. A palavra "dinâmico" tem significado próximo de "indutivo": pressupõe-se que a resposta seja conhecida por algum significado
e quero encontrar a resposta para
. Em matemática, isso é chamado de transição indutiva e é a principal idéia da programação dinâmica.
Exemplos simples
A tarefa mais marcante e indicativa é a tarefa de calcular
número da sequência de Fibonacci.
Sabe-se que a sequência possui as seguintes propriedades:
Isso implica imediatamente a fórmula de recorrência:
int Fibonacci(int n) { if(n == 1 || n == 2) return 1; return Fibonacci(n-1) + Fibonacci(n-2); }
Se a recursão estiver procurando por um número "do final", o método a seguir calcula sequencialmente todos os números localizados entre
e
:
int dpFibonacci(int n) { int prev1 = 1; int prev2 = 1; int curr = 0; for(int j = 2; j < n; j++) { curr = prev1 + prev2; prev1 = prev2; prev2 = curr; } return curr; }
É claro que para empresas suficientemente grandes
esse algoritmo funciona muito mais rápido: ele não calcula valores intermediários várias vezes. Considere um exemplo um pouco mais complexo.
Exemplo 1. Você está andando em uma escada de pedágio. Para pisar
passo que você tem que pagar
moedas. Você pode passar para o próximo passo ou pular um. Tarefa: passar
passos e gaste o mínimo de moedas possível.
É claro que, percorrendo cada etapa, minimizamos o número de "pagamentos", mas podemos executar uma etapa muito cara, que gostaríamos de evitar. Crie uma matriz de valores
em que
o quinto lugar será o número (mínimo) de moedas que devem ser gastas para chegar a
ª etapa. É imediatamente claro que
. E então tomaremos no mínimo as duas etapas anteriores e adicionaremos o custo da própria etapa:
Mudamos um pouco as condições do problema: suponha que em algumas etapas você possa obter moedas (isso significa que
) O que precisa ser alterado no algoritmo para que ele dê o resultado correto?
SoluçãoSó é necessário mudar o "começo" de nossa dinâmica. Se a primeira escada não nos trouxer moedas, é aconselhável pular sobre ela, no entanto, se , é melhor avançar e coletar suas moedas. Então .
Considere outro exemplo que usa dinâmica "bidimensional".
Exemplo 2. No labirinto há
quartos, cada um dos quais contém ouro (em uma gaiola
mentiras
ouro). A tarefa é determinar qual quantidade máxima de ouro pode ser coletada com uma rota ideal a partir de um ponto
direto ao ponto
se você pode ir para baixo ou para a direita.
Então, queremos saber a melhor rota para a célula
. Podemos chegar aqui de duas células -
e
. Dado que as rotas ideais para essas duas células são conhecidas (elas são armazenadas em alguma tabela
), então a resposta para a célula
obtido da seguinte forma:
Essa é outra tarefa clássica de programação dinâmica, cujas modificações são bastante comuns em tarefas de programação esportiva. Uma tarefa semelhante é explicada em mais detalhes
aqui .
Tarefas mais desafiadoras
Se desejado, uma abordagem dinâmica pode ser parafusada onde você quiser. Considere uma
tarefa do arquivo Timus Online Judge.
A formulação matemática do problema é a seguinte: é necessário encontrar o número mínimo de termos necessários para decompor um determinado número em quadrados completos.
Como antes, suponha que sabemos as respostas para todos os números
que são armazenados em alguma matriz
e gostaríamos de encontrar
.
Pegue esse número
e analise quais situações podem ser:
- é um quadrado cheio. Neste caso .
- Talvez o número anterior era um quadrado completo. Então .
Em geral, a opção de adicionar uma unidade à anterior não parece tão ruim.
Procuramos da seguinte forma: buscamos uma decomposição
tal que
Desde
- quadrado cheio então
e
ou seja, encontramos uma partição simplesmente melhor do que
, e a resposta neste caso será
Exemplo de código Java que implementa esse algoritmo: for(int k = 1; k <= n; k++) { int best = d[k - 1] + 1;
Considere o seguinte
problema . O objetivo é construir uma escada de
cubos de acordo com as regras:
- a escada tem pelo menos dois degraus;
- uma escada não pode ter dois degraus idênticos;
- os degraus da escada estão em ordem crescente (ou seja, o próximo é maior que o anterior).
Desta vez, construiremos uma dinâmica bidimensional. Crie uma tabela
em que a posição
o número de escadas constituídas por
cubos cuja altura não exceda
. Se der certo, a resposta para o nosso problema será a soma
Então, resolveremos o problema de encontrar o número de escadas compostas por
cubos que são altos
. A imagem mostra as escadas que caem
:

Como conhecemos todas as escadas, que consistem em menos cubos, "separaremos" as escadas
coluna da direita. O resultado é uma escada c
cubos. Exemplo para
:

Mas para essas escadas, o resultado já é conhecido, portanto, classificaremos todas essas escadas com um ciclo em
e adicione todos os resultados. Desta maneira
Agora vamos classificar as alturas das escadas:
Finalmente, mudar
de
antes
nós obtemos a resposta.
Importante : no processo de construção de nossa matriz, é necessário levar em consideração
, pois, caso contrário, alguns tipos de escadas serão "perdidos" (quando "separados"), mas não é preciso dizer que essa escada não satisfaz as condições do problema; portanto, a resposta será o número
.
Exemplo de código Java que implementa esse algoritmo: dp = new long[n + 1][n+1]; d[1][1] = 1; d[2][1] = 0; d[2][2] = 1; for(int i = 3; i < n + 1; i++) { for(int j = 2; j <i; j++) { long cnt = 0; for(int k = 1; k < j; k++) { cnt += d[i - j][k]; } d[i][j] = cnt; } d[i][i] = 1;
A próxima
tarefa é resolvida usando uma matriz unidimensional.
Então, o que temos. O primeiro ent sabe 2 palavras. Cada um ensina todas as palavras que ele conhece a si próprio dois: jovens e velhos. Por sua vez, os jovens foram ensinados com tantas palavras quanto ele já sabia, e os velhos foram ensinados apenas uma palavra. Você precisa saber quantos Ents sabem exatamente
palavras (é necessário deduzir o número desses módulos ents
)
A solução é bastante simples. Crie uma matriz
em que
-th lugar vamos armazenar o número de ents (módulo
) quem sabe
palavras. Tudo começa com o primeiro ent, que conhece duas palavras, portanto
. E então tudo é simples:
- Todas as entradas que conhecem um número ímpar de palavras são antigas e só podem aprender com as anteriores. Portanto, para ímpares
- Quanto aos ents que conhecem um número par de palavras, são todos aqueles que receberam a mesma quantidade de palavras dos elfos (jovens) aqueles que aprenderam com o anterior (antigo); isto é, mesmo nós temos .
Resta lidar com o módulo de cálculo. Para não armazenar grandes números, lembraremos imediatamente de todos os valores do módulo.
Exemplo de código Java que implementa esse algoritmo: int[] d = new int[K + 1]; if(K >= 2) d[2] = 1; if(P != 1) { for(int i = 3; i <= K; i++) { if(i % 2 != 0) { d[i] = d[i - 1]; } else { d[i] = ((d[i/2] % P) + d[i - 1] % P) % P; } } } else d[K] = 0;
Recursos utilizados:
- Timus Online Judge;
- Um pouco sobre programação dinâmica;
- Módulo de propriedades de comparação.