LOS 10 MEJORES ALGORITMOS DE PROGRAMACIÓN DINÁMICA

 

 

1.- Más larga sub-secuencia común

2.- La sub-secuencia creciente más larga

3.- Distancia

4.- Partición mínima

5.- Modos de cubrir una distancia

6.- El camino más largo de la matriz

7.- Problema de la suma de subconjuntos

8.- Estrategia óptima para un juego

9.- Problema de la mochila

10.- Problema de parentesco booleano

 

&&&&&&&&&&

&&&&&&&

&&&

&

 

 

1.- Más larga subsecuencia común

 

Hemos discutido los sub-problemas superpuestos y las propiedades de la subestructura óptima en el Set 1 y el Set 2 respectivamente. También hemos discutido un problema de ejemplo en el Conjunto 3. Discutamos el problema de la Más Larga Subsecuencia Común (LCS) como un problema de ejemplo más que puede ser resuelto usando la Programación Dinámica.

 

Declaración del problema LCS: Dadas dos secuencias, encontrar la duración de la más larga subsecuencia presente en ambas. Una subsecuencia es una secuencia que aparece en el mismo orden relativo, pero no necesariamente contigua. Por ejemplo, "abc", "abg", "bdf", "aeg", '"acefg", .. etc. son subsecuencias de "abcdefg".

 

Para averiguar la complejidad del enfoque de la fuerza bruta, es necesario conocer primero el número de posibles subsiguientes diferentes de una cuerda con longitud n, es decir, encontrar el número de subsiguientes con longitudes que oscilan entre 1,2,..n-1. Recordemos de la teoría de la permutación y combinación que el número de combinaciones con 1 elemento son nC1. El número de combinaciones con 2 elementos son nC2 y así sucesivamente. Sabemos que nC0 + nC1 + nC2 + ... nCn = 2n. Por lo tanto, una cadena de longitud n tiene 2n-1 posibles subsiguientes diferentes ya que no consideramos la subsiguiente con longitud 0. Esto implica que la complejidad temporal del enfoque de la fuerza bruta será O(n * 2n). Obsérvese que lleva O(n) tiempo comprobar si una subsecuencia es común a ambas cadenas. Esta complejidad temporal puede mejorarse utilizando una programación dinámica.

 

Es un problema clásico de la informática, la base de diff (un programa de comparación de archivos que produce las diferencias entre dos archivos), y tiene aplicaciones en la bioinformática.

 

Ejemplos:

Para las secuencias de entrada "ABCDGH" y "AEDFHR" es "ADH" de longitud 3.

Para las secuencias de entrada "AGGTAB" y "GXTXAYB" es "GTAB" de longitud 4.

 

La solución sencilla para este problema es generar todas las subsecuencias de ambas secuencias dadas y encontrar la subsecuencia coincidente más larga. Esta solución es exponencial en términos de complejidad temporal. Veamos cómo este problema posee las dos importantes propiedades de un problema de programación dinámica (DP).

 

1) Subestructura óptima:

Que las secuencias de entrada sean X[0..m-1] e Y[0..n-1] de longitudes m y n respectivamente. Y que L(X[0..m-1], Y[0..n-1]) sea la longitud de LCS de las dos secuencias X e Y. A continuación la definición recursiva de L(X[0..m-1], Y[0..n-1]).

 

Si los últimos caracteres de ambas secuencias coinciden (o X[m-1] == Y[n-1]) entonces

L(X[0..m-1], Y[0..n-1]) = 1 + L(X[0..m-2], Y[0..n-2])

 

Si los últimos caracteres de ambas secuencias no coinciden (o X[m-1] != Y[n-1]) entonces

L(X[0..m-1], Y[0..n-1]) = MAX ( L(X[0..m-2], Y[0..n-1]), L(X[0..m-1], Y[0..n-2])

 

Ejemplos:

1) Considere las cadenas de entrada "AGGTAB" y "GXTXAYB". Los últimos caracteres coinciden con las cadenas. Así que la longitud de LCS puede ser escrita como:

L("AGGTAB", "GXTXAYB") = 1 + L("AGGTA", "GXTXAY")

 

 

2) Considerar las cadenas de entrada "ABCDGH" y "AEDFHR". Los últimos caracteres no coinciden con las cadenas. Así que la longitud de LCS puede ser escrita como:

L("ABCDGH", "AEDFHR") = MAX ( L("ABCDG", "AEDFHR"), L("ABCDGH", "AEDFH") )

 

Así que el problema del LCS tiene una propiedad de subestructura óptima ya que el problema principal puede ser resuelto usando soluciones a los subproblemas.

 

2) Subproblemas superpuestos:

Lo siguiente es una simple implementación recursiva del problema LCS. La implementación simplemente sigue la estructura recursiva mencionada anteriormente.

 

En lenguaje C++:

 

#include <bits/stdc++.h>

using namespace std;

 int max(int a, int b); 

  

int lcs( char *X, char *Y, int m, int n ) 

{   if (m == 0 || n == 0) 

        return 0; 

    if (X[m-1] == Y[n-1]) 

        return 1 + lcs(X, Y, m-1, n-1); 

    else

        return max(lcs(X, Y, m, n-1), lcs(X, Y, m-1, n)); 

  

int max(int a, int b)  {   return (a > b)? a : b;  } 

  

int main() 

    char X[] = "AGGTAB"; 

    char Y[] = "GXTXAYB"; 

    int m = strlen(X); 

    int n = strlen(Y); 

    cout<<"Length of LCS is "<< lcs( X, Y, m, n ) ; 

    return 0; 

  

Salida: 4

 

La complejidad temporal del enfoque recursivo sencillo anterior es O(2^n) en el peor caso y el peor caso ocurre cuando todos los caracteres de X e Y no coinciden, es decir, la longitud del LCS es 0.

Considerando la implementación anterior, a continuación se muestra un árbol de recursividad parcial para las cadenas de entrada "AXYT" y "AYZX".

 

En el árbol de recursividad parcial anterior, lcs ("AXY", "AYZ") se está resolviendo dos veces. Si dibujamos el árbol de recursividad completa, entonces podemos ver que hay muchos subproblemas que se resuelven una y otra vez. Así que este problema tiene la propiedad Subestructura Superpuesta y la recomposición de los mismos subproblemas puede evitarse usando la Memotización o la Tabulación. A continuación se muestra una implementación tabulada para el problema LCS.

 

#include<bits/stdc++.h> 

usando namespace std;

 

int max(int a, int b); 

 

/* Devuelve la longitud del LCS para X[0..m-1], Y[0..n-1] */

int lcs( char *X, char *Y, int m, int n ) 

{ 

    int L[m + 1][n + 1]; 

    int i, j; 

     

    /* Los siguientes pasos construyen L[m+1][n+1] en 

       la moda de abajo hacia arriba. Tengan en cuenta que L[i][j] 

       contiene la longitud de LCS de X[0..i-1]

       y Y[0..j-1] */

    para (i = 0; i <= m; i++) 

    { 

        para (j = 0; j <= n; j++) 

        { 

        si (i == 0 || j == 0) 

            L[i][j] = 0; 

     

        si no, si (X[i - 1] == Y[j - 1]) 

            L[i][j] = L[i - 1][j - 1] + 1; 

     

        más

            L[i][j] = max(L[i - 1][j], L[i][j - 1]); 

        } 

    } 

         

    /* L[m][n] contiene la longitud de LCS 

    para X[0..n-1] y Y[0..m-1] */

    Devuelve L[m][n]; 

} 

 

/* Función de utilidad para obtener un máximo de 2 números enteros */

int max(int a, int b) 

{ 

    ¿Devolver (a > b)? a : b; 

} 

 

// Programa principal

int main() 

{ 

    char X[] = "AGGTAB"; 

    char Y[] = "GXTXAYB"; 

     

    int m = strlen(X); 

    int n = strlen(Y); 

     

    cout << "La longitud del LCS es "

         << lcs( X, Y, m, n ); 

     

    return 0; 

}  

 

Salida:  La longitud del LCS es de 4

 

La complejidad temporal de la aplicación anterior es O(mn), que es mucho mejor que la complejidad temporal del peor de los casos de la aplicación Recursiva sencilla.

El algoritmo/código anterior devuelve sólo la longitud del LCS.

 

 

 

2.- La subsecuencia creciente más larga

 

Ya hemos discutido los sub-problemas superpuestos y las propiedades de la subestructura óptima.

 

Ahora, vamos a discutir el problema de la Subsecuencia Creciente Más Larga (LIS) como un problema de ejemplo que puede ser resuelto usando la Programación Dinámica.

El problema de la Mayor Subsecuencia Creciente (LIS) es encontrar la longitud de la mayor subsecuencia de una secuencia dada, de tal manera que todos los elementos de la subsecuencia se ordenen en orden creciente. Por ejemplo, la longitud de LIS para {10, 22, 9, 33, 21, 50, 41, 60, 80} es 6 y LIS es {10, 22, 33, 50, 60, 80}.

 

Ejemplo:

Entrada: arr[] = {3, 10, 2, 1, 20}

Salida: Longitud = 3

La subsecuencia creciente más larga es 3, 10, 20

 

Método Recursivo:

 

Subestructura óptima: Dejemos que arr[0..n-1] sea la matriz de entrada y L(i) sea la longitud del LIS que termina en el índice i, de tal manera que arr[i] sea el último elemento del LIS.

 

Entonces, L(i) puede ser escrita recursivamente como:

 

L(i) = 1 + max( L(j) ) donde 0 < j < i y arr[j] < arr[i]; o

L(i) = 1, si no existe tal j.

Para encontrar el LIS de un conjunto dado, necesitamos devolver max(L(i)) donde 0 < i < n.

Formalmente, la longitud de la subsecuente más larga que termina en el índice i, será 1 mayor que el máximo de longitudes de todas las subsecuentes más largas que terminan en los índices antes de i, donde arr[j] < arr[i] (j < i). Así pues, vemos que el problema del SIL satisface la propiedad de subestructura óptima, ya que el problema principal puede resolverse mediante soluciones a los subproblemas. El árbol recursivo que se muestra a continuación hará que el enfoque sea más claro:

 

Entrada : arr[] = {3, 10, 2, 11}

f(i): Denota el LIS del subconjunto que termina en el índice 'i'.

 

(LIS(1)=1)

 

      f(4) {f(4) = 1 + max(f(1), f(2), f(3))}

  / | \

f(1) f(2) f(3) {f(3) = 1, f(2) y f(1) son > f(3)}

       | | \

      f(1) f(2) f(1) {f(2) = 1 + max(f(1)}

              |

            f(1) {f(1) = 1}

A continuación se presenta la aplicación del enfoque recursivo:

 

#include<stdio.h>

#include<stdlib.h>

  

int _lis( int arr[], int n, int *max_ref)

{

    if (n == 1)    return 1;

  

    int res, max_ending_here = 1; 

  

    for (int i = 1; i < n; i++)

    {

        res = _lis(arr, i, max_ref);

        if (arr[i-1] < arr[n-1] && res + 1 > max_ending_here)

            max_ending_here = res + 1;

    }

  

    if (*max_ref < max_ending_here)

       *max_ref = max_ending_here;

  

    return max_ending_here;

}

  

int lis(int arr[], int n)

{

    int max = 1;

    _lis( arr, n, &max );

      return max;

}

  

//Principal

int main()

{

    int arr[] = { 10, 22, 9, 33, 21, 50, 41, 60 };

    int n = sizeof(arr)/sizeof(arr[0]);

    printf("La longitud es ",

           lis( arr, n ));

    return 0;

}

Salida: La longitud es 5

Complejidad temporal: La complejidad temporal de este enfoque recursivo es exponencial, ya que existe un caso de subproblemas superpuestos como se explica en el diagrama de árbol recursivo anterior.

Espacio Auxiliar: O(1). No se utiliza ningún espacio externo para almacenar valores aparte del espacio interno de la pila.

 

 

3.- Distancia

 

Dadas dos cadenas str1 y str2 y por debajo de las operaciones que se pueden realizar en str1. Encuentra el mínimo número de ediciones (operaciones) necesarias para convertir 'str1' en 'str2'.

 

Insertar

Retire

Reemplazar

Todas las operaciones anteriores tienen el mismo costo.

 

Ejemplos:

Entrada: str1 = "geek", str2 = "gesek"

Salida:  1

Podemos convertir str1 en str2 insertando una 's'.

 

¿Cuáles son los subproblemas en este caso?

La idea es procesar todos los personajes uno por uno mirando desde el lado izquierdo o derecho de ambas cuerdas.

Vamos a atravesar desde la esquina derecha, hay dos posibilidades para cada par de personajes que se atraviesan.

 

m: Longitud de la cadena 1 (primera cadena)

n: Longitud de la cadena 2 (segunda cadena)

Si los últimos caracteres de dos cuerdas son iguales, no hay mucho que hacer. Ignorar los últimos caracteres y obtener la cuenta de las cuerdas restantes. Así que recurrimos a las longitudes m-1 y n-1.

Si los últimos caracteres no son iguales, consideramos todas las operaciones en 'str1', consideramos las tres operaciones en el último carácter de la primera cadena, calculamos recursivamente el coste mínimo de las tres operaciones y tomamos un mínimo de tres valores.

Insertar: Recurrir para m y n-1

Quita: Recurrir para m-1 y n

Reemplazar: Recurrir para m-1 y n-1

A continuación se muestra la implementación en C++ de la solución recursiva ingenua anterior.

 

#include <bits/stdc++.h>

using namespace std;

  

int min(int x, int y, int z)

{

    return min(min(x, y), z);

}

  

int editDist(string str1, string str2, int m, int n)

{

    if (m == 0)    return n;

      if (n == 0)    return m;

  

   if (str1[m - 1] == str2[n - 1])    return editDist(str1, str2, m - 1, n - 1);

 

  return 1 + min(editDist(str1, str2, m, n - 1), // Insert

                   editDist(str1, str2, m - 1, n), // Remove

                   editDist(str1, str2, m - 1, n - 1) // Replace

                   );

}

  

// Principal

int main()

{

     string str1 = "sunday";

    string str2 = "saturday";

  

    cout << editDist(str1, str2, str1.length(), str2.length());

  

    return 0;

}

Salida: 3

La complejidad temporal de la solución anterior es exponencial. En el peor de los casos, podemos terminar haciendo operaciones de O(3m). El peor caso ocurre cuando ninguno de los caracteres de dos cadenas coinciden. A continuación se muestra un diagrama de llamada recursivo para el peor caso.

 

Podemos ver que muchos subproblemas se resuelven, una y otra vez, por ejemplo, eD(2, 2) se llama tres veces. Dado que los mismos suproblemas se llaman de nuevo, este problema tiene la propiedad Subproblemas Superpuestos. Por lo tanto, el problema Edit Distance tiene ambas propiedades (ver esto y esto) de un problema de programación dinámica. Al igual que otros problemas típicos de Programación dinámica (DP), se pueden evitar los recálculos de los mismos subproblemas construyendo una matriz temporal que almacene los resultados de los subproblemas.

 

 

4.- Partición mínima

 

Dado un conjunto de números enteros, la tarea consiste en dividirlo en dos conjuntos S1 y S2 de manera que la diferencia absoluta entre sus sumas sea mínima.

Si hay un conjunto S con n elementos, entonces si suponemos que el Subconjunto1 tiene m elementos, el Subconjunto2 debe tener n-m elementos y el valor de abs(suma(Subconjunto1) - suma(Subconjunto2)) debe ser mínimo.

 

Ejemplo:

Entrada: arr[] = {1, 6, 11, 5}

Salida: 1

Explicación:

Subconjunto1 = {1, 5, 6}, suma del subconjunto1 = 12

Subconjunto2 = {11}, suma del subconjunto2 = 11       

 

Solución recursiva

El enfoque recursivo consiste en generar todas las sumas posibles a partir de todos los valores de la matriz y comprobar qué solución es la más óptima.

Para generar las sumas, o bien incluimos el ítem i en el conjunto 1 o no incluimos, es decir, incluimos en el conjunto 2.  

 

#include <bits/stdc++.h>

using namespace std;

  

int findMinRec(int arr[], int i, int sumCalculated, int sumTotal)

{

    if (i==0)    return abs((sumTotal-sumCalculated) - sumCalculated);

  

   return min(findMinRec(arr, i-1, sumCalculated+arr[i-1], sumTotal),

               findMinRec(arr, i-1, sumCalculated, sumTotal));

}

  

int findMin(int arr[], int n)

{

    int sumTotal = 0;

    for (int i=0; i<n; i++)

        sumTotal += arr[i];

  

    return findMinRec(arr, n, 0, sumTotal);

}

  

//Principal

int main()

{

    int arr[] = {3, 1, 4, 2, 2, 1};

    int n = sizeof(arr)/sizeof(arr[0]);

    cout << " La diferencia mínima entre dos conjuntos es  "

         << findMin(arr, n);

    return 0;

}

Salida: La diferencia mínima entre dos conjuntos es   1

Todas las sumas pueden ser generadas por

(1) incluyendo ese elemento en el conjunto 1.

(2) sin incluir ese elemento en el conjunto 1.

Así que las posibles combinaciones son :- 

arr[0] (1 o 2) -> 2 valores

arr[1] (1 o 2) -> 2 valores

arr[n] (2 o 2) -> 2 valores

Así que la complejidad del tiempo será 2*2*..... *2 (Por n veces),

que es O(2^n).

 

 

5.- Modos de cubrir una distancia

 

Dada una distancia, cuente el número total de formas de cubrir la distancia con 1, 2 y 3 pasos.

Ejemplos:

 

Entrada: n = 3

Salida: 4

Explicación:

A continuación están las cuatro formas

 1 paso + 1 paso + 1 paso

 1 paso + 2 pasos

 2 pasos + 1 paso

 3 pasos

 

Entrada: n = 4

Salida: 7

Explicación:

A continuación están las cuatro formas

 1 paso + 1 paso + 1 paso + 1 paso

 1 paso + 2 pasos + 1 paso

 2 pasos + 1 paso + 1 paso

 1 paso + 1 paso + 2 pasos

 2 pasos + 2 pasos

 3 pasos + 1 paso

 1 paso + 3 pasos

 

Solución recursiva

 

Hay n escaleras, y una persona puede dar el siguiente paso, saltar una posición o saltar dos posiciones. Así que hay n posiciones. La idea es pararse en la posición i, la persona puede moverse por la posición i+1, i+2, i+3. Así que se puede formar una función recursiva donde en el índice actual i la función se llama recursivamente para las posiciones i+1, i+2 y i+3.

Hay otra forma de formar la función recursiva. Para llegar a la posición i, una persona tiene que saltar desde la posición i-1, i-2 o i-3 donde i es la posición inicial.

Algoritmo:

Crear una función recursiva (count(int n)) que toma un solo parámetro.

Comprueba los casos base. Si el valor de n es menor que 0 entonces devuelve 0, y si el valor de n es igual a cero entonces devuelve 1 ya que es la posición inicial.

Llama a la función de forma recursiva con los valores n-1, n-2 y n-3 y suma los valores que se devuelven, es decir, suma = cuenta(n-1) + cuenta(n-2) + cuenta(n-3).

Devuelve el valor de la suma.

Implementación:

 

#include<iostream>

using namespace std;

  

int printCountRec(int dist)

{

    if (dist<0)      return 0;

    if (dist==0)  return 1;

  

    return printCountRec(dist-1) +

           printCountRec(dist-2) +

           printCountRec(dist-3);

}

  

// Principal

int main()

{

    int dist = 4;

    cout << printCountRec(dist);

    return 0;

}


Salida: 7

 

Complejidad temporal: O(3n).

La complejidad temporal de la solución anterior es exponencial, un límite superior cercano es O(3n). De cada estado 3, se llama una función recursiva. Así que el límite superior para n estados es O(3n).

Complejidad espacial: O(1).

No se requiere espacio adicional.

 

 

6.- El camino más largo de la matriz

 

Dada una matriz de n*n donde todos los números son distintos, encuentre el camino de máxima longitud (empezando por cualquier célula) de tal manera que todas las células a lo largo del camino estén en orden creciente con una diferencia de 1.

 

Podemos movernos en 4 direcciones desde una célula dada (i, j), es decir, podemos movernos a (i+1, j) o (i, j+1) o (i-1, j) o (i, j-1) con la condición de que las células adyacentes tengan una diferencia de 1.

 

Ejemplo:

Entrada: mat[][] = {{1, 2, 9}

                   {5, 3, 8}

                   {4, 6, 7}}

Salida: 4

El camino más largo es el 6-7-8-9.

 

La idea es sencilla; calculamos el camino más largo empezando por cada célula. Una vez que hayamos calculado el más largo para todas las células, regresamos el máximo de todos los caminos más largos. Una observación importante en este enfoque es que muchos subproblemas se superponen. Por lo tanto, este problema puede ser resuelto de manera óptima usando la Programación Dinámica.

 

A continuación se muestra la implementación basada en la Programación Dinámica que utiliza una tabla de búsqueda dp[][] para comprobar si un problema ya está resuelto o no.

 

#include <bits/stdc++.h>

#define n 3

using namespace std;

  

int findLongestFromACell(int i, int j, int mat[n][n], int dp[n][n])

{

    if (i < 0 || i >= n || j < 0 || j >= n)     return 0;

    if (dp[i][j] != -1)     return dp[i][j];

    int x = INT_MIN, y = INT_MIN, z = INT_MIN, w = INT_MIN;

  

    if (j < n - 1 && ((mat[i][j] + 1) == mat[i][j + 1]))

        x = 1 + findLongestFromACell(i, j + 1, mat, dp);

  

    if (j > 0 && (mat[i][j] + 1 == mat[i][j - 1]))

        y = 1 + findLongestFromACell(i, j - 1, mat, dp);

  

    if (i > 0 && (mat[i][j] + 1 == mat[i - 1][j]))

        z = 1 + findLongestFromACell(i - 1, j, mat, dp);

  

    if (i < n - 1 && (mat[i][j] + 1 == mat[i + 1][j]))

        w = 1 + findLongestFromACell(i + 1, j, mat, dp);

  

    return dp[i][j] = max(x, max(y, max(z, max(w, 1))));

}

  

int finLongestOverAll(int mat[n][n])

{

    int result = 1; // Initialize result

    int dp[n][n];

    memset(dp, -1, sizeof dp);

  

    for (int i = 0; i < n; i++) {

        for (int j = 0; j < n; j++) {

            if (dp[i][j] == -1)

                findLongestFromACell(i, j, mat, dp);

             result = max(result, dp[i][j]);

        }

    }

  return result;

}

  

// Principal

int main()

{

    int mat[n][n] = { { 1, 2, 9 },

                      { 5, 3, 8 },

                      { 4, 6, 7 } };

    cout << " La longitud del camino más largo es de "

         << finLongestOverAll(mat);

    return 0;

}

Salida: La longitud del camino más largo es de 4

La complejidad temporal de la solución anterior es O(n2). Puede parecer más a primera vista. Si miramos más de cerca, podemos notar que todos los valores de dp[i][j] se calculan sólo una vez.

 

 

7.- Problema de la suma de subconjuntos

 

Dado un conjunto de números enteros no negativos, y una suma de valores, determinar si hay un subconjunto del conjunto dado con una suma igual a la suma dada.

Ejemplo:

Entrada: conjunto[] = {3, 34, 4, 12, 5, 2}, suma = 9

Salida: Verdadero 

Hay un subconjunto (4, 5) con la suma 9.

 

Método Recursivo:

 

Para el enfoque recursivo consideraremos dos casos.

 

Consideremos el último elemento y ahora la suma requerida = suma objetivo - valor del "último" elemento y número de elementos = total de elementos - 1

Deje el "último" elemento y ahora la suma requerida = suma objetivo y número de elementos = total de elementos - 1

A continuación la fórmula recursiva para el problema de isSubsetSum().

 

isSubsetSum(set, n, sum)

= isSubsetSum(set, n-1, sum) ||

  isSubsetSum(set, n-1, sum-set[n-1])

Casos base:

isSubsetSum(set, n, sum) = falso, si la suma > 0 y n == 0

isSubsetSum(set, n, sum) = true, if sum == 0

Echemos un vistazo a la simulación de la aproximación anterior..:

 

set[]={3, 4, 5, 2}

suma=9

(x, y)= 'x' es el número de elementos de la izquierda,

"y" es la suma requerida

 

              (4, 9)

             {Verdad}

           / \ 

        (3, 6) (3, 9)

              

        / \ / \

     (2, 2) (2, 6) (2, 5) (2, 9)

     {Verdad} 

     / \

  (1, -3) (1, 2) 

{Falso} {Verdadero}

         / \

       (0, 0) (0, 2)

       {Verdadero} {Falso}   

 

#include <stdio.h>

  

bool isSubsetSum(int set[], int n, int sum)

{

    // Base Cases

    if (sum == 0)

        return true;

    if (n == 0)

        return false;

  

    if (set[n - 1] > sum)    return isSubsetSum(set, n - 1, sum);

  

    return isSubsetSum(set, n - 1, sum)

           || isSubsetSum(set, n - 1, sum - set[n - 1]);

}

  

// Principal

int main()

{

    int set[] = { 3, 34, 4, 12, 5, 2 };

    int sum = 9;

    int n = sizeof(set) / sizeof(set[0]);

    if (isSubsetSum(set, n, sum) == true)

        printf("Encontré un subconjunto con una suma dada ");

    else

        printf("No encontré un subconjunto con una suma dada ");

    return 0;

}

Salida: Encontré un subconjunto con una suma dada

Complejidad: La solución anterior puede probar todos los subconjuntos de un determinado conjunto en el peor de los casos. Por lo tanto, la complejidad temporal de la solución anterior es exponencial. El problema es de hecho NP-Completo

 

 

8.- Estrategia óptima para un juego

 

Considere una fila de n monedas de valores v1 . . . vn, donde n es par. Jugamos una partida contra un oponente alternando turnos. En cada turno, un jugador selecciona la primera o la última moneda de la fila, la retira de la fila permanentemente y recibe el valor de la moneda. Determina la máxima cantidad de dinero posible que podemos ganar definitivamente si nos movemos primero.

 

Nota: El oponente es tan inteligente como el usuario.

 

Entendamos el problema con algunos ejemplos:

 

5, 3, 7, 10 : El usuario recoge el valor máximo como 15(10 + 5)

8, 15, 3, 7 : El usuario recoge el valor máximo como 22(7 + 15)

¿Elegir lo mejor en cada movimiento da una solución óptima? No.

En el segundo ejemplo, así es como se puede terminar el juego:

 

.......El usuario elige 8.

.......El usuario elige 15.

.......El usuario elige 7.

.......El opositor elige 3.

El valor total recogido por el usuario es 15(8 + 7)

.......El usuario elige 7.

.......El usuario elige 8.

.......El usuario elige 15.

.......El opositor elige 3.

El valor total recogido por el usuario es 22(7 + 15)

Así que si el usuario sigue el segundo estado de juego, el valor máximo puede ser recogido aunque el primer movimiento no sea el mejor.

 

Como ambos jugadores son igual de fuertes, ambos intentarán reducir la posibilidad de ganarse el uno al otro. Ahora veamos cómo el oponente puede lograr esto.

 

Hay dos opciones:

 

El usuario elige la moneda "i" con el valor "Vi": El oponente elige la moneda "i+1" o la moneda "j". El oponente tiene la intención de elegir la moneda que deja al usuario con un valor mínimo.

Es decir, el usuario puede recoger el valor Vi + min(F(i+2, j), F(i+1, j-1) ).

 

 

El usuario elige la moneda 'jth' con valor 'Vj': El oponente elige la moneda 'ith' o la 'j-1'. El oponente tiene la intención de elegir la moneda que deja al usuario con un valor mínimo, es decir, el usuario puede recoger el valor Vj + min(F(i+1, j-1), F(i, j-2) ).

 

 

A continuación se presenta la solución recursiva que se basa en las dos opciones anteriores. Tomamos un máximo de dos opciones.

 

F(i, j) representa el valor máximo que el usuario

puede recoger desde la moneda I'th hasta la J'th.

 

F(i, j) = Max(Vi + min(F(i+2, j), F(i+1, j-1) ),

              Vj + min(F(i+1, j-1), F(i, j-2) ))

Como usuario quiere maximizar el número de monedas.

 

Casos de base

    F(i, j) = Vi Si j == i

    F(i, j) = max(Vi, Vj) Si j == i + 1

 

#include <bits/stdc++.h>

using namespace std;

  

int optimalStrategyOfGame(

    int* arr, int n)

{

    int table[n][n];

  

    for (int gap = 0; gap < n; ++gap) {

        for (int i = 0, j = gap; j < n; ++i, ++j) {

            int x = ((i + 2) <= j)

                        ? table[i + 2][j]

                        : 0;

            int y = ((i + 1) <= (j - 1))

                        ? table[i + 1][j - 1]

                        : 0;

            int z = (i <= (j - 2))

                        ? table[i][j - 2]

                        : 0;

  

            table[i][j] = max(

                arr[i] + min(x, y),

                arr[j] + min(y, z));

        }

    }

  

    return table[0][n - 1];

}

  

// Principal

int main()

{

    int arr1[] = { 8, 15, 3, 7 };

    int n = sizeof(arr1) / sizeof(arr1[0]);

    printf("%d\n",

           optimalStrategyOfGame(arr1, n));

  

    int arr2[] = { 2, 2, 2, 2 };

    n = sizeof(arr2) / sizeof(arr2[0]);

    printf("%d\n",

           optimalStrategyOfGame(arr2, n));

  

    int arr3[] = { 20, 30, 2, 2, 2, 10 };

    n = sizeof(arr3) / sizeof(arr3[0]);

    printf("%d\n",

           optimalStrategyOfGame(arr3, n));

  

    return 0;

}

Salida: 22     4      42

Complejidad: O(n2).

El uso de un bucle anidado para el bucle lleva la complejidad del tiempo a n2.

Espacio auxiliar: O(n2).

Como una tabla 2-D se utiliza para almacenar estados.

 

 

9.- Problema de la mochila

 

Dado el peso y el valor de n artículos, colóquelos en una mochila de capacidad W para obtener el máximo valor total en la mochila. En otras palabras, dados dos conjuntos de números enteros val[0..n-1] y wt[0..n-1] que representan valores y pesos asociados a n artículos respectivamente. También dado un entero W que representa la capacidad de la mochila, averigua el máximo valor del subconjunto de val[] de tal manera que la suma de los pesos de este subconjunto sea menor o igual que W. No puedes romper un artículo, ni escoger el artículo completo ni no escogerlo (propiedad 0-1).

 

Problema de la mochila:

 

Método Recursivo:

 

Una solución simple es considerar todos los subconjuntos de artículos y calcular el peso y el valor total de todos los subconjuntos. Considere los únicos subconjuntos cuyo peso total es menor que W. De todos esos subconjuntos, elija el subconjunto de valor máximo.

 

Subestructura óptima: Para considerar todos los subconjuntos de artículos, puede haber dos casos para cada artículo.

 

Caso 1: El artículo está incluido en el subconjunto óptimo.

Caso 2: El artículo no está incluido en el conjunto óptimo.

Por lo tanto, el valor máximo que se puede obtener de "n" artículos es el máximo de los dos valores siguientes.

 

Valor máximo obtenido por n-1 artículos y peso W (excluyendo n-ésimo artículo).

Valor del n-ésimo artículo más el valor máximo obtenido por n-1 artículos y W menos el peso del n-ésimo artículo (incluido el n-ésimo artículo).

Si el peso del artículo "n-ésimo" es mayor que el de "W", entonces el artículo n-ésimo no puede incluirse y el caso 1 es la única posibilidad.

 

A continuación se presenta la aplicación del enfoque anterior:

 

#include <bits/stdc++.h>

using namespace std;

  

int max(int a, int b) { return (a > b) ? a : b; }

 

int knapSack(int W, int wt[], int val[], int n)

{

    if (n == 0 || W == 0)     return 0;

  

    if (wt[n] > W)   return knapSack(W, wt, val, n - 1);

    else

        return max(

            val[n] + knapSack(W - wt[n], 

                                    wt, val, n - 1),

            knapSack(W, wt, val, n - 1));

}

  

// Principal

int main()

{

    int val[] = { 60, 100, 120 };

    int wt[] = { 10, 20, 30 };

    int W = 50;

    int n = sizeof(val) / sizeof(val[0]);

    cout << knapSack(W, wt, val, n);

    return 0;

}

Salida: 220

 

Cabe señalar que la función anterior calcula los mismos subproblemas una y otra vez. Véase el siguiente árbol de recursividad, K(1, 1) se evalúa dos veces. La complejidad temporal de esta solución recursiva ingenua es exponencial (2^n).

 

En el siguiente árbol de recursión, K() se refiere a

a KnapSack. Los dos parámetros indicados en el

El siguiente árbol de recursividad son la n y la W.

El árbol de recursividad es para las siguientes entradas de muestra.

wt[] = {1, 1, 1}, W = 2, val[] = {10, 20, 30}

                       K(n, W)

                       K(3, 2) 

                   / \

                 / \              

            K(2, 2) K(2, 1)

          / \ / \

        / \ / \

       K(1, 2) K(1, 1) K(1, 1) K(1, 0)

       / \ / \ / \

     / \ / \ / \

K(0, 2) K(0, 1) K(0, 1) K(0, 0) K(0, 1) K(0, 0)

Árbol de recursividad para la capacidad de la mochila 2

y 3 artículos de 1 unidad de peso.

 

Complejidad: O(2^n).

Como hay subproblemas redundantes.

Espacio auxiliar :O(1).

Como no se ha utilizado ninguna estructura de datos extra para almacenar valores.

 

 

 

10.- Problema de parentesco booleano

 

Dada una expresión booleana con los siguientes símbolos.

 

Símbolos

    "T"... cierto...

    F' ---> falso

Y los siguientes operadores llenos entre los símbolos

 

Operadores

    & ---> booleano Y

    ||> booleano O

    ^ ---> booleano XOR

Cuente el número de formas en que podemos poner entre paréntesis la expresión para que el valor de la expresión se evalúe como verdadero.

 

Dejemos que la entrada sea en forma de dos matrices, una que contenga los símbolos (T y F) en orden y otra que contenga los operadores (&, | y ^}

 

Ejemplos:

 

Entrada: símbolo[] = {T, F, T}

       operador[] = {^, &}

Salida: 2

La expresión dada es "T ^ F & T", evalúa la verdadera

de dos maneras "((T ^ F) & T)" y "(T ^ (F & T))"

 

Casos:

T(i, i) = 1 si el símbolo[i] = "T

T(i, i) = 0 si el símbolo[i] = 'F'.

 

F(i, i) = 1 si el símbolo[i] = "F

F(i, i) = 0 si el símbolo[i] = 'T'.

Si dibujamos el árbol de recursividad de la solución recursiva anterior, podemos observar que tiene muchos subproblemas superpuestos. Como otros problemas de programación dinámica, puede ser resuelto llenando una tabla de abajo hacia arriba. A continuación se muestra la implementación en C++ de la solución de programación dinámica.

 

#include<iostream>

#include<cstring>

using namespace std;

  

int countParenth(char symb[], char oper[], int n)

{

    int F[n][n], T[n][n];

    for (int i = 0; i < n; i++)

    {

        F[i][i] = (symb[i] == 'F')? 1: 0;

        T[i][i] = (symb[i] == 'T')? 1: 0;

    }

  

  for (int gap=1; gap<n; ++gap)

    {

        for (int i=0, j=gap; j<n; ++i, ++j)

        {

            T[i][j] = F[i][j] = 0;

            for (int g=0; g<gap; g++)

            {

                int k = i + g;

                int tik = T[i][k] + F[i][k];

                int tkj = T[k+1][j] + F[k+1][j];

                if (oper[k] == '&')

                {

                    T[i][j] += T[i][k]*T[k+1][j];

                    F[i][j] += (tik*tkj - T[i][k]*T[k+1][j]);

                }

                if (oper[k] == '|')

                {

                    F[i][j] += F[i][k]*F[k+1][j];

                    T[i][j] += (tik*tkj - F[i][k]*F[k+1][j]);

                }

                if (oper[k] == '^')

                {

                    T[i][j] += F[i][k]*T[k+1][j] + T[i][k]*F[k+1][j];

                    F[i][j] += T[i][k]*T[k+1][j] + F[i][k]*F[k+1][j];

                }

            }

        }

    }

    return T[0][n-1];

}

  

//Principal

int main()

{

    char symbols[] = "TTFT";

    char operators[] = "|&^";

    int n = strlen(symbols);

  

    cout << countParenth(symbols, operators, n);

    return 0;

}

Salida:  4

Complejidad: O(n3)
Espacio auxiliar: O(n2)

 

 

&&&&&&&&&&&&&

&&&&&&&&&&

&&&

&