Partage de technologie

Notes d'étude des algorithmes (8) - Bases de la programmation dynamique

2024-07-12

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

Table des matières

Contenu de base :

Programmation dynamique:

Des problèmes de compréhension de la programmation dynamique sont introduits :

Analyse : (retour en arrière violent)

Exemple de code :

Recherche brutale :

Exemple de code Dfs : (recherche)

Arbre de récursion généré par récursion par force brute :

Recherche mémorisée :

Exemple de code :

Programmation dynamique:

Exemple de code : (programmation dynamique, en commençant par le plus petit sous-problème)

Processus d'exécution (programmation dynamique) :

Analyse : (programmation dynamique)

Optimisation de l'espace :

Exemple de code :

Analyse:


Contenu de base :

Qu'est-ce que la programmation dynamique, quels problèmes la programmation dynamique peut-elle résoudre comme moyen, la classification de la programmation dynamique et la classification des problèmes spécifiques que des classifications spécifiques peuvent résoudre.

Programmation dynamique:

Il s'agit d'un paradigme algorithmique important qui décompose un problème en une série de sous-problèmes plus petits et évite les calculs répétés en stockant les solutions des sous-problèmes, améliorant ainsi considérablement l'efficacité du temps.

Des problèmes de compréhension de la programmation dynamique sont introduits :

Ce problème est introduit dans le cas de la montée d'escaliers. Étant donné un escalier comportant n marches au total, vous pouvez monter 1 ou 2 marches à chaque marche. Combien y a-t-il d'options pour monter jusqu'au sommet du bâtiment ?

Analyse : (retour en arrière violent)

Le but de cette question est de trouver le nombre de solutions,On peut envisager d’épuiser exhaustivement toutes les possibilités en faisant marche arrière . Plus précisément, imaginez monter les escaliers comme un processus de sélection à plusieurs tours : en commençant par le sol, en choisissant une ou deux marches à chaque tour, en ajoutant 1 au nombre d'options à chaque fois que vous atteignez le haut de l'escalier, et en augmentant le nombre d'options lorsque vous montez les escaliers. vous atteignez le haut des escaliers.

exemple de code

  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. }

Recherche brutale :

Les algorithmes de backtracking ne démantelent généralement pas explicitement le problème, mais traitent le problème comme une série d’étapes de prise de décision et recherchent toutes les solutions possibles par le biais d’heuristiques et d’élagage.

Nous pouvons tenter d’analyser cette question sous l’angle de la décomposition du problème. Supposons qu'il existe des solutions dp[i] pour grimper au i-ème niveau, alors dp[i] est le problème d'origine, et ses sous-problèmes incluent :

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

Puisque nous ne pouvons monter que 1 ou 2 marches à chaque tour, lorsque nous nous tenons sur le ième escalier, nous ne pouvions monter que sur les marches i-1 ou i-2 au tour précédent. En d’autres termes, nous ne pouvons passer que du i-1ème ou du i-2ème niveau au i-ème niveau.

De là, nous pouvons tirer une conclusion importante : le nombre de plans qui ont grimpé au niveau i-1 plus le nombre de plans qui ont grimpé au niveau i-2 est égal au nombre de plans qui ont grimpé au niveau i. -ième niveau. La formule est la suivante :

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

Cela signifie qu'il existe une relation récursive dans le problème de l'escalade du bâtiment et que le problème d'origine peut être résolu en construisant les solutions des sous-problèmes.

Exemple de code Dfs : (recherche)

  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. }

Arbre récursif généré par une récursion violente

Pour résoudre le problème de duplication mentionné ci-dessus dans l'arbre récursif, le procédé de recherche de mémoire peut être utilisé pour supprimer un grand nombre de sous-arbres identiques qui sont construits de manière répétée, améliorant ainsi l'efficacité du calcul. (sous-problèmes qui se chevauchent

Recherche mémorisée :

Pour calculer tous les sous-problèmes qui se chevauchent une seule fois, vous devez déclarer un tableau nem pour enregistrer la solution de chaque sous-problème et élaguer les sous-problèmes qui se chevauchent pendant le processus de recherche.

  1. Lorsque dp[i] est calculé pour la première fois, il est enregistré dans nem[i] pour une utilisation ultérieure.
  2. Lorsque dp[i] est calculé à nouveau, le résultat est obtenu directement dans nem[i] pour éviter des calculs répétés de sous-problèmes.

Exemple de code :

  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. }

Après mémorisation, tous les sous-problèmes qui se chevauchent ne sont calculés qu'une seule fois et la complexité temporelle est optimisée à O(n).

Programmation dynamique:

La recherche mémorisée est une méthode "de haut en bas". Nous partons du problème d'origine (nœud racine) et décomposons de manière récursive les sous-problèmes plus importants en sous-problèmes plus petits jusqu'à ce que nous résolvions le plus petit sous-problème connu (nœud feuille). . Ensuite, les solutions aux sous-problèmes sont collectées couche par couche grâce à un retour en arrière pour construire une solution au problème d'origine.

En revanche, la programmation dynamique est une approche « de bas en haut » : en commençant par une solution au plus petit sous-problème et en construisant de manière itérative des solutions aux sous-problèmes plus importants jusqu'à ce qu'une solution au problème d'origine soit obtenue.

Étant donné que la programmation dynamique n'inclut pas de processus de retour en arrière, elle doit uniquement être implémentée en utilisant une itération de boucle sans utiliser de récursion.

Exemple de code : (programmation dynamique, en commençant par le plus petit sous-problème)

  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. }

Processus d'exécution (programmation dynamique) :

Analyse : (programmation dynamique)

Semblable à l'algorithme de backtracking, la programmation dynamique utilise également la notion d'« état » pour représenter une étape spécifique de résolution de problème. Chaque état correspond à un sous-problème et à la solution optimale locale correspondante. Exemple : L'état du problème de montée d'escalier est défini comme l'ordre i de l'escalier actuel.

Sur la base de ce qui précède, nous pouvons résumer les termes courants pour les termes dynamiques :

  1. Le tableau dp est appelé {dp table}, dp[i] représente la solution au sous-problème correspondant à l'état i
  2. L'état correspondant au sous-problème minimum (le premier et le deuxième escalier) est appelé état initial
  3. La formule récursive dp[i] = dp[i-1] + dp[i-2] est appelée l'équation d'état

Optimisation de l'espace :

dp[i] n'est lié qu'à dp[i-1] et dp[i-2]

Au lieu d'utiliser un tableau pour stocker les solutions à tous les sous-problèmes, seules deux variables sont nécessaires pour faire défiler vers l'avant.

Exemple de code :

  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. }

Analyse:

L'espace occupé par le tableau dp est omis et la complexité spatiale est réduite de O(n) à O(1)

Dans les problèmes de programmation dynamique, l'état actuel n'est lié qu'à un nombre limité d'états précédents. Pour le moment, nous ne pouvons conserver que les états nécessaires et économiser de l'espace mémoire grâce à la « réduction de dimensionnalité ». . Cette technique d'optimisation de l'espace est appelée « variables glissantes » ou « tableaux roulants ».