☀️ Stage Pré-rentrée · dès le 24 aoûtRéserver ma place →
Majorant
📘 Fiche de cours · 1re année📐 MPSI💻 Informatique Informatique communeNiveau · Spé

Programmation dynamique

Le paradigme qui bat le glouton : mémoïser les sous-problèmes chevauchants pour ne jamais recalculer. Fibonacci mémoïsé (exponentiel → linéaire) et le rendu de monnaie optimal par tableau dp, qui trouve 2 pièces là où le glouton en donnait 3 — avec trois exercices corrigés.

Fiche rédigée par les mentors Majorant — alumni Polytechnique, CentraleSupélec et Mines Paris.

2 définitionsMis à jour le 2026-08-02

Vue d'ensemble

Tu as vu avec les algorithmes gloutons qu'on peut parfois construire une solution en faisant à chaque étape le choix qui paraît le meilleur sur le moment. Mais tu as aussi vu que ce raisonnement myope se trompe parfois. La programmation dynamique est l'artillerie lourde : au lieu de parier sur un seul choix, elle explore tous les sous-problèmes utiles, mais sans jamais recalculer deux fois le même. C'est cette mémorisation qui la rend efficace là où la récursion naïve explose.

L'idée tient en une phrase : décomposer un problème en sous-problèmes qui se chevauchent, résoudre chaque sous-problème une seule fois, et retenir sa solution pour la réutiliser. Deux façons de coder cela : garder la récursion naturelle et lui coller un cache (mémoïsation, dite top-down), ou remplir un tableau des petits cas vers les grands (itératif, dit bottom-up).

Au programme. Principe de la programmation dynamique : sous-problèmes chevauchants et sous-structure optimale. Deux mises en œuvre : mémoïsation (top-down, récursion + dictionnaire) et remplissage itératif d'un tableau (bottom-up). Exemples : suite de Fibonacci (passage de l'exponentiel au linéaire), rendu de monnaie optimal. Analyse de la complexité en temps.

Prérequis

  • Récursivité : cas de base, appels récursifs, arbre des appels — indispensable pour comprendre pourquoi le Fibonacci naïf explose.
  • Complexité temporelle : distinguer un coût exponentiel d'un coût linéaire .
  • Manipulation des listes et tableaux Python (indexation, initialisation d'un tableau de taille fixe).
  • Algorithmes gloutons : pour comparer l'approche myope et l'approche exhaustive optimale.
🎯 Accompagnement Majorant

La prog dynamique bloque beaucoup d'élèves au concours. Reconnaître un problème « DP » et écrire la bonne récurrence, ça s'apprend par la pratique guidée avec nos mentors alumni X · Centrale · Mines.

Trouver un mentor →

1. Deux définitions à connaître

Définition 1.1 — Programmation dynamique

La programmation dynamique est une méthode de résolution qui s'applique quand un problème possède deux propriétés :

  • Sous-structure optimale : une solution optimale du problème se construit à partir de solutions optimales de sous-problèmes plus petits.
  • Sous-problèmes chevauchants : les mêmes sous-problèmes reviennent de nombreuses fois quand on développe la récursion.

On résout alors chaque sous-problème une seule fois et on mémorise son résultat.

Définition 1.2 — Mémoïsation

La mémoïsation est la technique consistant à conserver dans une structure (typiquement un dictionnaire) le résultat de chaque appel d'une fonction, indexé par ses arguments. Avant tout calcul, on regarde si le résultat est déjà en cache : si oui on le renvoie directement, sinon on le calcule puis on le range. C'est la mise en œuvre top-down de la programmation dynamique : on garde la récursion naturelle et on lui ajoute une mémoire.

📝 Remarque. Le mot « programmation » ici n'a rien à voir avec le fait d'écrire du code : c'est un terme historique (années 1950, R. Bellman) qui signifie « planification par tableaux ». « Dynamique » évoque le remplissage progressif de ces tableaux.

2. Exemple canonique — Fibonacci

La suite de Fibonacci est définie par , et pour . C'est le terrain d'entraînement idéal : la récurrence est immédiate, mais la traduire naïvement en récursion est un désastre.

2.1 La version récursive naïve est exponentielle

def fib_naif(n):
    if n <= 1:          # cas de base : F0 = 0 et F1 = 1
        return n
    return fib_naif(n - 1) + fib_naif(n - 2)
🔍 Décryptage ligne par ligne
def fib_naif(n):on définit la fonction qui doit renvoyer le -ième terme de Fibonacci.
if n <= 1: return ncas de base : pour on renvoie 0, pour on renvoie 1 (dans les deux cas c'est exactement n). Sans lui, la récursion ne s'arrête jamais.
return fib_naif(n-1) + fib_naif(n-2)on applique littéralement la définition. Le problème : chaque appel en déclenche deux autres, et rien ne mémorise ce qui a déjà été calculé.

Regarde ce qui se passe pour calculer : l'arbre des appels recalcule deux fois, trois fois, cinq fois… Le nombre d'appels double presque à chaque niveau : la complexité est exponentielle, en avec (grosso modo ). Dès , la machine rame.

Nombre d'appels à fib_naif pour calculer chaque lors du calcul de — les mêmes sous-problèmes reviennent (chevauchement).
Sous-problème
Nombre de fois calculé112353

Total : 15 appels pour , et cela grossit de façon explosive. Tout ce travail est gâché car on recalcule sans cesse les mêmes valeurs : c'est exactement la situation où la mémoïsation change tout.

2.2 Version mémoïsée (top-down) : on passe au linéaire

def fib_memo(n, cache=None):
    if cache is None:       # au premier appel, on cree le dictionnaire vide
        cache = {}
    if n <= 1:              # cas de base inchange
        return n
    if n in cache:          # deja calcule ? on renvoie sans refaire le travail
        return cache[n]
    cache[n] = fib_memo(n - 1, cache) + fib_memo(n - 2, cache)
    return cache[n]
🔍 Décryptage ligne par ligne
def fib_memo(n, cache=None):même fonction, avec un second paramètre cache qui transporte la mémoire partagée entre tous les appels.
if cache is None: cache = {}astuce : on n'écrit jamais cache={} par défaut (piège des arguments mutables). On crée le dictionnaire au tout premier appel seulement.
if n <= 1: return ncas de base : les valeurs 0 et 1 ne se stockent pas, elles se renvoient directement.
if n in cache: return cache[n]le cœur de la mémoïsation : si a déjà été calculé, on le récupère instantanément au lieu de relancer toute la récursion.
cache[n] = fib_memo(n-1, cache) + fib_memo(n-2, cache)sinon on calcule une seule fois, puis on range le résultat dans le cache.
return cache[n]on renvoie la valeur fraîchement mémorisée.

Maintenant chaque n'est calculé qu'une seule fois : on remplit le cache de jusqu'à . Il y a valeurs à calculer, chacune en temps constant : la complexité tombe à en temps (et en mémoire pour le cache).

Remplissage du cache lors du calcul de fib_memo(6) (les cas de base 0 et 1 ne sont pas stockés).
Étape (ordre de rangement)Clé Calculcache[n]
121
232
343
455
568 ✓

2.3 Version itérative (bottom-up)

On peut se passer de récursion : on remplit un tableau dp des petits indices vers les grands. C'est souvent plus rapide (pas de pile d'appels) et tout aussi simple ici.

def fib_iter(n):
    if n <= 1:
        return n
    dp = [0] * (n + 1)      # dp[i] contiendra F_i
    dp[1] = 1               # on initialise les cas de base
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]
🔍 Décryptage ligne par ligne
if n <= 1: return non traite d'abord les cas de base pour éviter d'indexer dp[1] quand n=0.
dp = [0]*(n+1)on crée un tableau de cases (indices 0 à ), toutes à 0. La case dp[0] vaut déjà .
dp[1] = 1second cas de base : .
for i in range(2, n+1):on avance des petits sous-problèmes vers les grands : quand on calcule dp[i], dp[i-1] et dp[i-2] sont déjà remplis.
dp[i] = dp[i-1] + dp[i-2]la récurrence, mais en lecture dans le tableau : aucun recalcul, chaque case remplie une fois.
return dp[n]la dernière case contient la réponse.
📝 Top-down vs bottom-up. Les deux ont la même complexité . Le top-down (mémoïsation) ne calcule que les sous-problèmes réellement utiles et garde la formulation récursive ; le bottom-up évite la pile d'appels récursifs et se prête bien à l'optimisation mémoire (ici on pourrait même ne garder que les deux dernières valeurs).

3. Exemple fort — le rendu de monnaie optimal

Rendre une somme avec un minimum de pièces, en piochant dans un système . L'algorithme glouton (prendre la plus grosse pièce possible) marche sur le système euro, mais échoue sur certains systèmes. La programmation dynamique, elle, donne toujours l'optimum.

⚠ Le glouton se trompe sur [1, 3, 4] pour rendre 6. Il prend 4, puis il reste 2 qu'il ne peut faire qu'avec 1+1 : total 3 pièces (4+1+1). Alors qu'avec 3+3 on ne met que 2 pièces ! Le choix myope « la plus grosse pièce » a raté l'optimum.

3.1 La récurrence

Notons le nombre minimal de pièces pour rendre exactement la somme . Sous-structure optimale : pour rendre , la dernière pièce posée est une pièce , et il reste alors à rendre de façon optimale. On teste donc toutes les dernières pièces possibles et on garde la meilleure :

Le compte la pièce qu'on vient de poser. Le dit qu'il faut 0 pièce pour rendre 0. Les sous-problèmes se chevauchent massivement d'une somme à l'autre : c'est bien de la programmation dynamique.

3.2 Le code (bottom-up)

def rendu_optimal(pieces, s):
    INF = float('inf')
    dp = [0] + [INF] * s           # dp[0]=0, le reste = infini (pas encore atteint)
    for m in range(1, s + 1):      # on remplit les sommes de 1 a s
        for p in pieces:           # on essaie chaque piece comme derniere posee
            if p <= m and dp[m - p] + 1 < dp[m]:
                dp[m] = dp[m - p] + 1
    return dp[s]
🔍 Décryptage ligne par ligne
INF = float('inf')on représente « somme pas encore réalisable » par l'infini : ainsi tout vrai nombre de pièces sera plus petit.
dp = [0] + [INF]*stableau de cases : dp[0]=0 (rendre 0 coûte 0 pièce), toutes les autres à l'infini au départ.
for m in range(1, s+1):on remplit dans l'ordre croissant : quand on traite la somme m, toutes les sommes m-p plus petites sont déjà finales.
for p in pieces:on envisage chaque pièce p comme étant la dernière posée pour atteindre m.
if p <= m and dp[m-p]+1 < dp[m]:la pièce doit tenir dans la somme (p<=m), et on ne garde ce choix que s'il améliore le minimum courant. C'est le de la récurrence, calculé au fil de l'eau.
dp[m] = dp[m-p] + 1on met à jour : une pièce p de plus que la solution optimale de m-p.
return dp[s]la case dp[s] contient le nombre minimal de pièces. (Elle vaut l'infini si s est irréalisable avec ce système.)
Remplissage du tableau dp pour le système et . On lit la meilleure « dernière pièce » posée à chaque somme.
Somme 0123456
Meilleure dernière pièce11344 ou 13
dp[m] (nb min de pièces)0121122 ✓

Lecture de la case : la meilleure dernière pièce est 3, donc . La programmation dynamique rend 6 avec 2 pièces (3+3), là où le glouton en utilisait 3. Détail des cases clés : (une pièce de 3), (une pièce de 4), (1+1), (4+1).

3.3 Complexité

Deux boucles imbriquées : la somme va de 1 à ( itérations), et pour chacune on parcourt les pièces. Le coût est donc en temps, où est la somme à rendre et le nombre de pièces. La mémoire est pour le tableau dp. C'est bien plus fiable qu'un glouton : on paie un peu plus cher, mais on obtient toujours l'optimum.

🎯 Accompagnement Majorant

Glouton ou prog dynamique ? Savoir trancher au concours entre les deux, c'est un réflexe qui se travaille sur des dizaines d'exemples avec nos mentors alumni X · Centrale · Mines.

Trouver un mentor →

4. Méthode — reconnaître un problème de programmation dynamique

📐 Les 4 réflexes.
  1. Repérer les deux ingrédients. Le problème a-t-il une sous-structure optimale (l'optimum global se compose d'optimums de sous-problèmes) et des sous-problèmes chevauchants (les mêmes reviennent) ? Si oui : DP.
  2. Définir précisément l'état. Écris ce que représente dp[...] en une phrase (« nombre minimal de pièces pour rendre », « »). Un état mal défini = récurrence fausse.
  3. Écrire la récurrence + les cas de base. Exprime dp[état] en fonction d'états plus petits, et n'oublie pas les cas de base (dp[0], dp[1]…).
  4. Choisir top-down ou bottom-up. Récursion + cache si la formulation récursive est naturelle ; tableau itératif si l'ordre de remplissage est évident (des petits vers les grands).
💡 Exemple de détection. « Combien de chemins pour aller du coin haut-gauche au coin bas-droite d'une grille en n'allant que vers la droite ou le bas ? » Sous-structure : un chemin vers vient de ou . Chevauchement : la case sert à et . Récurrence : . C'est un problème de programmation dynamique typique.

5. Exercices corrigés

Exo 1Trace du cache de FibonacciFacile

On appelle fib_memo(7) (fonction de la section 2.2). Donne, dans l'ordre où elles sont rangées, les paires (clé, valeur) ajoutées au dictionnaire cache, et la valeur finale renvoyée.

Voir la correction détaillée
Les cas de base 0 et 1 ne sont pas stockés. La récursion descend jusqu'à , qui est rangé en premier, puis on remonte.
Rangements successifs : , , , , , .
On a . La fonction renvoie donc 13.
Exo 2Rendu de monnaie sur un autre systèmeIntermédiaire

On utilise rendu_optimal (section 3.2) avec le système pour rendre . Construis le tableau dp[0..8], donne le résultat, et compare-le à ce que trouverait le glouton.

Voir la correction détaillée
dp[0]=0. dp[1]=1 (une pièce 1). dp[2]=2 (1+1). dp[3]=3 (1+1+1). dp[4]=1 (pièce 4).
dp[5]=2 (4+1). dp[6]=1 (pièce 6). dp[7]=2 (6+1). dp[8] : via 4 → ; via 6 → ; via 1 → . Le min est 2.
Tableau complet : . Donc rendu_optimal([1,4,6],8)=2 (soit 4+4).
Le glouton prendrait 6, puis 1+1 : total 3 pièces. Encore une fois la prog dynamique bat le glouton.
Exo 3Nombre de façons de monter un escalierDifficile

Un escalier a marches. À chaque pas on monte de 1 ou 2 marches. On veut le nombre de façons différentes d'atteindre la marche . Écris la récurrence, code une version bottom-up en , et calcule le résultat pour .

Voir la correction détaillée
État : dp[i] = nombre de façons d'atteindre la marche . Pour arriver en , on vient de (dernier pas de 1) ou de (dernier pas de 2). Récurrence : .
Cas de base : dp[0]=1 (une seule façon de « ne pas bouger »), dp[1]=1. On reconnaît Fibonacci décalé.
Code :
def escalier(n):
    dp = [0] * (n + 1)
    dp[0] = 1
    if n >= 1:
        dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]
Trace pour : . Il y a donc 8 façons de monter 5 marches.

Récap final — Ce qu'il faut absolument retenir

La programmation dynamique = décomposer en sous-problèmes chevauchants, les résoudre une fois, mémoriser. Deux mises en œuvre équivalentes en complexité : mémoïsation top-down et tableau bottom-up.

  • Sais-tu énoncer les deux propriétés qui rendent la programmation dynamique applicable (sous-structure optimale + sous-problèmes chevauchants) ?
  • Sais-tu expliquer pourquoi fib_naif est exponentiel et pourquoi la mémoïsation le ramène à ?
  • Sais-tu écrire une fonction mémoïsée avec un cache (dictionnaire) et le test « déjà calculé ? » ?
  • Sais-tu la différence entre top-down (récursion + cache) et bottom-up (tableau rempli des petits vers les grands) ?
  • Sais-tu écrire la récurrence du rendu de monnaie avec ses cas de base ?
  • Sais-tu montrer, sur pour 6, que la prog dynamique (2 pièces) bat le glouton (3 pièces) ?
  • Sais-tu donner la complexité du rendu de monnaie DP : pour la somme et pièces ?
  • Sais-tu, face à un nouveau problème, définir l'état dp[...] et écrire sa récurrence ?

Débloque la fiche complète

Théorèmes, démonstrations à savoir refaire, méthodes-types et pièges de concours : crée ton compte gratuit pour tout lire. Une seule fois pour toutes les fiches et ressources Majorant.

Gratuit · vos données restent confidentielles.

Valide tes acquis

Quiz — Programmation dynamique

11 questions · une à la fois · seuil de maîtrise 80 %.

Informatique commune · SpéQuiz — Programmation dynamiqueQuestion 1 / 11
FacileVrai / Faux1 pt

La version récursive naïve de Fibonacci (deux appels récursifs, sans cache) a une complexité linéaire en n.

Sélectionne une réponse pour valider.

Fiches associées

📐 MPSI·Informatique

Recherche par dichotomie

La recherche dichotomique dans un tableau trié, expliquée ligne par ligne : code Python commenté, exécution pas à pas, preuve de terminaison et de correction (variant \(d-g\), invariant de boucle), version récursive et complexité \(O(\log n)\).

📐 MPSI·Informatique

Variables, types et affectations

Les briques de base de Python pour bien démarrer la prépa : affectation, types int/float/bool/str, conversions, opérateurs (/ // %), f-strings — avec les pièges classiques et deux exercices corrigés.

📐 MPSI·Informatique

Conditions et booléens

Faire choisir un programme : booléens, comparaisons, if / elif / else, rôle de l'indentation, et combinaison de conditions avec and / or / not — avec pièges et exercices corrigés.

📐 MPSI·Informatique

Boucles for et while

Répéter une action : boucle for avec range, boucle while qui termine, et le motif de l'accumulateur (somme, compteur) — avec table de trace, pièges et exercices corrigés.

📐 MPSI·Informatique

Fonctions : paramètres et valeurs de retour

Définir et appeler une fonction, comprendre paramètres et valeur de retour, et surtout ne jamais confondre return (renvoie) et print (affiche) — avec pièges et exercices corrigés.

📐 MPSI·Informatique

Listes et chaînes de caractères

Créer, indexer, parcourir et découper des listes et des chaînes ; comprendre qu'une liste est modifiable et une chaîne non — avec les pièges d'indices et deux exercices corrigés.

Tu veux aller plus loin sur ce chapitre ?

Nos mentors alumni de Polytechnique, CentraleSupélec et Mines Paris t'accompagnent en cours particuliers — démonstrations détaillées, exos type concours, oraux blancs.

Trouver un mentor →