Leçon 15 sur 19

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é

Cours4 à 6 hComprendre les principes, les modèles d’état et les preuves.
Travaux dirigés4 hFormuler des récurrences, remplir des tables et analyser les coûts.
Travaux pratiques4 à 6 hImplémenter, tester et comparer les approches.
Évaluation1 à 2 hConception 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ïvePlusieurs foisAucunDescendant
MémoïsationÀ la demande, une seule foisCacheDescendant
TabulationUne seule fois, systématiquementTableAscendant

 

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)1Problème initial
F(4)1Sous-problème unique
F(3)2Résultat recalculé
F(2)3Résultat fortement répété
F(1)5Cas de base répété
F(0)3Cas 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

1055Quelques centainesInstantané
206 765Dizaines de milliersEncore acceptable
30832 040MillionsRalentissement visible
40102 334 155Centaines de millionsTrès lent
5012 586 269 025Dizaines de milliardsInexploitable 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

Fibonaccidp[i]Valeur de F(i)
Chemins dans une grilledp[i][j]Nombre de chemins jusqu’à la case (i, j)
Sac à dosdp[i][c]Meilleure valeur avec les i premiers objets et une capacité c
Somme minimaledp[i][j]Coût minimal pour atteindre la case (i, j)
Monnaie optimaledp[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 denseTableauFibonacci : n entre 0 et N
Deux indices bornésMatriceGrille ou sac à dos
États irréguliers ou clairsemésDictionnairePositions 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ésNombreux appels répétésAu plus n + 1 états distincts
TempsExponentielLinéaire
Mémoire de cacheAucuneO(n)
Pile d’appelsO(n)O(n)
LisibilitéTrès proche de la définitionToujours 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é

FibonacciO(n)O(1)O(n)
Grille n × mO(nm)O(1)O(nm)
Sac à dos n, capacité CO(nC)O(1)O(nC)
Monnaie, somme S, k piècesO(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]

2011
3112
4123
5235
6358

 

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 gauchelignes et colonnes croissantes
dp[i][c] dépend de la ligne i − 1objets croissants ; capacité selon la formulation
Mise à jour 1D du sac à dos 0/1capacité décroissante pour éviter de réutiliser un objet

 

15.3.4 Mémoïsation et tabulation : comparaison

Critère

Mémoïsation

Tabulation

SensDescendantAscendant
ContrôleRécursionBoucles
États calculésSeulement les états visitésSouvent tous les états de la table
PileOuiNon
Ordre des dépendancesImplicite dans les appelsDoit être choisi explicitement
Optimisation mémoirePossible mais moins directeSouvent naturelle
États clairsemésTrès adaptéePeut 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ïveO(2ⁿ)O(n) pileRecalcul massif
MémoïséeO(n)O(n) cache + pileÉtats calculés à la demande
TabuléeO(n)O(n)Ordre explicite
OptimiséeO(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 01111
Ligne 11234
Ligne 213610

 

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

A23
B34
C45

 

i \ capacité

0

1

2

3

4

5

0 objet000000
A003333
A, B003447
A, B, C003457

 

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 0131
Ligne 1151
Ligne 2421

 

Table DP

Col. 0

Col. 1

Col. 2

Ligne 0145
Ligne 1276
Ligne 2687

 

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

 

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

ÉtatQue représente dp[...] ?dp[s] = nombre minimal de pièces pour s
TransitionQuels choix permettent d’arriver à cet état ?Tester chaque pièce p ≤ s
BaseQuels résultats sont connus immédiatement ?dp[0] = 0
ImpossibleComment représenter une absence de solution ?+∞
OrdreLes dépendances sont-elles déjà calculées ?s croissant
ReconstructionQuel 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 incompletDeux situations différentes sont confondues.Ajouter le paramètre nécessaire à la décision future.
État trop richeTable énorme et calcul inutile.Supprimer les informations déductibles.
Cas de base manquantAccès hors limites ou valeur incorrecte.Lister les plus petits sous-problèmes avant le code.
Ordre invalideUtilisation 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 sensUn objet 0/1 est réutilisé plusieurs fois.Parcourir la capacité en ordre décroissant en version 1D.
Optimisation mémoire trop précoceReconstruction impossible.Valider d’abord la version complète.

 

Applications guidées

Domaine

État possible

Objectif

Planificationdp[t]Coût ou gain optimal jusqu’au temps t
Traitement de textedp[i][j]Distance entre deux préfixes
Réseauxdp[k][i][j]Chemin autorisant certains sommets intermédiaires
Bio-informatiquedp[i][j]Alignement de deux séquences
Gestion de ressourcesdp[i][c]Choix d’éléments sous contrainte
Robotique / grillesdp[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 ?

A26Oui
B210Oui
C312Oui
A+B416Oui
A+C518Oui
B+C522Oui
A+B+C728Non

 

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

Fibonaccin = 1055
Cheminsgrille 3 × 4 sans obstacle10
Sac à dosA(2,3), B(3,4), C(4,5), C = 5Valeur 7 : A + B
Somme minimale[[1,3,1],[1,5,1],[4,2,1]]7
Monnaiepièces {1,3,4}, S = 62 pièces : 3 + 3
Cas impossiblepièces {4,6}, S = 5−1 ou absence de solution

 

Grille d’évaluation

Critère

Points

Correction des états, transitions et bases5
Implémentation et modularité4
Reconstruction des solutions3
Jeux d’essai et cas limites3
Mesures et comparaison expérimentale3
Qualité de la conclusion2

 

Synthèse du chapitre

Concept

Idée essentielle

Sous-problèmes répétésLes mêmes états apparaissent dans plusieurs branches.
Sous-structure optimaleUne solution optimale repose sur des solutions optimales plus petites.
MémoïsationApproche descendante avec cache.
TabulationApproche ascendante avec ordre explicite.
ÉtatDescription minimale d’un sous-problème.
TransitionRègle reliant un état aux états déjà résolus.
Cas de baseÉtats connus sans calcul récursif.
ReconstructionMémorisation des décisions produisant la valeur finale.
Optimisation mémoireConserver 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

CacheStructure stockant les résultats déjà calculés.
ÉtatSous-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 recouvrantsSous-problèmes identiques présents dans plusieurs branches.
Sous-structure optimalePropriété permettant de construire l’optimum à partir d’optima locaux de sous-problèmes.
TransitionFormule ou règle de calcul d’un état.
SentinelleValeur spéciale signalant un état non calculé ou impossible.
Pseudo-polynomialTemps 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.