【C++】Programación dinámica

Blog de referencia: explicación detallada de la programación dinámica

1. ¿Qué es la programación dinámica?

La programación dinámica (inglés: programación dinámica, denominada DP) es un método utilizado en matemáticas, ciencias de la gestión, informática, economía y bioinformática para resolver problemas complejos descomponiendo el problema original en subproblemas relativamente simples. La programación dinámica suele ser adecuada para problemas con subproblemas superpuestos y propiedades óptimas de la subestructura.

1.1 Subproblemas superpuestos y subestructuras óptimas

1.1.1 Subproblemas superpuestos

Los subproblemas superpuestos se refieren a situaciones en las que los mismos subproblemas se utilizan varias veces durante el proceso de resolución del problema, es decir, en diferentes etapas de la resolución del problema, los subproblemas que deben resolverse pueden ser los mismos. La superposición de subproblemas es una de las bases del diseño de algoritmos de programación dinámica. El uso de la superposición de subproblemas puede reducir los cálculos repetidos y mejorar la eficiencia del algoritmo.

1.1.2 Subestructura óptima

Subestructura óptima significa que la solución óptima al problema se puede construir a partir de las soluciones óptimas a los subproblemas. Es decir, la solución óptima al problema contiene las soluciones óptimas a los subproblemas. En términos sencillos, la solución óptima para un problema grande se puede derivar de la solución óptima para un problema pequeño. Esta es una de las propiedades clave de la programación dinámica.

1.1.3 Ejemplos

Por ejemplo, supongamos que hay una secuencia que contiene n elementos y necesita encontrar la subsecuencia creciente más larga (LIS, Longest Increasing Subsequence). Este problema tiene propiedades de subestructura óptimas. Si se conoce el LIS de una secuencia, entonces si se añade un elemento al final, existen dos casos:

  1. Si el elemento es mayor que el último elemento del LIS actual, entonces el LIS de la nueva secuencia es el LIS actual más este elemento, y la longitud es la longitud del LIS de la secuencia original más 1;
  2. Si el elemento es menor o igual que el último elemento del LIS actual, el LIS actual no se verá afectado y el LIS de la nueva secuencia seguirá siendo el LIS de la secuencia original.

Por lo tanto, la solución óptima al problema se puede construir a partir de las soluciones óptimas de los subproblemas conocidos.

1.2 La idea central de la programación dinámica

La idea central de la programación dinámica es dividir subproblemas, recordar el pasado y reducir los cálculos repetidos.

Echemos un vistazo a un ejemplo popular en Internet:

A : "1+1+1+1+1+1+1+1 =?"
A : "上面等式的值是多少"
B : 计算 "8"
A : 在上面等式的左边写上 "1+" 呢?
A : "此时等式的值为多少"
B : 很快得出答案 "9"
A : "你怎么这么快就知道答案了"
A : "只要在8的基础上加1就行了"
A : "所以你不用重新计算,因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"

1.3 Saltar de la rana a la programación dinámica

Pregunta original de Leetcode: una rana puede saltar 1 o 2 pasos a la vez. Descubre de cuántas maneras puede la rana saltar una escalera de 10 niveles.

1.3.1 Ideas para la resolución de problemas

Idea básica: la programación dinámica determina gradualmente la solución del problema más grande a partir de la solución del problema más pequeño según la naturaleza superpuesta, desde la dirección de f (1) a f (10) y empuja hacia arriba para resolver el problema, por lo que se llama solución ascendente y ascendente.

¿Qué significa? Empujamos hacia arriba desde el primer paso. Supongamos que el número de saltos al enésimo paso se define como f(n):

  1. Cuando solo hay un paso, solo hay una forma de saltar, es decir, f (1) = 1;
  2. Cuando sólo hay 2 pasos, hay dos formas de saltar. La primera es saltarse dos niveles directamente. El segundo es saltarse un nivel primero y luego otro nivel. Es decir, f(2) = 2;
  3. Cuando hay 3 pasos, también hay dos formas de saltar. La primera es saltar dos pasos directamente desde el primer paso. El segundo es saltar un paso desde el paso 2. Es decir, f(3) = f(1) + f(2);
  4. Si desea saltar al cuarto paso, salte primero al tercer paso y luego suba al primer paso; o salte primero al segundo paso y luego suba 2 pasos a la vez. Es decir, f(4) = f(2) + f(3);

En este punto, podemos obtener la fórmula:

f(1) = 1;
f(2) = 2;
f(3) = f(1) + f(2);
f(4) = f(2) + f(3);

f(10) = f(8) + f(9);
y f(n) = f(n - 2) + f(n - 1)。

Llegados a este punto, echemos un vistazo a cómo se muestran las características típicas de la programación dinámica en esta pregunta:

  1. Subestructura óptima: f (n-1) y f (n-2) se denominan subestructura óptima de f (n).
  2. Subproblemas superpuestos: por ejemplo, f(10)= f(9)+f(8), f(9) = f(8) + f(7), f(8) es un subproblema superpuesto.
  3. Ecuación de transición de estado: f(n)= f(n-1)+f(n-2) se llama ecuación de transición de estado.
  4. Límite: f(1) = 1, f(2) = 2 es el límite.

1.3.2 Código

La idea del código es la siguiente:
Insertar descripción de la imagen aquí

Código:

class Solution {
    
    
public:
    int numWays(int n) {
    
    
        int dp[101] = {
    
    0};
        int mod = 1000000007;

        dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; i++) {
    
    
            dp[i] = dp[i - 1] + dp[i - 2];
            dp[i] %= mod;
        }

        return dp[n] % mod;
    }
};

La complejidad espacial de este método es O (n), sin embargo, si observa detenidamente la imagen de arriba, puede encontrar que f (n) solo depende de los dos primeros números, por lo que solo se necesitan dos variables a y b para almacenar. it, que puede satisfacer las necesidades. , por lo que la complejidad del espacio es O (1).
Insertar descripción de la imagen aquí

Código:

class Solution {
    
    
public:
    int numWays(int n) {
    
    
        if (n < 2) {
    
    
            return 1;
        }
        if (n == 2) {
    
    
            return 2;
        }
        int a = 1;
        int b = 2;
        int temp = 0;
        for (int i = 3; i <= n; i++) {
    
    
            temp = (a + b)% 1000000007;
            a = b;
            b = temp;
        }
        return temp;
    }
};

2. Rutinas de resolución de problemas de programación dinámica

2.1 Idea central

La idea central de la programación dinámica es dividir subproblemas, recordar el pasado y reducir los cálculos repetidos. Y la programación dinámica generalmente es de abajo hacia arriba, por lo que aquí, basándome en el problema del salto de la rana, resumo las ideas para la programación dinámica:

  1. análisis exhaustivo
  2. determinar los límites
  3. Encuentre las reglas y determine la subestructura óptima.
  4. Escribe la ecuación de transición de estado.

2.1.1 Análisis caso por caso

  1. análisis exhaustivo
  • Cuando el número de pasos es 1, existe un método de salto, f (1) = 1
  • Cuando solo hay 2 pasos, hay dos formas de saltar: la primera es saltar dos pasos directamente y la segunda es saltar un paso primero y luego otro paso. Es decir, f(2) = 2;
  • Cuando los pasos son de 3 pasos, si desea saltar al tercer paso, puede saltar primero al segundo paso y luego saltar al primer paso, o saltar primero al primer paso y luego subir 2 pasos en un momento. Entonces f(3) = f(2) + f(1) =3
  • Cuando los pasos son 4, si desea saltar al tercer paso, puede saltar primero al tercer paso y luego saltar 1 paso hacia arriba, o saltar primero al segundo paso y luego subir 2 pasos a la vez. . Entonces f(4) = f(3) + f(2) =5
  • Cuando los pasos son 5...
  1. determinar los límites

Mediante un análisis exhaustivo, encontramos que cuando el número de pasos es 1 o 2, podemos conocer claramente el método del salto de rana. f(1) =1, f(2) = 2, cuando el paso n>=3, se ha demostrado la regla f(3) = f(2) + f(1) =3, entonces f(1) = 1, f(2) = 2 es el límite para que salte la rana.

  1. Encuentre patrones y determine la subestructura óptima.

Cuando n>=3, se ha demostrado la regla f(n) = f(n-1) + f(n-2), por lo tanto, f(n-1) y f(n-2) se llaman f(n ) la subestructura óptima. ¿Cuál es la subestructura óptima? Existe tal explicación:

Un problema de programación dinámica es en realidad un problema de recursividad. Supongamos que el resultado de la decisión actual es f (n), entonces la subestructura óptima es hacer que f (nk) sea óptimo. La propiedad de la subestructura óptima es que la transición al estado de n es óptima y no tiene nada que ver con las decisiones posteriores. . , es decir, una propiedad que permite que decisiones posteriores utilicen de forma segura la solución óptima local anterior.

  1. A través de los tres pasos anteriores, análisis exhaustivo, determinación de límites y subestructura óptima, podemos derivar la ecuación de transición de estado:

Insertar descripción de la imagen aquí

3. Preguntas de ejemplo

3.1 Subsecuencia creciente

Insertar descripción de la imagen aquí

3.1.1 Análisis exhaustivo:

  1. Cuando nums es solo 10, la subsecuencia más larga [10] tiene una longitud de 1.
  2. Cuando se suma nums a 9, la subsecuencia más larga [10] o [9], longitud 1.
  3. Cuando se suma nums a 2, la subsecuencia más larga es [10] o [9] o [2], con longitud 1.
  4. Cuando se suma nums a 5, la subsecuencia más larga [2, 5], longitud 2.
  5. Cuando se suma nums a 3, la subsecuencia más larga es [2, 5] o [2, 3], con longitud 2.
  6. Cuando se suma nums a 7, la subsecuencia más larga es [2, 5, 7] o [2, 3, 7], con longitud 3.
  7. Cuando se agrega otro elemento 18 a nums, la subsecuencia creciente más larga es [2,5,7,101] o [2,3,7,101] o [2,5,7,18] o [2,3,7,18], la longitud es 4.

3.1.2 Determinar límites

Para cada elemento de la matriz nums, cuando no hemos comenzado a recorrer y buscar, su subsecuencia inicial más larga es su propia longitud de 1.

3.1.3 Encontrar patrones y determinar la subestructura óptima

A través del análisis anterior, podemos encontrar una regla:

Para la subsecuencia de aumento automático que termina en nums[i], simplemente busque la subsecuencia que termina en nums[j] más pequeña que nums[i] y agregue nums[i]. Obviamente, se pueden formar varias subsecuencias nuevas, elegimos la más larga, que es la subsecuencia creciente más larga.

Obtenga la subestructura óptima:

La subsecuencia creciente más larga (nums[i]) = max(la subsecuencia creciente más larga (nums[j])) + nums[i]; 0<= j < i, nums[j] < nums[i];

3.1.4 Escribe la ecuación de transición de estado.

Configuramos la matriz dp para almacenar la longitud de la subsecuencia más larga que termina con los elementos de la matriz nums, la inicializamos en 1 y obtenemos la ecuación de transición de estado de la subestructura óptima:

dp[i] = máx(dp[j]) + 1; 0<= j < i, números[j] < números[i];

3.1.5 Código

class Solution {
    
    
public:
    int lengthOfLIS(vector<int>& nums) {
    
    
        vector<int> dp(nums.size(), 1);
        int ans = 1;
        for (int i = 1; i < nums.size(); i++) {
    
    
            for (int j = 0; j < i; j++) {
    
    
                if (nums[j] < nums[i]) {
    
    
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

3.2 La cadena de hilo más larga

Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí

Esta pregunta es muy similar a la pregunta anterior, pero tenga en cuenta que esta pregunta no requiere que el orden de las palabras de la cadena de palabras esté en el orden de la matriz de palabras original.

3.2.1 Análisis exhaustivo

Tenga en cuenta aquí que debido a que esta pregunta no requiere que el orden de las palabras de la cadena de palabras sea el orden de la matriz de palabras original, nuestro análisis no puede ser exhaustivo en el orden de la matriz de palabras original. De acuerdo con las características de la cadena de palabras, debemos comenzar desde la palabra más corta en la matriz de palabras original y analizar exhaustivamente hasta la palabra más larga.

Ejemplo 1:

  1. Cuando las palabras solo tienen "a", la cadena de palabras más larga ["a"] tiene una longitud de 1.
  2. Cuando se agregan palabras a "b", la cadena de palabras más larga es ["a"] o ["b"], con una longitud de 1.
  3. Cuando se agregan palabras a "ba", la cadena de palabras más larga es ["a", "ba"] o ["b", "ba"]], con una longitud de 2.
  4. Cuando se agregan palabras a "bca", la cadena de palabras más larga es ["a", "ba", "bca"] o ["b", "ba", "bca"], con una longitud de 3.
  5. Cuando se agregan palabras a "bda", la cadena de palabras más larga es ["a", "ba", "bda"] o ["b", "ba", "bda"], con una longitud de 3.
  6. Cuando se agregan palabras a "bdca", la cadena de palabras más larga ["a", "ba", "bca", "bdca"] o ["b", "ba", "bca", "bdca"] o [ "a", "ba", "bda", "bdca"] o ["b", "ba", "bda", "bdca"], longitud 4.

3.2.2 Determinar límites

Para cada palabra en la matriz de palabras original, antes de comenzar a recorrer y buscar, su cadena de palabras más larga son ellos mismos, con una longitud de 1.

3.2.3 Encontrar patrones y determinar la subestructura óptima

Para cada palabra [i], si sus palabras predecesoras [j] existen en la matriz original, entonces una de sus cadenas de palabras es la cadena de palabras de sus palabras predecesoras [j] más sus propias palabras [i]. La subcadena más larga es la más larga entre ellas.

Obtenga la subestructura óptima

La subcadena más larga (palabras[i]) = max(palabras[j]) + palabras[i]; palabras[j] es la predecesora de palabras[i]

3.2.4 Escribe la ecuación de transición de estado.

Para garantizar que al atravesar la matriz original, atravesemos desde la palabra más corta en la matriz de palabras original hasta la palabra más larga, primero debemos ordenar la matriz original.

Configuramos la matriz dp para almacenar la longitud de la subcadena más larga de cada palabra en la matriz de palabras y obtenemos la ecuación de transición de estado.

dp[i] = max(dp[j]) + 1; 0 <= j < i, dp[j] es el predecesor de dp[i]

En esta ecuación, para encontrar el predecesor dp[j] de dp[i], necesitamos reducir las palabras[i] una letra a la vez para obtener todos sus predecesores y luego recorrer la parte anterior de la matriz de palabras original. palabras [i] Determine si este predecesor existe antes de continuar. Por supuesto, esto es engorroso y, a medida que las palabras se hacen más largas, el tiempo requerido se dispara. Entonces, ¿hay alguna manera de simplificar este proceso?

Sí, use una tabla hash para almacenar la longitud de subcadena más larga de cada palabra, usando la palabra misma como valor clave. De esta manera, utilizamos directamente todos los predecesores de palabras [i] para acceder a la tabla hash y podemos completar simultáneamente las dos tareas de determinar si este predecesor existe y operar en él.

Reescribe la ecuación de transición de estado usando una tabla hash.

dp[palabras[i]] = max(dp[palabra]) + 1; palabra son todos los predecesores de palabras[i]

3.2.5 Código

class Solution {
    
    
public:
    int longestStrChain(vector<string>& words) {
    
    
        unordered_map<string, int> cnt;
        sort(words.begin(), words.end(), [](const string a, const string b) {
    
    
            return a.size() < b.size();
        });
        int res = 0;
        for (string word : words) {
    
    
            cnt[word] = 1;
            for (int i = 0; i < word.size(); i++) {
    
    
                string prev = word.substr(0, i) + word.substr(i + 1, word.size());
                if (cnt[prev]) {
    
    
                    cnt[word] = max(cnt[prev] + 1, cnt[word]);
                }
            }
            res = max(cnt[word], res);
        }
        return res;
    }
};

Supongo que te gusta

Origin blog.csdn.net/m0_63852285/article/details/130541116
Recomendado
Clasificación