Compartir tecnología

Notas de estudio de algoritmos (8): conceptos básicos de programación dinámica

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Tabla de contenido

Contenido básico:

Programación dinámica:

Se introducen problemas para comprender la programación dinámica:

Análisis: (retroceso violento)

Ejemplo de código:

Búsqueda brutal:

Ejemplo de código DFS: (búsqueda)

Árbol de recursividad generado por recursividad de fuerza bruta:

Búsqueda memorizada:

Ejemplo de código:

Programación dinámica:

Ejemplo de código: (programación dinámica, comenzando desde el subproblema más pequeño)

Proceso de ejecución (programación dinámica):

Análisis: (programación dinámica)

Optimización del espacio:

Ejemplo de código:

Análisis:


Contenido básico:

Qué es la programación dinámica, qué problemas puede resolver la programación dinámica como medio, la clasificación de la programación dinámica y la clasificación de problemas específicos que las clasificaciones específicas pueden resolver.

Programación dinámica:

Es un paradigma algorítmico importante que descompone un problema en una serie de subproblemas más pequeños y evita cálculos repetidos al almacenar soluciones de subproblemas, lo que mejora en gran medida la eficiencia del tiempo.

Se introducen problemas para comprender la programación dinámica:

Este problema se introduce a través del caso de subir escaleras. Dada una escalera con n escalones en total, puedes subir 1 o 2 escalones en cada escalón. ¿Cuántas opciones hay para subir a la cima del edificio?

Análisis: (retroceso violento)

El objetivo de esta pregunta es encontrar el número de soluciones,Podemos considerar agotar exhaustivamente todas las posibilidades retrocediendo . Específicamente, imagine subir escaleras como un proceso de selección de múltiples rondas: comenzar desde el suelo, elegir uno o dos escalones en cada ronda, agregar 1 al número de opciones cada vez que llegue a la cima de las escaleras y aumentar el número de opciones cuando Llegas a lo alto de las escaleras.

ejemplo de código

  1. # python代码示例
  2. def backrack(choices,state,n,res) :
  3. if state == n :
  4. res[0] += 1
  5. for choice in choices :
  6. if state + choice > n :
  7. continue
  8. backrack(choices,state+choice,n,res)
  9. def climbing_stairs_backrack(n) :
  10. choices = [1,2]
  11. state = 0
  12. res = [0]
  13. backrack(choices,state,n,res)
  14. return res[0]
  15. n = int(input())
  16. print(climbing_stairs_backrack(n))
  1. // c++代码示例
  2. void backrack(vector<int> &choices, int state, int n, vector<int> &res)
  3. {
  4. if (state == n )
  5. {
  6. res[0]++ ;
  7. }
  8. for (auto &choice : choices)
  9. {
  10. if (state + choice > n)
  11. {
  12. continue ;
  13. }
  14. backrack(choices, state + choice, n, res)
  15. }
  16. }
  17. int climbingStairsBackrack(int n)
  18. {
  19. vector<int> choices = {1 , 2 } ;
  20. int state = 0 ;
  21. vector<int> res = [0] ;
  22. backrack(choices, state, n, res) ;
  23. return res[0] ;
  24. }

Búsqueda brutal:

Los algoritmos de retroceso generalmente no desmantelan explícitamente el problema, sino que lo tratan como una serie de pasos de toma de decisiones y buscan todas las soluciones posibles mediante heurísticas y poda.

Podemos intentar analizar esta cuestión desde la perspectiva de la descomposición del problema. Supongamos que hay soluciones dp [i] para subir al nivel i-ésimo, entonces dp [i] es el problema original y sus subproblemas incluyen:

dp[i-1], dp[i-2], dp[1], dp[2]

Como solo podemos subir 1 o 2 escalones en cada ronda, cuando nos paramos en la i-ésima escalera, solo pudimos pararnos en los escalones i-1 o i-2 de la ronda anterior. En otras palabras, solo podemos pasar del nivel i-1 o i-2 al nivel i-ésimo.

De esto, podemos sacar una inferencia importante: el número de planes que han subido al nivel i-1 más el número de planes que han subido al nivel i-2 es igual al número de planes que han subido al i -ésimo nivel. La fórmula es la siguiente:

dp[i] = dp[i-1] + dp[i-2]

Esto significa que existe una relación recursiva en el problema de escalada de edificios y el problema original se puede resolver construyendo las soluciones de los subproblemas.

Ejemplo de código DFS: (búsqueda)

  1. # python 代码示例
  2. def dfs(i : int) -> int :
  3. if i == 1 or i == 2 :
  4. return i
  5. count = dfs(i - 1) + dfs(i - 2)
  6. return count
  7. def climbing_stairs_dfs(n : int) -> int :
  8. retunr dfs(n)
  1. // c++ 代码示例
  2. int dfs(int i)
  3. {
  4. if (i == 1 || i == 2)
  5. {
  6. return i ;
  7. }
  8. int count = dfs(i - 1) + dfs(i - 2);
  9. return count ;
  10. }
  11. int climbingStairsDFS(int n)
  12. {
  13. retunr dfs(n) ;
  14. }

Árbol recursivo generado por recursividad violenta.

Para resolver el problema de duplicación mencionado anteriormente en el árbol recursivo, se puede utilizar el método de búsqueda de memoria para eliminar una gran cantidad de subárboles idénticos que se construyen repetidamente, mejorando así la eficiencia del cálculo. (subproblemas superpuestos

Búsqueda memorizada:

Para calcular todos los subproblemas superpuestos solo una vez, debe declarar una matriz nem para registrar la solución de cada subproblema y podar los subproblemas superpuestos durante el proceso de búsqueda.

  1. Cuando se calcula dp[i] por primera vez, se registra en nem[i] para su uso posterior.
  2. Cuando se vuelve a calcular dp [i], el resultado se obtiene directamente en nem [i] para evitar cálculos repetidos de subproblemas.

Ejemplo de código:

  1. # python 代码示例
  2. def dfs(i : int, mem : list[int]) -> int :
  3. if i == 1 or i == 2 :
  4. return i
  5. if mem[i] != -1 :
  6. return mem[i]
  7. count = dfs(i - 1, mem) + dfs(i - 2, mem)
  8. # 记录dfs(i)
  9. mem[i] = count
  10. return count
  11. def climbing_stairs_dfs_mem(n : int) -> int :
  12. mem = [-1] * (n + 1)
  13. return dfs(n, mem)
  1. // c++ 代码示例
  2. int dfs(int i, vector<int> &mem)
  3. {
  4. if (i == 1 || i == 2)
  5. {
  6. return i ;
  7. }
  8. if (mem != -1)
  9. {
  10. return mem[i] ;
  11. }
  12. int count = dfs(i - 1, mem) + dfs(i - 2, mem) ;
  13. mem[i] = count ;
  14. return count ;
  15. }
  16. int climbingStairsDFSMem(int n)
  17. {
  18. vector<int> mem(n + 1, -1) ;
  19. return dfs(n, mem) ;
  20. }

Después de la memorización, todos los subproblemas superpuestos se calculan solo una vez y la complejidad del tiempo se optimiza a O (n).

Programación dinámica:

La búsqueda memorizada es un método "de arriba a abajo". Partimos del problema original (nodo raíz) y descomponemos recursivamente los subproblemas más grandes en subproblemas más pequeños hasta resolver el subproblema más pequeño conocido (nodo hoja). . Luego, las soluciones a los subproblemas se recopilan capa por capa mediante un retroceso para construir una solución al problema original.

Por el contrario, la programación dinámica es un enfoque "de abajo hacia arriba": comienza con una solución al subproblema más pequeño y construye iterativamente soluciones para subproblemas más grandes hasta obtener una solución al problema original.

Dado que la programación dinámica no incluye un proceso de retroceso, solo debe implementarse mediante iteración de bucle sin utilizar recursividad.

Ejemplo de código: (programación dinámica, comenzando desde el subproblema más pequeño)

  1. # python 代码示例
  2. def clibing_stairs_dp(n) :
  3. if n == 1 or n == 2 :
  4. return n
  5. dp = [0] * (n + 1)
  6. dp[1], dp[2] = 1, 2
  7. for i in range(3,n + 1) :
  8. dp[i] = dp[i-1] + dp[i- 2]
  9. return dp[n]
  1. // c++ 代码示例
  2. int climbingStairsDP(int n)
  3. {
  4. if (n == 1 || n == 2)
  5. {
  6. retunr n ;
  7. }
  8. vector<int> dp(n + 1, -1) ;
  9. dp[1] = 1 ;
  10. dp[2] = 2 ;
  11. for (int i = 3 ; i <= n ; i++)
  12. {
  13. dp[i] = dp[i - 1] + dp[i- 2] ;
  14. }
  15. return dp[n] ;
  16. }

Proceso de ejecución (programación dinámica):

Análisis: (programación dinámica)

Al igual que el algoritmo de retroceso, la programación dinámica también utiliza el concepto de "estado" para representar una etapa específica de la resolución del problema. Cada estado corresponde a un subproblema y la solución óptima local correspondiente. Ejemplo: El estado del problema de subir escaleras se define como el orden i de la escalera actual.

Con base en lo anterior, podemos resumir los términos comunes para términos dinámicos:

  1. La matriz dp se llama {tabla dp}, dp[i] representa la solución al subproblema correspondiente al estado i
  2. El estado correspondiente al subproblema mínimo (la primera y segunda escalera) se llama estado inicial.
  3. La fórmula recursiva dp[i] = dp[i-1] + dp[i-2] se llama ecuación de estado.

Optimización del espacio:

dp[i] solo está relacionado con dp[i-1] y dp[i-2]

En lugar de utilizar una matriz para almacenar las soluciones de todos los subproblemas, sólo se necesitan dos variables para desplazarse hacia adelante.

Ejemplo de código:

  1. # python 代码示例
  2. def clibing_stairs_dp_comp(n) :
  3. if n == 1 or n == 2 :
  4. return n
  5. a, b = 1, 2
  6. for _ in range(3, n + 1) :
  7. a, b = b , a + b
  8. return b
  1. // c++ 代码示例
  2. int climbingStairsComp(int n)
  3. {
  4. if (n == 1 || n == 2)
  5. {
  6. return n ;
  7. }
  8. int a = 1 , b = 2 ;
  9. for (int i = 3 ; i <= n ; i++)
  10. {
  11. int temp = b ;
  12. b = a + b ;
  13. a = temp ;
  14. }
  15. return b ;
  16. }

Análisis:

Se omite el espacio ocupado por la matriz dp y la complejidad del espacio se reduce de O (n) a O (1)

En los problemas de programación dinámica, el estado actual solo está relacionado con un número limitado de estados anteriores. En este momento, solo podemos retener los estados necesarios y ahorrar espacio en la memoria mediante la "reducción de dimensionalidad". . Esta técnica de optimización del espacio se denomina "variables rodantes" o "matrices rodantes".