Chef and Rainbow Array - 2 (RAINBOWB - CodeChef)

por
Edit

Link al problema: RAINBOWB

Dificultad: Medio

Problema: Dado un número $N$, $1 ≤ N ≤ 10^6$, encontrar todas las maneras posibles de formar un “arreglo arcoiris” de tamaño $N$. Un arrelgo arcoíris contiene todos los números del 1 al 7 ordenados de manera ascendente hasta la mitad del arreglo, mientras que para la otra mitad contiene los la misma cantidad pero ordenados de manera descendente. Este es un ejemplo de un arreglo de ese tipo con $N = 15$: $\{1,\ 2,\ 3,\ 4,\ 5,\ 6,\ 7,\ 7,\ 7,\ 6,\ 5,\ 4,\ 3,\ 2,\ 1\}$.

Solución: (Existe solución en $O(1)$ para este problema, sin embargo aquí se expone una solución empleando la programación dinámica)

Primero que nada debemos identificar que podemos trabajar con una sola mitad del arreglo, puesto que la otra es igual. Después debemos identificar que para cada $N$ que nos den existen $R$ números extras (repetidos) que podemos colocar, (en el ejemplo presentado más arriba $R=2$ por eso hay dos $7$ más). Podemos obtener $R/2$ de la siguente manera: aumentando $N$ en uno, dividiéndolo entre dos y a eso restarle $7$, que es la cantidad de elementos distintos en un arreglo arcoíris.

La función de programación dinámica dependerá de dos parámetros, $r$ la cantidad de números repetidos que podemos poner todavía y $n$, el número que estamos poniendo en nuestro arreglo arcoíris.

Sabemos que $1≤n≤7$ , por tanto si en algún momento toma un valor distinto, hemos puesto $7$ números distintos en el arreglo, debemos entonces revisar $r$, si $r=0$ hemos usado todos nuestros números extras y por tanto esa es una manera de formar un arreglo, por el contrario si $r≠0$ llegamos a la mitad de nuestro arreglo sin usar todos nuestros números extras, por tanto esa no es una posibilidad.

Si se cumple que $1≤n≤7$ , de nuevo tenemos que fijarnos en el valor de $r$ para tomar la decisión de repetir un número o no.

$$ f(n,r) = \left\{ \begin{array}{ll} 1 & \mbox{if } n = 8\ and\ r = 0 \\ 0 & \mbox{if } n = 8\ and\ r \neq 0 \\ f(n,r-1) + f(n+1,r) & \mbox{if } r \neq 0 \\ f(n+1,r) & \mbox{if } r = 0 \\ \end{array} \right. $$

Por último, resta llamar a $f(1,R/2)$ para obtener el resultado. No olvides ir aplicando $%$ para mantener tu resultado en un rango correcto.

Pra obtener el preciado AC, tuve que implementar la DP de manera iterativa, ya que recursiva daba RTE

Código:

#include <cstdio>
#define L 7
#define MOD 1000000007
int N;
int m[10][1000010];
int dp(int i, int res){
    for(int ii = 8; ii >= 1; ii--) {
        for(int r = 0; r <= res; r++) {
            if(ii == 8) { 
                if(r) 
                    m[ii][r] = 0; 
                else 
                    m[ii][r] = 1; 
                continue; 
            }
            if(res){
                m[ii][r] = (m[ii][r-1] % MOD 
                                + m[ii+1][r] % MOD) % MOD;
            } else {
                m[ii][r] = m[ii+1][r] % MOD;
            }
        }
    }
    return m[i][res];
}
int main(){
    scanf("%d", &N);
    N++; N /= 2;
    if(N - L < 0) printf("0\n");
    else {
        printf("%d\n", dp(1,N - L) % MOD);
    }   
    return 0;
}

Sums in a Triangle I (COJ)

Link del problema: 1079 - Sums In a Triangle I

Dificultad: Fácil


Problema: Dado un número $N$ donde $1<=N<=100$ se tienen $N$ niveles en un triangulo, así el primer nivel tiene 1 número, el segundo nivel tiene 2 números, el tercero 3, etc. El programa debe encontrar la máxima suma desde el primer nivel hasta el último pudiendo tomar una ruta tal que solo se acceda al número de abajo o al de abajo y una posición a la derecha...

Ejemplo:
Con $N = 3 $
Tenemos el siguiente triángulo:
1
1 2
2 3 1

La respuesta es 6 ya que primero solo tomamos el primer elemento pues no hay más opción ya que es el único elemento en la fila en la siguiente podemos tomar el 1 o 2

1
1 2  //La mejor opción aquí es tomar el de abajo y a la derecha que es  el mayor
2 3 1

Hasta este punto llevamos la "mejor" ruta posible habiendo tomado los elementos de mayor prioridad entonces en la siguiente iteración solo podemos tomar dos elementos:
1
2  
2 3//La mejor opción aquí es tomar el de abajo que es  el mayor
Y así por fin llegar a nuestro cometido que era tomar la ruta tal que la suma sea la mayor...


Solución: (DP)
Este problema se resuelve con Programación Dinámica (DP) ya que tenemos que tomar una decisión en cada nivel (la mejor) y así ir tomar la mejor ruta que podamos, entonces como se mencionó en nuestra otra publicación de DP tenemos que establecer los ESTADOS de la DP y las DECISIONES para lo cual nuestros estados tendremos:
 int dp(int i,int j){
 }
Necesitaremos un apuntador a la columna donde estemos (i) y uno para la fila (j) para saber en que número vamos.
Lo siguiente es establecer donde se tendrá que detener la DP así que necesitamos un caso base para la recursión. Para lo cual:

if(i==n)
   return 0;
Vemos que si nuestro apuntador llega a la última columna tendremos que detener la DP.
Por último necesitamos nuestras decisiones, aquí solo basta con devolver el MÁXIMO entre tomar el de abajo dp(i+1,j) y tomar el de abajo y una posición a la derecha dp(i+1,j+1) y sumar en cada decisión el elemento que corresponde a esa posición de la siguiente manera:
return max(dp(i+1,j)+piramide[i+1][j],dp(i+1,j+1)+piramide[i+1][j+1]);
Y así se resolvería el problema, cabe decir que mis números los guarde en un arreglo llamado pirámide para así poder usarlos después y sumarlos. Ahora un punto IMPORTANTE al final a la respuesta que devuelva esta función tenemos que sumarle el primer elemento del triángulo ya que aquí NO estamos considerándolo.
res = dp(0,0);
printf("%d\n",res + piramide[0][0]);
Y bueno eso fue todo por esta ocasión, ya saben si gustan sugerir algún tema pueden hacerlo abiertamente y espero les haya gustado comenten si hay dudas y escriban.
Adrián Fernández @ferprogramming

DP (Programación Dinámica)


Tema:

La programación dinámica es un tema muy usual en las competencias de programación y a la vez una técnica que requiere de cierta práctica para empezar a dominarse. Bien empleada la programación dinámica puede ser muy útil para resolver problemas que requieran minimizar el tiempo de ejecución de su algoritmo y a la vez requieran checar muchas soluciones para encontrar la óptima.

Las "DP's" dividen un problema en sub-problemas es decir resuelven un "gran" problema partiendolo en problemas pequeños para así llegar a la solución. Suele relacionarse con la definición de recursividad pero no debe confundirse ya que a veces la programación dinámica requiere de la misma recursividad para emplearse.
Hay dos estilos (tipos tal vez) de programación dinámica la Top-Bottom y la Bottom-Top que son dos diferentes enfoques de ver las soluciones con DP's y la forma en como las programamos también. Para este problema usaremos Top-Bottom (Recursiva).
Lo primero que hay que hacer para resolver un problema bajo DP es ver y analizar que posibles "estados" que tendrá nuestra función dp();
Para ello usaré como ejemplo un problema de COJ llamado 

Link del problema: Coin Change
Dificultad: Fácil

Problema:

Este problema consiste a grandes rasgos en decir de cuantas maneras podemos representar un numero $N$ con $0<= N <= 10,000$, con 5 tipos de monedas de cambio (50,25,10,5 y 1) por ejemplo el caso de prueba que viene en la página
Con dinero = 11 ¿De cuántas formas se puede expresar?


Solución:
1° 1+1+1+1+1+1+1+1+1+1+1 = 11 
2° 5+5+1 = 11 
3° 1+1+1+1+1+1 + 5 = 11
4° 10 + 1 = 11
Para este caso es evidente que la solución es 4 porque solo existen 4 maneras diferentes de representar nuestro número N.

Paso 1 (Identificar los estados de nuestra dp() ): 

-¿De que estados o variables depende?
Para este problema necesitamos tanto la cantidad de 'dinero' que llevamos hasta el momento y además que 'monedas' utilizamos.

int dp(int dinero,int i);

además de devolver el valor que procese nuestra dp() como int

Paso 2 (Decisiones: "Tomar o no tomarlo")

Esta parte de la dp() es muy importante ya que de aquí deriva el buen funcionamiento de la misma

-¿Qué decisiones podemos establecerle a este problema?

Supongamos que tenemos un arreglo cambios de la siguiente manera:
int cambios[] = {50,25,10,5,1};
Además supongamos que tenemos nuestro total de dinero y cada que tomemos o no un cambio[] se lo restaremos al total de dinero entonces sabremos cuando frenar la dp(); y saber que ya hemos procesado todo así:

if(dinero==0)
   return 1;

Como caso base de la DP quiere decir que ya le restamos todo a nuestro dinero entonces para ese entonces ya abremos tomado todas las decisiones 
if(dinero!=0 && i==5)
   return 0;
Es otra decisión pues quiere decir que terminamos de recorrer nuestro arreglo de monedas pero que no llegamos a nuestro dinero==0 o sea que no quedó justa la resta de los dineros.

Ahora las decisiones tienen que ver con que el número del arreglo no sea mayor a nuestro dinero, SI ES MAYOR NO LO TOMAMOS y avanzamos al siguiente elemento SI ES MENOR O IGUAL lo tomamos y se lo restamos a nuestro dinero OJO (mandamos ambas probabilidades tomarlo y no tomarlo) para que se realice el árbol y tome todas las posibilidades...
if(cambios[i]>dinero)
     return dp(dinero,i+1);

  if (cambios[i] <= dinero) 
     return dp(dinero - cambios[i],i) + dp(dinero,i+1);

Esto tiene una complejidad O($2^N$) así que haría falta memorizarlo para hacerlo pueden checar el siguiente enlace donde explico en mi blog como memorizar (memoizar) una función recursiva y eso nos reducirá la complejidad de la siguiente DP a O($n^2$) y aquí ya la función dp() memorizada:

 int dp(int dinero,int i){

   if(memo[dinero][i]!=-1)
      return memo[dinero][i];
   if(dinero==0)
      return 1;
   if(dinero==0 && i==5)
      return 1;

   if(i==5)
      return 0;

   if(cambios[i]>dinero)
      return memo[dinero][i] = dp(dinero,i+1);

   if (cambios[i] <= dinero) 
      return memo[dinero][i] = dp(dinero - cambios[i],i) + dp(dinero,i+1);
}
Esta es una DP sencilla pero en sí mejorar y perfeccionar esta técnica requiere mucha práctica aunque seguro hay un AC con este código, así que eso es todo por esta sesión. 

Por: Adrián Fernández @ferprogramming