Objectifs de cette leçon
- Décomposer un problème en sous-problèmes
- Implémenter la mémoïsation pour optimiser les calculs
- Résoudre le problème du sac à dos avec la programmation dynamique
Programmation dynamique ⚡
La programmation dynamique (DP) est une technique qui consiste à décomposer un problème en sous-problèmes et à mémoriser leurs solutions pour éviter de les recalculer. C'est l'une des techniques les plus puissantes en algorithmique.
1. Le problème fondamental : Fibonacci 🌀
Exécution pas à pas
pseudo1/5🧩FONCTION fib_naif(n)FONCTION fib_naif(n)Le programme exécute cette action, puis passe à la suivante.On définit une fonction appelée "fib_naif(n)". Cette fonction contient des instructions qui seront exécutées quand on l'appelle.
Le problème : fib(5) appelle fib(4) et fib(3), fib(4) appelle fib(3) et fib(2) — on calcule fib(3) deux fois, fib(2) trois fois !
💡 La complexité explose : fib(50) nécessite ~2⁵⁰ = 1 quadrillion d'opérations !
2. Approche Top-Down (Mémoïsation) 📓
Idée : On garde un cache (dictionnaire). Avant de calculer, on vérifie si la valeur n'est pas déjà connue.
Exécution pas à pas
pseudo1/12🧩FONCTION fib_memo(n, memo ← {})FONCTION fib_memo(n, memo ← {})Le programme exécute cette action, puis passe à la suivante.On définit une fonction appelée "fib_memo(n, memo ← {})". Cette fonction contient des instructions qui seront exécutées quand on l'appelle.
# Naïf : O(2^n) — chaque appel crée deux branches def fib_naive(n): if n <= 1: return n return fib_naive(n-1) + fib_naive(n-2) # Mémoïsation : O(n) — chaque valeur calculée une seule fois def fib_memo(n, memo={}): if n in memo: return memo[n] if n <= 1: return n memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo) return memo[n]
Résultat : fib(50) passe de siècles à microsecondes ! Le cache transforme l'exponentiel en linéaire. [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765…]
3. Approche Bottom-Up (Tabulation) 📊
Idée : On remplit un tableau de bas en haut, du plus petit sous-problème au plus grand.
Exécution pas à pas
pseudo1/9🧩FONCTION fib_tab(n)FONCTION fib_tab(n)Le programme exécute cette action, puis passe à la suivante.On définit une fonction appelée "fib_tab(n)". Cette fonction contient des instructions qui seront exécutées quand on l'appelle.
def fib_tab(n): dp = [0, 1] for i in range(2, n+1): dp.append(dp[i-1] + dp[i-2]) # Chaque nouveau = somme des deux précédents return dp[n]
Visualisation du tableau dp pour n=7 :
i 0 1 2 3 4 5 6 7 dp[i] 0 1 1 2 3 5 8 13
4. Problème classique : Sac à dos (Knapsack) 🎒
Énoncé : Tu as des objets avec un poids et une valeur. Ton sac a une capacité limitée. Quels objets emporter pour maximiser la valeur totale ?
Exécution pas à pas
pseudo1/19🧩FONCTION knapsack(poids, valeurs, capacité)FONCTION knapsack(poids, valeurs, capacité)Le programme exécute cette action, puis passe à la suivante.On définit une fonction appelée "knapsack(poids, valeurs, capacité)". Cette fonction contient des instructions qui seront exécutées quand on l'appelle.
def knapsack(poids, valeurs, capacite): n = len(poids) dp = [[0] * (capacite + 1) for _ in range(n + 1)] for i in range(1, n + 1): for w in range(1, capacite + 1): if poids[i - 1] <= w: dp[i][w] = max( valeurs[i - 1] + dp[i - 1][w - poids[i - 1]], # Prendre dp[i - 1][w] # Pas prendre ) else: dp[i][w] = dp[i - 1][w] return dp[n][capacite] poids = [2, 3, 4, 5] valeurs = [3, 4, 5, 6] print(knapsack(poids, valeurs, 5)) # 7 (prendre objets 2+3 ou 4+1)
5. Plus longue sous-séquence commune (LCS) 🔤
Trouver la plus longue séquence de caractères commune à deux chaînes (dans l'ordre, pas forcément consécutifs).
Exécution pas à pas
pseudo1/17🧩FONCTION lcs(a, b)FONCTION lcs(a, b)Le programme exécute cette action, puis passe à la suivante.On définit une fonction appelée "lcs(a, b)". Cette fonction contient des instructions qui seront exécutées quand on l'appelle.
def lcs(a, b): m, n = len(a), len(b) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(1, m + 1): for j in range(1, n + 1): if a[i - 1] == b[j - 1]: dp[i][j] = dp[i - 1][j - 1] + 1 else: dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) return dp[m][n] print(lcs("ABCDEF", "ACDF")) # 4 → "ACDF"
6. Distance d'édition (Levenshtein) ✏️
Nombre minimum d'opérations (insertion, suppression, substitution) pour transformer une chaîne en une autre.
Exécution pas à pas
pseudo1/24🧩FONCTION levenshtein(a, b)FONCTION levenshtein(a, b)Le programme exécute cette action, puis passe à la suivante.On définit une fonction appelée "levenshtein(a, b)". Cette fonction contient des instructions qui seront exécutées quand on l'appelle.
def levenshtein(a, b): m, n = len(a), len(b) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(m + 1): dp[i][0] = i for j in range(n + 1): dp[0][j] = j for i in range(1, m + 1): for j in range(1, n + 1): if a[i - 1] == b[j - 1]: dp[i][j] = dp[i - 1][j - 1] else: dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) return dp[m][n] print(levenshtein("cheval", "chevalier")) # 3 (ajouter i, e, r)
7. Chemin dans une grille avec obstacles 🧭
Compter le nombre de chemins uniques du coin supérieur gauche au coin inférieur droit, en évitant les obstacles (1 = bloqué).
Exécution pas à pas
pseudo1/20🧩FONCTION chemins_unique(grille)FONCTION chemins_unique(grille)Le programme exécute cette action, puis passe à la suivante.On définit une fonction appelée "chemins_unique(grille)". Cette fonction contient des instructions qui seront exécutées quand on l'appelle.
def chemins_unique(grille): if not grille or grille[0][0] == 1: return 0 m, n = len(grille), len(grille[0]) dp = [[0] * n for _ in range(m)] dp[0][0] = 1 for i in range(m): for j in range(n): if grille[i][j] == 1: dp[i][j] = 0 continue if i > 0: dp[i][j] += dp[i - 1][j] if j > 0: dp[i][j] += dp[i][j - 1] return dp[m - 1][n - 1] grille = [[0, 0, 0], [0, 1, 0], [0, 0, 0]] print(chemins_unique(grille)) # 2
Progression recommandée 🎯
- Fibonacci avec mémoïsation — comprendre le cache (facile)
- Sac à dos — découverte du tableau 2D (moyen)
- LCS — manipulation de chaînes (moyen)
- Distance d'édition — 3 opérations possibles (moyen-difficile)
- Chemin dans une grille — obstacles et dénombrement (difficile)
Exécution pas à pas
État de la mémoire
6 variables⚡ Simulateur — Programmation dynamique
Étape 1/20- Programmation dynamique = diviser pour régner + mémoïsation
- Deux approches : top-down (mémoïsation) et bottom-up (tabulaire)
- Transforme un problème exponentiel en problème polynomial
- Fibonacci naïf : O(2ⁿ) → DP : O(n)
Deux approches pour Fibonacci.