Programmation dynamique

icône de pdf
Signaler
Dans cette leçon, tu apprendras à utiliser la programmation dynamique pour résoudre efficacement des problèmes en évitant les calculs redondants. Tu découvriras comment mémoriser les résultats intermédiaires, choisir entre les approches top-down et bottom-up, et optimiser l’espace mémoire utilisé. Mots-clés : programmation dynamique, sous-problèmes, mémoïsation, complexité, Fibonacci, NSI.

Introduction

La programmation dynamique est une technique algorithmique puissante utilisée pour résoudre des problèmes de manière efficace en évitant la répétition de calculs redondants. Elle est particulièrement utile pour les problèmes d'optimisation combinatoire, où le nombre de solutions possibles augmente de façon exponentielle avec la taille de l'entrée. Cette méthode repose sur le principe de diviser un problème en sous-problèmes plus simples, de résoudre chaque sous-problème une seule fois, et de stocker les résultats pour éviter des calculs répétitifs. Dans cette leçon, nous allons explorer les principes de la programmation dynamique, comprendre comment l'implémenter et discuter de ses implications en termes de complexité mémoire.

Les principes de la programmation dynamique

Décomposition en sous-problèmes

La première étape de la programmation dynamique consiste à identifier comment un problème peut être décomposé en sous-problèmes plus petits. Pour cela, il est essentiel de comprendre la structure du problème et de déterminer comment chaque solution partielle contribue à la solution globale.

Exemple : le calcul de la suite de Fibonacci. La suite de Fibonacci est définie récursivement par :

  • F(0) = 0

  • F(1) = 1

  • F(n) = F(n-1) + F(n-2) pour n ≥ 2

La solution naïve consiste à utiliser une approche récursive directe, mais celle-ci est inefficace car elle recalculera plusieurs fois les mêmes valeurs. En utilisant la programmation dynamique, nous pouvons éviter cette redondance.

Exemple d’un autre problème typique : le rendu de monnaie. Il s'agit de trouver le nombre minimal de pièces nécessaires pour atteindre un montant donné à partir d'une liste de valeurs de pièces. Ce problème peut également être décomposé en sous-problèmes et résolu efficacement par programmation dynamique.

Mémorisation des résultats intermédiaires

Une fois les sous-problèmes identifiés, la prochaine étape est de mémoriser les résultats intermédiaires pour éviter des calculs inutiles. Cela se fait généralement à l'aide d'une structure de données telle qu’un tableau ou un dictionnaire.

Dans le cas de la suite de Fibonacci, nous pouvons utiliser un tableau pour stocker les valeurs déjà calculées :

picture-in-text

Dans cet exemple, chaque valeur de la suite est calculée une seule fois et stockée, ce qui réduit considérablement le nombre de calculs nécessaires.

Implémentation et coût mémoire

Approche top-down et bottom-up

Il existe deux approches principales pour implémenter la programmation dynamique : top-down et bottom-up.

L’approche top-down utilise la récursion avec mémoïsation, où les résultats des sous-problèmes sont stockés au fur et à mesure qu’ils sont calculés. On peut l’implémenter en Python à l’aide d’un dictionnaire :

picture-in-text

L’approche bottom-up, quant à elle, résout d’abord les sous-problèmes les plus petits et utilise ces solutions pour construire la solution du problème original. L’exemple précédent de la suite de Fibonacci avec tableau illustre cette approche.

Implications en termes de coût mémoire

La programmation dynamique, bien qu’efficace en termes de complexité temporelle, peut avoir un coût mémoire non négligeable. En effet, stocker les résultats intermédiaires pour tous les sous-problèmes peut nécessiter de l’espace proportionnel au nombre de sous-problèmes.

Ce coût dépend donc du nombre de sous-problèmes distincts à mémoriser. Dans l’exemple de Fibonacci, l’approche avec tableau utilise un espace de O(n), mais ce coût peut être réduit à O(1) si l’on ne conserve que les deux dernières valeurs calculées :

Cette version optimise l'utilisation de la mémoire tout en conservant l'efficacité de la programmation dynamique.

À retenir

La programmation dynamique permet de résoudre efficacement des problèmes présentant des chevauchements de sous-problèmes.

Elle repose sur la mémorisation des résultats intermédiaires pour éviter des recalculs inutiles.

Elle peut être implémentée en top-down (récursif avec mémoïsation) ou en bottom-up (itératif).

Le coût mémoire dépend du nombre de sous-problèmes à stocker, mais peut souvent être optimisé.

Conclusion

La programmation dynamique est une technique essentielle pour résoudre efficacement des problèmes complexes en informatique. Elle repose sur la décomposition du problème en sous-problèmes et la mémorisation des résultats intermédiaires. Bien que cette méthode améliore considérablement le temps d'exécution, elle peut entraîner une complexité mémoire significative, souvent proportionnelle au nombre de sous-problèmes distincts, et parfois optimisable. Maîtriser la programmation dynamique permet de concevoir des algorithmes plus performants et de mieux comprendre les compromis entre temps de calcul et usage de la mémoire. En s’exerçant à identifier les situations où cette méthode est applicable, on développe une compétence précieuse en algorithmique.