Programación dinámica en problemas olímpicos

Hola

Recientemente resolví problemas del archivo Timus Online Judge y encontré una sección llamada tareas de programación dinámica . Este tipo de tarea es de particular interés para mí, porque a menudo este enfoque garantiza la velocidad y la elegancia de la solución. ¿Qué es la programación dinámica?

La programación dinámica es un enfoque para resolver problemas en el que hay una división en subtareas que son "más simples" en comparación con la original. La palabra "dinámico" tiene un significado cercano a "inductivo": se supone que la respuesta se conoce por algún significado k, y quiero encontrar la respuesta para k+1. En matemáticas, esto se llama transición inductiva y es la idea principal de la programación dinámica.

Ejemplos simples


La tarea más llamativa e indicativa es la tarea de computación nnúmero de la secuencia de Fibonacci. Se sabe que la secuencia tiene las siguientes propiedades:

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


Esto implica inmediatamente la fórmula de recurrencia:

int Fibonacci(int n) { if(n == 1 || n == 2) return 1; return Fibonacci(n-1) + Fibonacci(n-2); } 

Si la recursión está buscando un número "desde el final", entonces el siguiente método calcula secuencialmente todos los números ubicados entre 0y 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; } 

Está claro que para lo suficientemente grande nEste algoritmo funciona mucho más rápido: no calcula valores intermedios varias veces. Considere un ejemplo un poco más complejo.

Ejemplo 1. Estás caminando por una escalera de peaje. Pisar ipaso tienes que pagar aimonedas Puede pasar al siguiente paso o saltar sobre uno. Tarea: pasar npasos y gastar la menor cantidad de monedas posible.

Está claro que al pasar por cada paso, minimizamos el número de "pagos", pero podemos encontrar un paso muy costoso, que nos gustaría evitar. Crea una matriz de valores den el cual en j-el puesto será el número (mínimo) de monedas que se deben gastar para llegar a jpaso Está claro de inmediato que d1=a1, d2=a2. Y luego tomaremos un mínimo de los dos pasos anteriores y agregaremos el costo del paso en sí:

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



Cambiamos un poco las condiciones del problema: supongamos que en algunos pasos puede obtener monedas (esto significará que ak<0) ¿Qué hay que cambiar en el algoritmo para que dé el resultado correcto?

Solución
Solo es necesario cambiar el "comienzo" de nuestra dinámica. Si la primera escalera no nos trae monedas, entonces es aconsejable saltar sobre ella, sin embargo, si a1<0, es mejor pisar y recoger tus monedas. Entonces d2= min left(0,d1 right)+a2.

Considere otro ejemplo que utiliza dinámicas "bidimensionales".

Ejemplo 2. En el laberinto hay n vecesmhabitaciones, cada una de las cuales contiene oro (en una jaula (i,j)mentiras aijoro). La tarea es determinar qué cantidad máxima de oro se puede recolectar con una ruta óptima desde un punto (0,0)hasta el punto (n,m)si puedes ir hacia abajo o hacia la derecha.

Entonces, queremos saber la mejor ruta a la celda (i,j). Podemos llegar aquí desde dos celdas: (i1,j)y (i,j1). Dado que se conocen las rutas óptimas para estas dos celdas (se almacenan en alguna tabla d), entonces la respuesta para la celda (i,j)obtenido de la siguiente manera:

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


Esta es otra tarea de programación dinámica clásica, cuyas modificaciones son bastante comunes en las tareas de programación deportiva. Una tarea similar se explica con más detalle aquí .

Tareas más desafiantes

Si lo desea, se puede atornillar un enfoque dinámico donde lo desee. Considere una tarea del archivo Timus Online Judge.

La formulación matemática del problema es la siguiente: se requiere encontrar el número mínimo de términos necesarios para descomponer un número dado en cuadrados completos.

Como antes, supongamos que sabemos las respuestas para todos los números. k1que se almacenan en alguna matriz dy nos gustaría encontrar dk.

Toma este número ky analizar qué situaciones pueden ser:

  1. kEs una plaza llena. En este caso dk=1.
  2. Quizás el número anterior k1Era un cuadrado completo. Entonces dk=dk1+1.

En general, la opción de agregar una unidad a la anterior no parece tan mala.

Procedemos de la siguiente manera: buscamos una descomposición k=q2+stal que

dq2+ds<dk1+1.

Desde q2- plaza completa entonces dq2=1y

ds<dk1,

es decir, encontramos una partición que es simplemente mejor que dk1+1, y la respuesta en este caso será

dk=ds+dq2=ds+1.



Ejemplo de código Java que implementa este 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 el siguiente problema . El objetivo es construir una escalera desde Ncubos según las reglas:

  1. la escalera tiene al menos dos escalones;
  2. una escalera no puede tener dos escalones idénticos;
  3. los escalones de las escaleras van en orden ascendente (es decir, el siguiente es más grande que el anterior).

Esta vez construiremos dinámicas bidimensionales. Crear una tabla den el que el puesto (i,j)el número de escaleras que consiste en icubos cuya altura no exceda j. Si funciona, la respuesta a nuestro problema será la suma.

 sum limitsj=1ndnj.


Entonces, resolveremos el problema de encontrar el número de escaleras compuesto por icubos que son altos j. La imagen muestra las escaleras que caen en d74:

Como conocemos todas las escaleras, que consisten en menos cubos, las dividiremos. (i,j)columna derecha El resultado es una escalera c ijcubos Ejemplo para i=9, j=4:

Pero para tales escaleras, el resultado ya se conoce, por lo que clasificaremos todas esas escaleras con un ciclo en ky sumar todos los resultados. De esta manera

dij= sum limitsk=1j1dijk.


Ahora clasificaremos las alturas de las escaleras:

dij= sum limitsk=1j1dijk, j= overline1,i.

Finalmente cambiando ide 1antes nObtenemos la respuesta.

Importante : en el proceso de construcción de nuestra matriz, es necesario considerar dii=1, ya que de lo contrario algunos tipos de escaleras se “perderán” (cuando se “separen”), pero no hace falta decir que dicha escalera no satisface las condiciones del problema, por lo que la respuesta será el número dnn1.

Ejemplo de código Java que implementa este 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--; //     


La siguiente tarea se resuelve utilizando una matriz unidimensional.

Entonces lo que tenemos. El primero ent sabe 2 palabras. Cada ent enseña todas las palabras que él mismo conoce dos ents: jóvenes y viejos. A su vez, a los jóvenes se les enseñaron tantas palabras como él ya sabe, y al viejo solo se le enseñó una palabra. Necesitas saber cuántos ents saben exactamente Kpalabras (es necesario deducir el número de estos módulos ents P)

La solución es bastante simple. Crea una matriz den el cual en i-º lugar almacenaremos el número de ents (módulo P) quien sabe ipalabras Todo comienza con el primer ent, que conoce dos palabras, por lo tanto d2=1. Y luego todo es simple:

  • Todos los entrantes que conocen un número impar de palabras son viejos y solo pudieron aprender de los anteriores. Por lo tanto para impar i: di=di1;
  • En cuanto a los entrantes que saben un número par de palabras, estos son todos aquellos que recibieron la misma cantidad de palabras de los elfos (jóvenes) +aquellos que han aprendido de lo anterior (antiguo); es decir, incluso itenemos di=di barrainvertida2+di1.

Queda por tratar con el módulo de cálculo. Para no almacenar grandes cantidades, recordaremos de inmediato todos los módulos de valores.

Ejemplo de código Java que implementa este 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 Juez en línea;
  2. Un poco sobre programación dinámica;
  3. Propiedades de comparación módulo.

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


All Articles