Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 60

Mémoïsation vs Tabulation : l'exemple simple

Recommended resources

Bonjour,

Dans notre leçon précédente, nous avons posé les fondations conceptuelles de la programmation dynamique. Nous avons établi que cette méthode s'applique aux problèmes possédant une sous-structure optimale et un chevauchement de sous-problèmes. L'exemple de la suite de Fibonacci nous a montré de manière frappante comment une approche récursive naïve mène à une explosion de calculs redondants.

Aujourd'hui, nous allons transformer cette analyse théorique en algorithmes efficaces. L'objectif est de maîtriser et de comparer les deux stratégies fondamentales pour implémenter une solution de programmation dynamique :

  1. L'approche descendante (top-down), plus connue sous le nom de mémoïsation.
  2. L'approche ascendante (bottom-up), souvent appelée tabulation.

La distinction entre ces deux techniques est fine mais fondamentale. Sa parfaite compréhension est un attendu classique à l'agrégation, tant pour l'écrit que pour l'oral de leçon.


1. Le principe général : stocker pour ne pas recalculer

Avant d'examiner chaque approche, rappelons l'idée maîtresse. Face à un grand nombre de sous-problèmes identiques, la solution est de ne les résoudre qu'une seule fois. Pour cela, on utilise une structure de données (typiquement un tableau ou une table de hachage) pour stocker les résultats intermédiaires. La différence entre mémoïsation et tabulation réside dans la manière de remplir et d'utiliser cette structure.

La vidéo suivante introduit de manière très claire ces deux stratégies et leur application au calcul de la suite de Fibonacci.

Introduction à la programmation dynamique et à la mémoïsation

Cette vidéo de la chaîne "Informatique Théorique" présente un panorama complet de la programmation dynamique sur l'exemple de Fibonacci. Elle vous servira de fil rouge pour cette leçon.

Regardez la vidéo du début jusqu'à 06:02. Portez attention aux points suivants : La visualisation de la redondance des calculs (00:25 - 01:28). La description de l'approche "descendante" qui "découvre et calcule à la demande" (01:28 - 03:00). La description de l'approche "ascendante" qui nécessite de connaître la structure des dépendances (03:00 - 04:18). L'application concrète à la suite de Fibonacci et la visualisation de son graphe de dépendances (04:18 - 06:02).

Maintenant que vous avez une vue d'ensemble, nous allons disséquer et formaliser chaque approche.



2. L'approche descendante : la Mémoïsation

La mémoïsation est souvent la transition la plus naturelle depuis une fonction récursive. La logique est la suivante :

  1. On conserve la structure d'appels récursifs de la solution naïve (d'où le terme top-down, on part du problème F(n) pour descendre vers les cas de base).
  2. On ajoute un "cache" (mémoire), souvent un dictionnaire ou un tableau, pour stocker les résultats.
  3. Au début de la fonction, on vérifie si le résultat pour l'entrée courante est déjà dans le cache.
    • Si oui, on le retourne directement, coupant court à toute branche récursive inutile.
    • Si non, on effectue le calcul récursif normalement.
  4. Juste avant de retourner le résultat nouvellement calculé, on le stocke dans le cache pour un usage futur.

C'est une stratégie de "paresseux intelligent" : on ne calcule une valeur que si on en a besoin, et la première fois seulement.

Reprenons l'exemple du calcul de fib(n) :

  • Sans mémoïsation, l'appel à fib(5) génère des appels redondants à fib(3), fib(2), etc.
  • Avec mémoïsation, lorsque fib(4) est appelé la première fois (via fib(5)), il calcule et stocke fib(3) et fib(2). Plus tard, lorsque fib(5) appelle directement fib(3), le résultat est trouvé instantanément dans le cache.

Le gain en complexité est spectaculaire : on passe d'une complexité exponentielle à une complexité linéaire , car chaque valeur de fib(k) pour k de 0 à n n'est calculée qu'une seule fois.

La vidéo suivante vous montre le code Python correspondant à cette approche, ainsi que la mesure de sa performance.

Introduction à la programmation dynamique et à la mémoïsation

Retournons à la vidéo "Informatique Théorique". La partie suivante montre l'implémentation concrète de ces idées en Python.

Regardez la section de 06:02 à 07:34. Comparez le code de la version naïve (fibo_naif) à celui de la version descendante (fibo_descendant). Observez l'utilisation du dictionnaire deja_calcule et le test if n in deja_calcule qui est au cœur de la mémoïsation. Notez l'amélioration drastique des temps de calcul.

Pour une référence textuelle, le document ci-dessous présente une implémentation quasi-identique.

Programmation dynamique

Le document "Programmation Dynamique" de P. Connault formalise également cette approche avec des extraits de code clairs.

Consultez la section 6.4 "Récursivité et mémoïsation" (page 13). Le Listing 4 montre une implémentation avec une fonction auxiliaire, une structure très commune pour gérer le cache.



3. L'approche ascendante : la Tabulation

La tabulation adopte une philosophie différente. Au lieu de partir du haut et de descendre, elle part du bas et construit la solution étape par étape.

  1. On identifie l'ordre dans lequel les sous-problèmes doivent être résolus (des plus petits aux plus grands).
  2. On alloue une table (généralement un tableau) pour stocker les résultats des sous-problèmes.
  3. On initialise la table avec les cas de base.
  4. On parcourt la table de manière itérative, en remplissant chaque entrée à partir des valeurs déjà calculées (celles des sous-problèmes plus petits).

Pour Fibonacci, cela revient à faire ce que l'on ferait à la main :

  • On crée un tableau fib de taille n+1.
  • On initialise fib[0] = 0 et fib[1] = 1.
  • On boucle de i = 2 à n, et pour chaque i, on calcule fib[i] = fib[i-1] + fib[i-2].

Cette approche est itérative et garantit que lorsque nous devons calculer fib[i], les valeurs fib[i-1] et fib[i-2] dont il dépend sont déjà disponibles dans notre table.

Reprenez la vidéo et le document pour voir les implémentations correspondantes.

Introduction à la programmation dynamique et à la mémoïsation

La même section de la vidéo "Informatique Théorique" que précédemment présente aussi l'approche ascendante.

Si besoin, revisionnez la section de 06:02 à 07:34. Cette fois, concentrez-vous sur la fonction fibo_ascendant. Observez comment elle remplit un tableau table de manière itérative, sans aucun appel récursif.

Programmation dynamique

Le document de P. Connault présente également cette approche de manière très directe.

Lisez la section 6.3 "Programmation dynamique" (page 12). Le Listing 3 est une implémentation canonique de la tabulation pour Fibonacci.



4. Synthèse et Comparaison

Maintenant que vous comprenez les deux approches, il est crucial de savoir les comparer. C'est une question d'analyse algorithmique fine.

Comparaison Visuelle : Tabulation vs. Arbre de Récursion (Mémoïsation)
Cette image illustre la différence fondamentale : à gauche, la tabulation remplit méthodiquement une table (bottom-up). À droite, la mémoïsation explore un arbre de récursion, mais élague les branches déjà visitées (top-down).

Le document suivant, plus théorique, offre une perspective mature et synthétique, particulièrement adaptée à votre niveau de préparation.

Algorithmique avancée – Bloc 5 du DIU

Le cours "Algorithmique avancée" de Bruno Grenet contient une section remarquable qui met en perspective la programmation dynamique et la mémoïsation.

Lisez la section 2.1.2 "La programmation dynamique, qu'es aquó?" (pages 22-23). Concentrez-vous sur le paragraphe "Comparaison avec d’autres techniques". Notez comment l'auteur positionne la mémoïsation comme un "cousin proche" de la programmation dynamique (qu'il associe à l'approche ascendante/itérative), en soulignant les différences en termes d'approche (ascendante vs. descendante) et d'efficacité pratique.

Résumons les points clés de la comparaison dans un tableau :

Caractéristique Mémoïsation (Top-Down) Tabulation (Bottom-Up)
Philosophie Approche descendante. "Résoudre à la demande". Approche ascendante. "Construire à partir de la base".
Implémentation Récursive. Itérative.
Sous-problèmes Ne résout que les sous-problèmes réellement rencontrés lors de la descente récursive. Résout tous les sous-problèmes, du plus petit jusqu'à la solution finale.
Contrôle du flux Géré par la pile d'appels récursifs. Géré par des boucles explicites.
Avantages - Souvent plus simple à écrire si l'on dispose déjà de la formule récursive.
- Potentiellement plus rapide si certains sous-problèmes ne sont jamais atteints.
- Pas de risque de dépassement de la pile d'appels (stack overflow).
- Pas de surcoût lié aux appels de fonction, donc souvent un peu plus rapide en pratique (meilleures constantes).
Inconvénients - Risque de stack overflow pour des profondeurs de récursion très grandes.
- Surcoût des appels récursifs.
- Peut calculer des sous-problèmes inutiles.
- Demande de déterminer explicitement l'ordre de calcul des sous-problèmes.

Terminologie pour l'agrégation : Il est important d'être précis. Souvent, le terme "programmation dynamique" est utilisé pour désigner l'approche ascendante (tabulation) par opposition à la mémoïsation. Cependant, on peut aussi considérer la programmation dynamique comme le paradigme général qui englobe ces deux techniques d'implémentation. Soyez prêt à clarifier le sens que vous donnez aux termes.



Conclusion

Vous avez maintenant les outils pour implémenter une solution de programmation dynamique de deux manières distinctes et pour analyser leurs compromis.

Points clés à retenir :

  • La mémoïsation est une technique top-down qui optimise une fonction récursive en ajoutant un cache. Elle est intuitive mais limitée par la profondeur de la pile d'appels.
  • La tabulation est une technique bottom-up qui résout les sous-problèmes de manière itérative dans un ordre prédéfini. Elle est souvent plus performante en pratique et n'a pas de limite de récursion.
  • Le choix entre les deux dépend du problème : si tous les sous-problèmes doivent être résolus, la tabulation est souvent préférable. Si l'espace des sous-problèmes est vaste et que seule une petite partie est nécessaire, la mémoïsation peut être plus efficace.

Dans notre prochaine leçon, nous quitterons l'exemple unidimensionnel de Fibonacci pour aborder un problème classique en deux dimensions : la plus longue sous-séquence commune (LCS). Vous devrez alors formuler la relation de récurrence et implémenter la solution par tabulation, ce qui mettra en jeu une table à deux dimensions.