Chapitre 15 — Introduction à la programmation dynamique
Mémoriser les sous-problèmes, organiser les dépendances et construire une solution optimale ÉTAT → RÉCURRENCE → CAS DE BASE → MÉMOÏSATION → TABULATION → SOLUTION |
Fiche pédagogique du chapitre
Objectifs d’apprentissage
À la fin de ce chapitre, l’étudiant devra être capable de :
• reconnaître un problème présentant des sous-problèmes répétés et une sous-structure optimale ;
• expliquer pourquoi une récursion naïve peut entraîner un coût exponentiel ;
• transformer une solution récursive en solution mémoïsée, puis en solution tabulée ;
• définir précisément l’état, la relation de transition, les cas de base et l’ordre de calcul ;
• analyser le temps d’exécution et la mémoire d’une solution dynamique ;
• reconstruire une solution optimale à partir d’une table de décisions ;
• appliquer la programmation dynamique à Fibonacci, aux chemins dans une grille, au sac à dos, au chemin de somme minimale et au rendu de monnaie optimal ;
• choisir entre récursion naïve, mémoïsation, tabulation et optimisation mémoire selon le contexte.
Prérequis
• fonctions, procédures et récursivité ;
• tableaux à une et deux dimensions ;
• relations de récurrence simples et analyse asymptotique ;
• méthode diviser pour régner ;
• notions d’optimalité, de minimum, de maximum et de solution réalisable.
Organisation indicative
Activité | Volume conseillé | Finalité |
|---|---|---|
| Cours | 4 à 6 h | Comprendre les principes, les modèles d’état et les preuves. |
| Travaux dirigés | 4 h | Formuler des récurrences, remplir des tables et analyser les coûts. |
| Travaux pratiques | 4 à 6 h | Implémenter, tester et comparer les approches. |
| Évaluation | 1 à 2 h | Conception d’une solution dynamique et justification. |
Idée directrice La programmation dynamique ne consiste pas seulement à « remplir un tableau ». Elle repose sur une modélisation : choisir un état qui contient exactement l’information nécessaire, exprimer cet état à partir d’états plus simples, puis calculer chaque état une seule fois. • Sous-problèmes répétés : les mêmes calculs réapparaissent. • Sous-structure optimale : une solution optimale se construit à partir de solutions optimales de sous-problèmes. |
Introduction générale
De nombreux problèmes algorithmiques semblent difficiles parce qu’une solution récursive explore un grand nombre de possibilités et recalcule plusieurs fois les mêmes résultats. La programmation dynamique transforme cette répétition en avantage : lorsqu’un sous-problème est résolu, son résultat est conservé afin d’être réutilisé.
Cette méthode est particulièrement efficace lorsque le problème possède deux propriétés : des sous-problèmes qui se recouvrent et une sous-structure optimale. Elle permet souvent de remplacer un temps exponentiel par un temps polynomial, au prix d’une mémoire supplémentaire.
Programmation dynamique ou diviser pour régner ? • Diviser pour régner : les sous-problèmes sont généralement indépendants. • Programmation dynamique : plusieurs branches ont besoin des mêmes sous-problèmes. • Dans les deux cas, on décompose le problème ; la différence essentielle est la réutilisation des résultats. |
Méthode | Calcul des sous-problèmes | Stockage | Ordre |
|---|---|---|---|
| Récursion naïve | Plusieurs fois | Aucun | Descendant |
| Mémoïsation | À la demande, une seule fois | Cache | Descendant |
| Tabulation | Une seule fois, systématiquement | Table | Ascendant |
15.1 Problèmes à sous-problèmes répétés
15.1.1 Calcul naïf
Une solution naïve applique directement la définition récursive du problème. Elle peut être correcte et très lisible, mais elle ne tient pas compte du fait que plusieurs appels demandent exactement le même résultat.
Exemple introductif — Fibonacci
La suite est définie par F(0) = 0, F(1) = 1 et F(n) = F(n − 1) + F(n − 2) pour n ≥ 2.
ZONE DE PSEUDO-CODE — Fibonacci récursif naïf Fonction FibonacciNaif(n : Entier) : Entier Si n = 0 Alors Retourner 0 SinonSi n = 1 Alors Retourner 1 Sinon Retourner FibonacciNaif(n - 1) + FibonacciNaif(n - 2) FinSi FinFonction La fonction est correcte, mais elle recalcule de nombreux termes. |
15.1.2 Appels identiques
Pour calculer F(5), l’appel F(3) apparaît dans les branches issues de F(4) et de F(3). De même, F(2), F(1) et F(0) sont évalués plusieurs fois. L’arbre des appels contient donc beaucoup de nœuds identiques.
ZONE DE PSEUDO-CODE — Arbre partiel des appels pour F(5) F(5) ├── F(4) │ ├── F(3) │ │ ├── F(2) │ │ └── F(1) │ └── F(2) └── F(3) ├── F(2) └── F(1) Les appels F(3) et F(2) sont répétés : ils constituent des sous-problèmes qui se recouvrent. |
Sous-problème | Nombre d’occurrences dans le calcul de F(5) | Observation |
|---|---|---|
| F(5) | 1 | Problème initial |
| F(4) | 1 | Sous-problème unique |
| F(3) | 2 | Résultat recalculé |
| F(2) | 3 | Résultat fortement répété |
| F(1) | 5 | Cas de base répété |
| F(0) | 3 | Cas de base répété |
15.1.3 Coût exponentiel
Le nombre d’appels du Fibonacci naïf croît approximativement comme la suite elle-même. Sa complexité est souvent majorée par O(2ⁿ). Une augmentation modeste de n peut donc multiplier fortement le temps d’exécution.
n | F(n) | Ordre de grandeur des appels naïfs | Effet pratique |
|---|---|---|---|
| 10 | 55 | Quelques centaines | Instantané |
| 20 | 6 765 | Dizaines de milliers | Encore acceptable |
| 30 | 832 040 | Millions | Ralentissement visible |
| 40 | 102 334 155 | Centaines de millions | Très lent |
| 50 | 12 586 269 025 | Dizaines de milliards | Inexploitable sans optimisation |
Attention Une récursion n’est pas automatiquement exponentielle. Le coût dépend du nombre d’appels produits par chaque état et du nombre d’états distincts. Une récursion qui ne crée qu’un appel de taille n − 1 peut rester linéaire. |
15.1.4 Sous-problèmes recouvrants et sous-structure optimale
Deux propriétés guident l’utilisation de la programmation dynamique :
• Sous-problèmes recouvrants : plusieurs chemins de calcul demandent le même état.
• Sous-structure optimale : une solution optimale du problème peut être obtenue en combinant des solutions optimales de sous-problèmes.
Question de diagnostic | Réponse favorable à la programmation dynamique |
|---|---|
| Les mêmes paramètres d’appel réapparaissent-ils ? | Oui : une mémorisation peut éviter des recalculs. |
| La solution globale dépend-elle de solutions plus petites ? | Oui : une relation de transition peut être définie. |
| Le nombre d’états distincts est-il raisonnable ? | Oui : la table reste calculable et stockable. |
| L’ordre des dépendances peut-il être déterminé ? | Oui : la tabulation est envisageable. |
15.1.5 Modéliser un état
Un état est une description compacte d’un sous-problème. Il doit contenir assez d’informations pour déterminer la suite du calcul, sans stocker de détails inutiles.
Problème | Exemple d’état | Signification |
|---|---|---|
| Fibonacci | dp[i] | Valeur de F(i) |
| Chemins dans une grille | dp[i][j] | Nombre de chemins jusqu’à la case (i, j) |
| Sac à dos | dp[i][c] | Meilleure valeur avec les i premiers objets et une capacité c |
| Somme minimale | dp[i][j] | Coût minimal pour atteindre la case (i, j) |
| Monnaie optimale | dp[s] | Nombre minimal de pièces pour former la somme s |
15.2 Mémoïsation
15.2.1 Approche descendante
La mémoïsation conserve la structure récursive. La fonction commence par le problème complet et descend vers les sous-problèmes nécessaires. Avant tout calcul, elle consulte un cache. Si le résultat existe déjà, il est retourné immédiatement.
ZONE DE PSEUDO-CODE — Schéma générique de mémoïsation Fonction Resoudre(etat) Si etat est un cas de base Alors Retourner ValeurDeBase(etat) FinSi
Si Memo contient etat Alors Retourner Memo[etat] FinSi
resultat ← Combiner(Resoudre(sousEtat1), Resoudre(sousEtat2), ...) Memo[etat] ← resultat Retourner resultat FinFonction |
15.2.2 Stockage des résultats
Le cache peut être un tableau, une matrice ou un dictionnaire. Le choix dépend de la forme des états.
Type d’état | Structure adaptée | Exemple |
|---|---|---|
| Entier dans un intervalle dense | Tableau | Fibonacci : n entre 0 et N |
| Deux indices bornés | Matrice | Grille ou sac à dos |
| États irréguliers ou clairsemés | Dictionnaire | Positions accessibles uniquement |
| État composé | Dictionnaire avec clé structurée | (indice, capacité, masque, ... ) |
15.2.3 Réutilisation des valeurs calculées
ZONE DE PSEUDO-CODE — Fibonacci mémoïsé Fonction FibonacciMemo(n : Entier, Memo : Tableau) : Entier Si n = 0 Alors Retourner 0 FinSi Si n = 1 Alors Retourner 1 FinSi
Si Memo[n] ≠ NON_CALCULE Alors Retourner Memo[n] FinSi
Memo[n] ← FibonacciMemo(n - 1, Memo) + FibonacciMemo(n - 2, Memo) Retourner Memo[n] FinFonction |
Avec cette transformation, chaque valeur F(i) est calculée au plus une fois. Le temps devient O(n), tandis que le tableau et la pile récursive utilisent O(n) mémoire.
Critère | Récursion naïve | Mémoïsation |
|---|---|---|
| Nombre d’états calculés | Nombreux appels répétés | Au plus n + 1 états distincts |
| Temps | Exponentiel | Linéaire |
| Mémoire de cache | Aucune | O(n) |
| Pile d’appels | O(n) | O(n) |
| Lisibilité | Très proche de la définition | Toujours proche de la définition |
15.2.4 Valeur sentinelle et état réellement calculé
Il faut distinguer une valeur non calculée d’un résultat valide. Une valeur sentinelle telle que −1 ne convient que si −1 ne peut jamais être une réponse. Sinon, on utilise un tableau booléen Calcule, une valeur spéciale ou la présence de la clé dans un dictionnaire.
Erreur fréquente Initialiser la table à 0 puis interpréter 0 comme « non calculé » est incorrect lorsque 0 est une réponse possible. C’est le cas de nombreux problèmes de comptage ou d’optimisation. |
15.2.5 Complexité de la mémoïsation
Une estimation pratique est : nombre d’états distincts × coût de traitement d’un état. Si chaque état examine k transitions, le temps est généralement O(nombre d’états × k).
Problème | Nombre d’états | Transitions par état | Temps mémoïsé |
|---|---|---|---|
| Fibonacci | O(n) | O(1) | O(n) |
| Grille n × m | O(nm) | O(1) | O(nm) |
| Sac à dos n, capacité C | O(nC) | O(1) | O(nC) |
| Monnaie, somme S, k pièces | O(S) | O(k) | O(kS) |
15.2.6 Avantages et limites
Avantages | Limites |
|---|---|
| Transformation naturelle d’une récursion existante. | Utilise la pile d’appels récursifs. |
| Calcule uniquement les états réellement nécessaires. | Peut provoquer un dépassement de pile pour une grande profondeur. |
| Adaptée aux états clairsemés avec un dictionnaire. | L’ordre de calcul est moins visible. |
| Facilite parfois la preuve par récurrence. | Le cache doit être correctement initialisé et indexé. |
15.3 Tabulation
15.3.1 Approche ascendante
La tabulation commence par les cas de base et construit progressivement les états plus grands. Elle remplace les appels récursifs par des boucles et rend explicite l’ordre des dépendances.
ZONE DE PSEUDO-CODE — Schéma générique de tabulation Initialiser la table DP avec les cas de base
Pour chaque etat dans un ordre valide Faire DP[etat] ← Combiner(DP[etatPrecedent1], DP[etatPrecedent2], ...) FinPour
Retourner DP[etatFinal] |
15.3.2 Tableau de résultats
ZONE DE PSEUDO-CODE — Fibonacci tabulé Fonction FibonacciTab(n : Entier) : Entier Si n = 0 Alors Retourner 0 FinSi
DP[0] ← 0 DP[1] ← 1 Pour i allant de 2 à n Faire DP[i] ← DP[i - 1] + DP[i - 2] FinPour Retourner DP[n] FinFonction |
i | DP[i − 2] | DP[i − 1] | DP[i] |
|---|---|---|---|
| 2 | 0 | 1 | 1 |
| 3 | 1 | 1 | 2 |
| 4 | 1 | 2 | 3 |
| 5 | 2 | 3 | 5 |
| 6 | 3 | 5 | 8 |
15.3.3 Ordre de calcul
Un état ne peut être calculé que lorsque tous les états dont il dépend sont disponibles. L’ordre de parcours constitue donc une partie essentielle de la conception.
Dépendance | Ordre possible |
|---|---|
| dp[i] dépend de dp[i − 1] et dp[i − 2] | i croissant |
| dp[i][j] dépend du haut et de la gauche | lignes et colonnes croissantes |
| dp[i][c] dépend de la ligne i − 1 | objets croissants ; capacité selon la formulation |
| Mise à jour 1D du sac à dos 0/1 | capacité décroissante pour éviter de réutiliser un objet |
15.3.4 Mémoïsation et tabulation : comparaison
Critère | Mémoïsation | Tabulation |
|---|---|---|
| Sens | Descendant | Ascendant |
| Contrôle | Récursion | Boucles |
| États calculés | Seulement les états visités | Souvent tous les états de la table |
| Pile | Oui | Non |
| Ordre des dépendances | Implicite dans les appels | Doit être choisi explicitement |
| Optimisation mémoire | Possible mais moins directe | Souvent naturelle |
| États clairsemés | Très adaptée | Peut remplir inutilement une grande table |
15.3.5 Optimisation de la mémoire
Si la transition n’utilise qu’un petit nombre d’états précédents, la table complète n’est pas toujours nécessaire. Pour Fibonacci, deux variables suffisent.
ZONE DE PSEUDO-CODE — Fibonacci avec mémoire constante Fonction FibonacciOptimise(n : Entier) : Entier Si n = 0 Alors Retourner 0 FinSi precedent2 ← 0 precedent1 ← 1 Pour i allant de 2 à n Faire courant ← precedent1 + precedent2 precedent2 ← precedent1 precedent1 ← courant FinPour Retourner precedent1 FinFonction |
Compromis important Réduire la mémoire peut empêcher la reconstruction d’une solution complète. Si l’on doit retrouver les objets choisis, le chemin suivi ou les pièces utilisées, il faut conserver des décisions ou reconstruire à partir de la table. |
15.4 Exemples détaillés
15.4.1 Fibonacci
Fibonacci illustre clairement les sous-problèmes répétés, mais ne constitue pas toujours le meilleur exemple d’optimisation réelle. Il permet néanmoins de comparer les trois approches.
Approche | Temps | Mémoire | Caractéristique |
|---|---|---|---|
| Naïve | O(2ⁿ) | O(n) pile | Recalcul massif |
| Mémoïsée | O(n) | O(n) cache + pile | États calculés à la demande |
| Tabulée | O(n) | O(n) | Ordre explicite |
| Optimisée | O(n) | O(1) | Deux valeurs précédentes |
15.4.2 Nombre de chemins dans une grille
On considère une grille de n lignes et m colonnes. Depuis la case supérieure gauche, on peut uniquement se déplacer vers la droite ou vers le bas. Le nombre de chemins vers (i, j) est la somme des chemins vers la case située au-dessus et vers celle située à gauche.
État : dp[i][j] représente le nombre de chemins permettant d’atteindre la case (i, j).
Transition : dp[i][j] = dp[i − 1][j] + dp[i][j − 1].
Cas de base : la première ligne et la première colonne contiennent une seule possibilité, en l’absence d’obstacle.
ZONE DE PSEUDO-CODE — Nombre de chemins — tabulation Fonction NombreChemins(n, m : Entiers) : Entier Créer DP[n][m] Pour i allant de 0 à n - 1 Faire DP[i][0] ← 1 FinPour Pour j allant de 0 à m - 1 Faire DP[0][j] ← 1 FinPour
Pour i allant de 1 à n - 1 Faire Pour j allant de 1 à m - 1 Faire DP[i][j] ← DP[i - 1][j] + DP[i][j - 1] FinPour FinPour Retourner DP[n - 1][m - 1] FinFonction |
| Col. 0 | Col. 1 | Col. 2 | Col. 3 |
|---|---|---|---|---|
| Ligne 0 | 1 | 1 | 1 | 1 |
| Ligne 1 | 1 | 2 | 3 | 4 |
| Ligne 2 | 1 | 3 | 6 | 10 |
Pour une grille 3 × 4, le résultat est 10. La complexité est O(nm) en temps et O(nm) en mémoire. Une seule ligne suffit si l’on ne reconstruit pas les chemins.
Variante avec obstacles
Une case bloquée reçoit 0 chemin. Les autres cases conservent la même transition. La valeur de la case initiale vaut 1 uniquement si elle est accessible.
ZONE DE PSEUDO-CODE — Nombre de chemins avec obstacles DP[0][0] ← 1 si la case initiale est libre, sinon 0 Pour chaque case (i, j) dans l’ordre ligne par ligne Faire Si Grille[i][j] est bloquée Alors DP[i][j] ← 0 SinonSi (i, j) ≠ (0, 0) Alors DP[i][j] ← ValeurHaut(i, j) + ValeurGauche(i, j) FinSi FinPour |
15.4.3 Problème du sac à dos simplifié
On dispose de n objets. L’objet i possède un poids poids[i] et une valeur valeur[i]. Le sac a une capacité C. Chaque objet peut être choisi au plus une fois. L’objectif est de maximiser la valeur totale sans dépasser C.
État : dp[i][c] est la valeur maximale obtenue avec les i premiers objets et une capacité c.
Transition : si poids[i] > c, l’objet ne peut pas être pris. Sinon, on choisit le meilleur résultat entre ne pas le prendre et le prendre.
ZONE DE PSEUDO-CODE — Sac à dos 0/1 — tabulation 2D Fonction SacADos(Poids, Valeurs, n, C) : Entier Créer DP[0..n][0..C] initialisé à 0
Pour i allant de 1 à n Faire Pour c allant de 0 à C Faire DP[i][c] ← DP[i - 1][c] Si Poids[i] ≤ c Alors DP[i][c] ← Maximum(DP[i][c], Valeurs[i] + DP[i - 1][c - Poids[i]]) FinSi FinPour FinPour Retourner DP[n][C] FinFonction |
Objet | Poids | Valeur |
|---|---|---|
| A | 2 | 3 |
| B | 3 | 4 |
| C | 4 | 5 |
i \ capacité | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| 0 objet | 0 | 0 | 0 | 0 | 0 | 0 |
| A | 0 | 0 | 3 | 3 | 3 | 3 |
| A, B | 0 | 0 | 3 | 4 | 4 | 7 |
| A, B, C | 0 | 0 | 3 | 4 | 5 | 7 |
Pour C = 5, la meilleure valeur est 7, obtenue avec A et B. Le temps et la mémoire de la version 2D sont O(nC).
Reconstruction des objets choisis
ZONE DE PSEUDO-CODE — Reconstruction dans la table du sac à dos c ← C Pour i allant de n à 1 avec un pas de -1 Faire Si DP[i][c] ≠ DP[i - 1][c] Alors Sélectionner l’objet i c ← c - Poids[i] FinSi FinPour |
Pseudo-polynomialité La complexité O(nC) dépend de la valeur numérique de la capacité C, et non seulement du nombre de bits nécessaires pour l’écrire. L’algorithme est donc dit pseudo-polynomial. |
15.4.4 Chemin de somme minimale
Dans ce chapitre, « somme minimale » désigne le problème suivant : chaque case d’une grille possède un coût. On cherche un chemin de la case supérieure gauche à la case inférieure droite, avec des déplacements vers la droite ou vers le bas, minimisant la somme des coûts visités.
État : dp[i][j] représente le coût minimal pour atteindre (i, j).
Transition : dp[i][j] = cout[i][j] + min(dp[i − 1][j], dp[i][j − 1]).
ZONE DE PSEUDO-CODE — Chemin de somme minimale — tabulation Fonction SommeMinimale(Cout, n, m) : Entier Créer DP[n][m] DP[0][0] ← Cout[0][0]
Pour i allant de 1 à n - 1 Faire DP[i][0] ← Cout[i][0] + DP[i - 1][0] FinPour Pour j allant de 1 à m - 1 Faire DP[0][j] ← Cout[0][j] + DP[0][j - 1] FinPour
Pour i allant de 1 à n - 1 Faire Pour j allant de 1 à m - 1 Faire DP[i][j] ← Cout[i][j] + Minimum(DP[i - 1][j], DP[i][j - 1]) FinPour FinPour Retourner DP[n - 1][m - 1] FinFonction |
Grille des coûts | Col. 0 | Col. 1 | Col. 2 |
|---|---|---|---|
| Ligne 0 | 1 | 3 | 1 |
| Ligne 1 | 1 | 5 | 1 |
| Ligne 2 | 4 | 2 | 1 |
Table DP | Col. 0 | Col. 1 | Col. 2 |
|---|---|---|---|
| Ligne 0 | 1 | 4 | 5 |
| Ligne 1 | 2 | 7 | 6 |
| Ligne 2 | 6 | 8 | 7 |
Le coût minimal est 7. Pour reconstruire le chemin, on remonte depuis la destination vers le voisin ayant fourni le minimum.
15.4.5 Rendu de monnaie optimal
On connaît des valeurs de pièces et une somme S. On cherche le nombre minimal de pièces nécessaire pour former exactement S. Contrairement à une stratégie gloutonne, la programmation dynamique garantit l’optimalité pour tout système de pièces.
État : dp[s] est le nombre minimal de pièces pour former la somme s.
Cas de base : dp[0] = 0. Les autres valeurs sont initialisées à +∞.
Transition : dp[s] = min(dp[s], 1 + dp[s − piece]) pour chaque pièce utilisable.
ZONE DE PSEUDO-CODE — Rendu de monnaie optimal — tabulation Fonction MonnaieOptimale(Pieces, S) : Entier DP[0] ← 0 Pour s allant de 1 à S Faire DP[s] ← +INFINI FinPour
Pour s allant de 1 à S Faire Pour chaque piece dans Pieces Faire Si piece ≤ s ET DP[s - piece] ≠ +INFINI Alors DP[s] ← Minimum(DP[s], 1 + DP[s - piece]) FinSi FinPour FinPour
Si DP[S] = +INFINI Alors Retourner -1 Sinon Retourner DP[S] FinFonction |
Somme s | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| dp[s] pour pièces {1,3,4} | 0 | 1 | 2 | 1 | 1 | 2 | 2 |
Pour S = 6, la réponse optimale est 2, avec 3 + 3. Une stratégie gloutonne prenant 4 puis deux pièces de 1 utiliserait 3 pièces.
Reconstruction des pièces
ZONE DE PSEUDO-CODE — Mémoriser la dernière pièce utilisée Lorsqu’une transition améliore DP[s] : DP[s] ← 1 + DP[s - piece] DernierePiece[s] ← piece
s ← S TantQue s > 0 Faire Afficher DernierePiece[s] s ← s - DernierePiece[s] FinTantQue |
Méthode générale de conception
Une solution dynamique fiable peut être conçue selon une démarche en sept étapes.
1. Définir précisément le problème, les entrées, les contraintes et la valeur recherchée.
2. Identifier les sous-problèmes et choisir un état minimal mais suffisant.
3. Écrire la relation de transition entre un état et des états plus simples.
4. Définir tous les cas de base et les valeurs impossibles.
5. Choisir mémoïsation ou tabulation, puis déterminer l’ordre de calcul.
6. Analyser le nombre d’états, le coût des transitions et la mémoire.
7. Prévoir la reconstruction de la solution si le résultat attendu ne se limite pas à sa valeur.
Étape | Question à poser | Exemple : monnaie optimale |
|---|---|---|
| État | Que représente dp[...] ? | dp[s] = nombre minimal de pièces pour s |
| Transition | Quels choix permettent d’arriver à cet état ? | Tester chaque pièce p ≤ s |
| Base | Quels résultats sont connus immédiatement ? | dp[0] = 0 |
| Impossible | Comment représenter une absence de solution ? | +∞ |
| Ordre | Les dépendances sont-elles déjà calculées ? | s croissant |
| Reconstruction | Quel choix a produit le meilleur résultat ? | DernierePiece[s] |
Preuve de correction
La correction d’un algorithme dynamique repose généralement sur une induction selon l’ordre de calcul :
• Cas de base : les plus petits états possèdent la bonne valeur.
• Hypothèse : les états dont dépend l’état courant sont corrects.
• Étape : la transition examine tous les choix admissibles et sélectionne la combinaison correcte ou optimale.
• Conclusion : tous les états, notamment l’état final, sont corrects.
Erreurs fréquentes
Erreur | Conséquence | Prévention |
|---|---|---|
| État incomplet | Deux situations différentes sont confondues. | Ajouter le paramètre nécessaire à la décision future. |
| État trop riche | Table énorme et calcul inutile. | Supprimer les informations déductibles. |
| Cas de base manquant | Accès hors limites ou valeur incorrecte. | Lister les plus petits sous-problèmes avant le code. |
| Ordre invalide | Utilisation d’une valeur non calculée. | Dessiner les dépendances. |
| Sentinelle ambiguë | Un résultat valide est pris pour « non calculé ». | Séparer présence et valeur. |
| Boucle de capacité dans le mauvais sens | Un objet 0/1 est réutilisé plusieurs fois. | Parcourir la capacité en ordre décroissant en version 1D. |
| Optimisation mémoire trop précoce | Reconstruction impossible. | Valider d’abord la version complète. |
Applications guidées
Domaine | État possible | Objectif |
|---|---|---|
| Planification | dp[t] | Coût ou gain optimal jusqu’au temps t |
| Traitement de texte | dp[i][j] | Distance entre deux préfixes |
| Réseaux | dp[k][i][j] | Chemin autorisant certains sommets intermédiaires |
| Bio-informatique | dp[i][j] | Alignement de deux séquences |
| Gestion de ressources | dp[i][c] | Choix d’éléments sous contrainte |
| Robotique / grilles | dp[i][j] | Nombre de chemins ou coût minimal |
Quand ne pas utiliser la programmation dynamique ? • Lorsque les sous-problèmes sont indépendants et ne se répètent pas : diviser pour régner peut suffire. • Lorsque le nombre d’états est trop grand pour être stocké ou parcouru. • Lorsque l’objectif peut être obtenu par une propriété gloutonne prouvée, avec une solution plus simple. • Lorsque la structure du problème permet une formule directe ou un algorithme spécialisé plus efficace. |
Travaux dirigés
Les exercices suivants sont conçus pour travailler la modélisation avant l’écriture du code. Pour chaque problème, préciser l’état, la transition, les cas de base, l’ordre de calcul et la complexité.
TD 1 — Reconnaissance des sous-problèmes
Pour Fibonacci(6), dessiner l’arbre des appels naïfs et identifier les appels répétés. Estimer le nombre d’états distincts.
TD 2 — Comparaison mémoïsation / tabulation
Présenter les avantages et limites des deux approches pour une grille contenant de nombreux obstacles et dont seule une petite partie est accessible.
TD 3 — Escalier
Une personne peut monter une ou deux marches. Déterminer le nombre de façons d’atteindre la marche n.
TD 4 — Chemins avec obstacles
Calculer le nombre de chemins dans une grille 4 × 5 contenant des cases interdites.
TD 5 — Sac à dos
Résoudre le sac à dos pour des objets (poids, valeur) : (2, 6), (2, 10), (3, 12), capacité 5.
TD 6 — Somme minimale
Calculer le coût minimal dans une grille 3 × 4 et reconstruire un chemin optimal.
TD 7 — Monnaie
Pour les pièces {1, 3, 4} et S = 6, comparer le choix glouton à la solution dynamique.
TD 8 — Optimisation mémoire
Transformer une table 2D de chemins en une solution utilisant une seule ligne.
TD 9 — Sous-ensemble de somme
Déterminer si un sous-ensemble de {3, 5, 7, 10} peut former la somme 15.
TD 10 — Conception libre
Proposer un état dynamique pour minimiser le coût d’une suite de décisions sur n étapes, avec deux choix possibles à chaque étape et un coût de changement.
Corrigés indicatifs des travaux dirigés
Correction du TD 1
F(6) produit notamment plusieurs occurrences de F(4), F(3), F(2), F(1) et F(0). Les états distincts sont seulement F(0) à F(6), soit 7 états. L’arbre naïf contient beaucoup plus de nœuds, ce qui justifie le cache.
Correction du TD 2
La mémoïsation est intéressante si les obstacles rendent une grande partie de la grille inaccessible : seuls les états visités sont calculés. La tabulation reste souvent plus simple et évite la pile, mais elle parcourt potentiellement toutes les cases.
Correction du TD 3 — Escalier
État : dp[i] est le nombre de façons d’atteindre la marche i. Transition : dp[i] = dp[i − 1] + dp[i − 2]. Bases : dp[0] = 1 et dp[1] = 1. Temps O(n), mémoire O(n), réductible à O(1).
ZONE DE PSEUDO-CODE — Nombre de façons de monter un escalier DP[0] ← 1 DP[1] ← 1 Pour i allant de 2 à n Faire DP[i] ← DP[i - 1] + DP[i - 2] FinPour Retourner DP[n] |
Correction du TD 4 — Grille avec obstacles
Initialiser la case de départ à 1 si elle est libre. Chaque obstacle reçoit 0. Pour une case libre, additionner le haut et la gauche. Le résultat dépend de la disposition exacte des obstacles. La complexité est O(nm).
Correction du TD 5 — Sac à dos
Objets : A(2,6), B(2,10), C(3,12), capacité 5. Les choix possibles montrent que B + C pèse 5 et vaut 22. A + C vaut 18 et A + B vaut 16. La valeur optimale est donc 22.
Choix | Poids | Valeur | Admissible ? |
|---|---|---|---|
| A | 2 | 6 | Oui |
| B | 2 | 10 | Oui |
| C | 3 | 12 | Oui |
| A+B | 4 | 16 | Oui |
| A+C | 5 | 18 | Oui |
| B+C | 5 | 22 | Oui |
| A+B+C | 7 | 28 | Non |
Correction du TD 6 — Somme minimale
Construire la première ligne et la première colonne par cumul, puis appliquer la transition coût + minimum(haut, gauche). Pour reconstruire, partir de la destination et remonter vers le voisin de plus faible coût cumulé jusqu’à la source.
Correction du TD 7 — Monnaie
Le glouton choisit 4, puis 1, puis 1 : trois pièces. La programmation dynamique trouve 3 + 3 : deux pièces. Ce contre-exemple montre que le choix local du plus grand montant n’est pas toujours optimal.
Correction du TD 8 — Une seule ligne
ZONE DE PSEUDO-CODE — Chemins dans une grille avec une ligne Créer DP[0..m - 1] initialisé à 1 Pour i allant de 1 à n - 1 Faire Pour j allant de 1 à m - 1 Faire DP[j] ← DP[j] + DP[j - 1] FinPour FinPour Retourner DP[m - 1] DP[j] contient encore la valeur de la ligne précédente ; DP[j − 1] contient la valeur courante à gauche. |
Correction du TD 9 — Sous-ensemble de somme
État booléen dp[s] : une somme s est-elle réalisable ? Initialiser dp[0] = Vrai. Pour chaque valeur x, parcourir s en ordre décroissant et poser dp[s] = dp[s] OU dp[s − x]. Pour 15, la réponse est Vrai grâce à 5 + 10 ou 3 + 5 + 7.
ZONE DE PSEUDO-CODE — Sous-ensemble de somme — version 1D DP[0] ← Vrai Pour s allant de 1 à S Faire DP[s] ← Faux FinPour Pour chaque x dans Valeurs Faire Pour s allant de S à x avec un pas de -1 Faire DP[s] ← DP[s] OU DP[s - x] FinPour FinPour Retourner DP[S] |
Correction du TD 10 — Coût de changement
Un état possible est dp[i][c], coût minimal après les i premières étapes lorsque le choix courant est c ∈ {0,1}. La transition ajoute le coût de l’étape et éventuellement une pénalité si le choix diffère de celui de l’étape précédente. Il existe O(n) états et O(1) transitions par état.
Travail pratique — Laboratoire de programmation dynamique
Objectif
Implémenter et comparer les approches naïve, mémoïsée, tabulée et optimisée. Le TP doit mettre en évidence le lien entre théorie, mesures expérimentales et reconstruction des solutions.
Travail demandé
1. Implémenter Fibonacci dans les quatre versions et compter les appels ou itérations.
2. Implémenter le nombre de chemins dans une grille, avec puis sans obstacles.
3. Implémenter le sac à dos 0/1 et reconstruire la liste des objets choisis.
4. Implémenter le chemin de somme minimale et afficher un chemin optimal.
5. Implémenter le rendu de monnaie optimal et afficher les pièces utilisées.
6. Mesurer le temps, la mémoire approximative et le nombre d’états calculés.
7. Rédiger une conclusion expliquant les différences entre mémoïsation et tabulation.
Architecture suggérée
ZONE DE PSEUDO-CODE — Organisation modulaire du TP Module Fibonacci FibonacciNaif, FibonacciMemo, FibonacciTab, FibonacciOptimise
Module Grille CompterChemins, SommeMinimale, ReconstruireChemin
Module Optimisation SacADos, ReconstruireObjets, MonnaieOptimale, ReconstruirePieces
Module Mesures Chronometrer, CompterEtats, AfficherComparaison |
Jeux d’essai minimaux
Test | Entrée | Résultat attendu |
|---|---|---|
| Fibonacci | n = 10 | 55 |
| Chemins | grille 3 × 4 sans obstacle | 10 |
| Sac à dos | A(2,3), B(3,4), C(4,5), C = 5 | Valeur 7 : A + B |
| Somme minimale | [[1,3,1],[1,5,1],[4,2,1]] | 7 |
| Monnaie | pièces {1,3,4}, S = 6 | 2 pièces : 3 + 3 |
| Cas impossible | pièces {4,6}, S = 5 | −1 ou absence de solution |
Grille d’évaluation
Critère | Points |
|---|---|
| Correction des états, transitions et bases | 5 |
| Implémentation et modularité | 4 |
| Reconstruction des solutions | 3 |
| Jeux d’essai et cas limites | 3 |
| Mesures et comparaison expérimentale | 3 |
| Qualité de la conclusion | 2 |
Synthèse du chapitre
Concept | Idée essentielle |
|---|---|
| Sous-problèmes répétés | Les mêmes états apparaissent dans plusieurs branches. |
| Sous-structure optimale | Une solution optimale repose sur des solutions optimales plus petites. |
| Mémoïsation | Approche descendante avec cache. |
| Tabulation | Approche ascendante avec ordre explicite. |
| État | Description minimale d’un sous-problème. |
| Transition | Règle reliant un état aux états déjà résolus. |
| Cas de base | États connus sans calcul récursif. |
| Reconstruction | Mémorisation des décisions produisant la valeur finale. |
| Optimisation mémoire | Conserver uniquement les états encore nécessaires. |
À retenir La difficulté principale n’est pas l’écriture des boucles, mais la définition de l’état et de la transition. Une table correcte est la conséquence d’une modélisation correcte. |
Glossaire
Terme | Définition |
|---|---|
| Cache | Structure stockant les résultats déjà calculés. |
| État | Sous-problème identifié par un ensemble de paramètres. |
| Mémoïsation | Évaluation descendante avec mémorisation des résultats. |
| Tabulation | Évaluation ascendante d’une table de sous-problèmes. |
| Sous-problèmes recouvrants | Sous-problèmes identiques présents dans plusieurs branches. |
| Sous-structure optimale | Propriété permettant de construire l’optimum à partir d’optima locaux de sous-problèmes. |
| Transition | Formule ou règle de calcul d’un état. |
| Sentinelle | Valeur spéciale signalant un état non calculé ou impossible. |
| Pseudo-polynomial | Temps polynomial dans une valeur numérique telle que la capacité, mais pas nécessairement dans la taille binaire de l’entrée. |
Auto-évaluation
Pour chaque compétence, indiquer : acquis, en cours d’acquisition ou à revoir.
Je suis capable de… | Acquis | En cours | À revoir |
|---|---|---|---|
| repérer des sous-problèmes répétés ; | □ | □ | □ |
| définir un état et une transition ; | □ | □ | □ |
| écrire une solution mémoïsée ; | □ | □ | □ |
| choisir un ordre de tabulation valide ; | □ | □ | □ |
| analyser le nombre d’états et de transitions ; | □ | □ | □ |
| optimiser la mémoire sans perdre une information nécessaire ; | □ | □ | □ |
| reconstruire une solution optimale ; | □ | □ | □ |
| appliquer la méthode aux cinq exemples du chapitre. | □ | □ | □ |
Conclusion
La programmation dynamique fournit une méthode générale pour remplacer des recalculs coûteux par une organisation méthodique des sous-problèmes. La mémoïsation conserve la logique récursive et calcule à la demande ; la tabulation explicite l’ordre de calcul et facilite souvent l’optimisation mémoire. Dans les deux cas, la qualité de la solution dépend d’abord de la définition de l’état, de la transition et des cas de base.
Les chapitres suivants pourront approfondir cette méthode à travers des problèmes classiques plus complexes, la reconstruction d’optima et l’optimisation de l’espace mémoire.