Programação dinâmica em problemas de olimpíadas

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 ke quero encontrar a resposta para k+1. 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 nnúmero da sequência de Fibonacci. Sabe-se que a sequência possui as seguintes propriedades:

F0=F1=1, Fn=Fn1+Fn2.


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 0e n:

 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 nesse 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 ipasso que você tem que pagar aimoedas. Você pode passar para o próximo passo ou pular um. Tarefa: passar npassos 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 dem que jo quinto lugar será o número (mínimo) de moedas que devem ser gastas para chegar a jª etapa. É imediatamente claro que d1=a1, d2=a2. E então tomaremos no mínimo as duas etapas anteriores e adicionaremos o custo da própria etapa:

di= min left(di1,di2 right)+ai.



Mudamos um pouco as condições do problema: suponha que em algumas etapas você possa obter moedas (isso significa que ak<0) O que precisa ser alterado no algoritmo para que ele dê o resultado correto?

Solução
Só é 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 a1<0, é melhor avançar e coletar suas moedas. Então d2= min esquerda(0,d1 direita)+a2.

Considere outro exemplo que usa dinâmica "bidimensional".

Exemplo 2. No labirinto há n vezesmquartos, cada um dos quais contém ouro (em uma gaiola (i,j)mentiras aijouro). A tarefa é determinar qual quantidade máxima de ouro pode ser coletada com uma rota ideal a partir de um ponto (0,0)direto ao ponto (n,m)se você pode ir para baixo ou para a direita.

Então, queremos saber a melhor rota para a célula (i,j). Podemos chegar aqui de duas células - (i1,j)e (i,j1). Dado que as rotas ideais para essas duas células são conhecidas (elas são armazenadas em alguma tabela d), então a resposta para a célula (i,j)obtido da seguinte forma:

dij= max left(di1j,dij1 right)+aij.


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 k1que são armazenados em alguma matriz de gostaríamos de encontrar dk.

Pegue esse número ke analise quais situações podem ser:

  1. ké um quadrado cheio. Neste caso dk=1.
  2. Talvez o número anterior k1era um quadrado completo. Então dk=dk1+1.

Em geral, a opção de adicionar uma unidade à anterior não parece tão ruim.

Procuramos da seguinte forma: buscamos uma decomposição k=q2+stal que

dq2+ds<dk1+1.

Desde q2- quadrado cheio então dq2=1e

ds<dk1,

ou seja, encontramos uma partição simplesmente melhor do que dk1+1, e a resposta neste caso será

dk=ds+dq2=ds+1.



Exemplo de código Java que implementa esse algoritmo:
 for(int k = 1; k <= n; k++) { int best = d[k - 1] + 1; //      int q = 1; while(k - q*q >= 0) { // k = q*q + s if(k - q*q == 0) { // k -   best = 1; break; } else if(d[k - q*q] < best) best = d[k - q*q] + 1; q++; } d[k] = best; } 


Considere o seguinte problema . O objetivo é construir uma escada de Ncubos de acordo com as regras:

  1. a escada tem pelo menos dois degraus;
  2. uma escada não pode ter dois degraus idênticos;
  3. 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 dem que a posição (i,j)o número de escadas constituídas por icubos cuja altura não exceda j. Se der certo, a resposta para o nosso problema será a soma

 sum limitsj=1ndnj.


Então, resolveremos o problema de encontrar o número de escadas compostas por icubos que são altos j. A imagem mostra as escadas que caem d74:

Como conhecemos todas as escadas, que consistem em menos cubos, "separaremos" as escadas (i,j)coluna da direita. O resultado é uma escada c ijcubos. Exemplo para i=9, j=4:

Mas para essas escadas, o resultado já é conhecido, portanto, classificaremos todas essas escadas com um ciclo em ke adicione todos os resultados. Desta maneira

dij= soma limitesk=1j1dijk.


Agora vamos classificar as alturas das escadas:

dij= soma limitesk=1j1dijk, j= overline1,i.

Finalmente, mudar ide 1antes nnós obtemos a resposta.

Importante : no processo de construção de nossa matriz, é necessário levar em consideração dii=1, 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 dnn1.

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; //    } long answer = 0L; for(int i = 0; i <= n; i++) { answer += d[n][i]; } answer--; //     


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 Kpalavras (é necessário deduzir o número desses módulos ents P)

A solução é bastante simples. Crie uma matriz dem que i-th lugar vamos armazenar o número de ents (módulo P) quem sabe ipalavras. Tudo começa com o primeiro ent, que conhece duas palavras, portanto d2=1. 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 i: di=di1;
  • 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 inós temos di=di barrainvertida2+di1.

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:

  1. Timus Online Judge;
  2. Um pouco sobre programação dinâmica;
  3. Módulo de propriedades de comparação.

Source: https://habr.com/ru/post/pt418867/


All Articles