Bonjour,
Dans les modules précédents, nous avons exploré plusieurs algorithmes d'optimisation sur les graphes, comme Dijkstra, Prim ou Kruskal. Ces algorithmes construisent des solutions optimales (plus court chemin, arbre couvrant de poids minimum) en faisant des choix locaux. L'algorithme de Kruskal, par exemple, utilise une stratégie "gloutonne" en sélectionnant systématiquement l'arête la moins chère qui ne forme pas de cycle.
Aujourd'hui, nous abordons une nouvelle et puissante technique d'optimisation : la programmation dynamique. Cette approche est fondamentale en algorithmique et sa maîtrise est un prérequis indispensable pour l'agrégation. Notre objectif pour cette première leçon est conceptuel mais crucial : identifier la sous-structure optimale et le chevauchement des sous-problèmes dans un problème donné. Apprendre à reconnaître ces deux propriétés est la première étape, et la plus importante, pour déterminer si un problème peut être résolu efficacement par programmation dynamique.
1. Le symptôme : le chevauchement des sous-problèmes
Le signe le plus visible qu'une approche récursive naïve est inefficace — et que la programmation dynamique pourrait être la solution — est la répétition massive des mêmes calculs.
Considérons l'exemple le plus canonique : le calcul du n-ième terme de la suite de Fibonacci, définie par :
Une traduction directe en fonction récursive est simple, mais observons l'arbre des appels pour calculer F(6) :

Ce phénomène où une même sous-tâche (par exemple, le calcul de F(3)) est résolue à de multiples reprises est appelé le chevauchement des sous-problèmes (overlapping subproblems). C'est le premier des deux piliers de la programmation dynamique.
Pour approfondir cette idée avec des exemples concrets, je vous propose de lire les premières sections du document suivant, qui illustre ce concept non seulement avec Fibonacci mais aussi avec le calcul des coefficients du binôme.
Le document "programmation dynamique" du site Cahier de prépa formalise très bien ce concept. Il quantifie l'inefficacité de l'approche récursive naïve et introduit la terminologie correcte.
Lisez la partie I, "La technique de mémoïsation" (pages 2 à 9). Concentrez-vous sur les sections 1) "Le problème de la suite de Fibonacci" et 2) "La formule du binôme de Newton". Observez comment, dans les deux cas, l'analyse de l'arbre des appels récursifs met en évidence le "chevauchement des sous-problèmes". Notez également l'introduction de la mémoïsation comme une première solution, un concept que nous développerons dans la prochaine leçon.
2. Le principe fondamental : la sous-structure optimale
Le chevauchement des sous-problèmes nous indique qu'il y a une inefficacité à corriger. Mais la raison pour laquelle la programmation dynamique fonctionne repose sur une propriété plus profonde : la sous-structure optimale.
Un problème présente une sous-structure optimale si une solution optimale du problème global peut être construite à partir des solutions optimales de ses sous-problèmes.
Vous avez déjà intuitivement rencontré ce principe. Dans l'algorithme de Dijkstra, si le plus court chemin de A à C passe par B, alors le segment de chemin de A à B est nécessairement le plus court chemin de A à B. La solution optimale globale contient des solutions optimales locales.
La vidéo suivante introduit la programmation dynamique sous cet angle, en posant la question fondamentale de la structure d'une solution optimale.
Programmation dynamique: Plan du chapitre et intention pédagogique
Cette vidéo de la chaîne "Recherche Opérationnelle" propose une vision très élégante de la programmation dynamique, non pas comme une simple astuce de programmation, mais comme une méthode de modélisation.
Regardez la vidéo de 00:28 à 01:41. L'auteur fait le lien avec les plus courts chemins et énonce la question clé : "Est-ce qu'une solution optimale est constituée avec les solutions optimales de problèmes plus petits ?". C'est l'essence même de la recherche d'une sous-structure optimale.
Pour formaliser cela, étudions un autre problème classique : le rendu de monnaie.
- Problème : Étant donné un système de pièces (ex: {1, 5, 10, 25}) et une somme
Sà rendre, quel est le nombre minimum de pièces à utiliser ? - Sous-structure optimale : Supposons que nous ayons une solution optimale pour rendre la somme
S. Cette solution utilise une première pièce, disons de valeurc. Le reste des pièces utilisées doit alors obligatoirement constituer une solution optimale pour rendre la sommeS-c. Si ce n'était pas le cas, on pourrait remplacer cette partie de la solution par une meilleure, ce qui améliorerait notre solution globale pourS, contredisant son optimalité.
L'optimalité de la solution globale dépend directement de l'optimalité des solutions des sous-problèmes. Cette propriété nous permet de formuler une équation de récurrence. Si N(S) est le nombre minimal de pièces pour la somme S, alors :
Le document PDF que vous avez commencé à lire formalise cette déduction de manière très claire.
Le problème du rendu de monnaie
Retournons au document "programmation dynamique". La partie sur le problème du rendu de monnaie est un excellent exemple de la manière de décomposer un problème et de prouver qu'il possède une sous-structure optimale.
Lisez la section II.1 "Le problème du rendu de monnaie", en particulier la sous-section "d) Résolution par programmation dynamique" (pages 12-13). Portez une attention particulière à la dérivation de l'équation de récurrence (marquée (∗)). C'est l'étape de formalisation de la propriété de sous-structure optimale.
3. Synthèse : Diviser pour Régner vs. Programmation Dynamique
Un problème doit posséder à la fois une sous-structure optimale et des sous-problèmes qui se chevauchent pour que la programmation dynamique soit la bonne approche.
Cela la distingue d'une autre technique que vous connaissez bien : diviser pour régner (utilisée par exemple dans le tri fusion).
- Diviser pour régner : Les sous-problèmes sont indépendants. Pour trier un tableau, on trie la moitié gauche et la moitié droite séparément. Le travail n'est jamais dupliqué.
- Programmation dynamique : Les sous-problèmes se chevauchent. Le calcul de
F(n)etF(n-1)requièrent tous les deux le calcul deF(n-2).
L'image suivante illustre cette différence fondamentale.

La ressource suivante, issue d'un cours du MIT, résume parfaitement ces deux "marques de fabrique" de la programmation dynamique.
Hallmarks of Dynamic Programming
Ce support de cours du MIT, "Lecture 18 Dynamic Programming I", offre une définition concise et rigoureuse des concepts que nous venons d'explorer.
Dans ce document, lisez la diapositive intitulée "Hallmarks of Dynamic Programming". Elle définit clairement la sous-structure optimale et le chevauchement des sous-problèmes. Lisez ensuite la diapositive "Dynamic Programming" un peu plus loin, qui réitère ces deux points de manière encore plus formelle. C'est un excellent résumé.
4. Exercice d'identification
Mettons en pratique votre nouvelle compétence de diagnostic. Considérez le problème de la plus longue sous-séquence commune (LCS - Longest Common Subsequence), que nous étudierons en détail plus tard.
- Définition : Une sous-séquence est obtenue en supprimant zéro, un ou plusieurs caractères d'une chaîne. Par exemple, "ACE" est une sous-séquence de "ABCDE".
- Problème : Étant donné deux chaînes
XetY, trouver la longueur de la plus longue sous-séquence qui soit commune aux deux.
Exemple : Pour X = "AGGTAB" et Y = "GXTXAYB", la LCS est "GTAB" de longueur 4.
Votre tâche est de répondre aux deux questions suivantes (sans écrire de code) :
-
Sous-structure optimale : Comment la solution pour
XetYdépend-elle des solutions pour des versions plus petites deXetY(c'est-à-dire leurs préfixes) ?
Indice : SoitX_ile préfixe deXde longueurietY_jcelui deYde longueurj. Considérez deux cas :X[i] == Y[j]etX[i] != Y[j]. Comment la longueur deLCS(X_i, Y_j)se déduit-elle deLCS(X_{i-1}, Y_j),LCS(X_i, Y_{j-1})ouLCS(X_{i-1}, Y_{j-1})? -
Chevauchement des sous-problèmes : Si vous écriviez une fonction récursive
longueur_lcs(i, j)qui calcule la longueur de la LCS pour les préfixesX_ietY_j, pourquoi des appels redondants se produiraient-ils ?
Prenez quelques minutes pour y réfléchir avant de lire la solution.
Solution de l'exercice
-
Sous-structure optimale : Le problème LCS possède bien une sous-structure optimale. Soit
L(i,j)la longueur de la LCS deX[1..i]etY[1..j].- Si les derniers caractères sont identiques (
X[i] == Y[j]), alors ce caractère fait partie de la LCS. La longueur de la LCS est donc 1 + la longueur de la LCS des chaînes sans ce dernier caractère.
- Si les derniers caractères sont différents (
X[i] != Y[j]), alors la LCS est la plus longue des deux options suivantes : la LCS deX[1..i-1]etY[1..j], ou la LCS deX[1..i]etY[1..j-1].
Dans tous les cas, la solution optimale pour(i,j)est exprimée en fonction des solutions optimales de sous-problèmes de taille inférieure.
- Si les derniers caractères sont identiques (
-
Chevauchement des sous-problèmes : Une fonction récursive naïve explorerait un arbre d'appels. On peut voir que le sous-problème
L(i-1, j-1)est nécessaire pour calculerL(i-1, j)etL(i, j-1). Donc, l'appel àL(i-1, j-1)serait effectué plusieurs fois, de manière redondante. Par exemple, le calcul deL(i,j)pourrait mener aux appelsL(i-1,j)etL(i,j-1), qui tous deux pourraient à leur tour appelerL(i-2, j-1), et ainsi de suite.
Conclusion
Aujourd'hui, nous avons posé les fondations théoriques de la programmation dynamique. Vous devriez maintenant être capable de définir et d'identifier ses deux caractéristiques essentielles :
- La sous-structure optimale : La solution optimale d'un problème se compose de solutions optimales de ses sous-problèmes. C'est ce qui nous permet d'écrire une relation de récurrence.
- Le chevauchement des sous-problèmes : Une résolution récursive naïve calcule de nombreuses fois les mêmes sous-problèmes, ce qui rend l'approche exponentielle. C'est cette redondance que la programmation dynamique vise à éliminer.
La reconnaissance de ces deux propriétés est la compétence la plus fondamentale. Avant même d'écrire une ligne de code, un bon algorithmicien doit être capable de diagnostiquer un problème et de dire : "Ceci est un problème de programmation dynamique".
Dans notre prochaine leçon, nous passerons à la pratique. Nous verrons comment transformer cette analyse en algorithmes concrets en comparant les deux approches principales : l'approche descendante (avec mémoïsation) et l'approche ascendante (par tabulation).