0 XP
?
Algorithmes Avancés/Programmation dynamique
Avancé75 min50 XP

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

pseudo
1/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

pseudo
1/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

pseudo
1/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 :

i01234567
dp[i]011235813

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

pseudo
1/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

pseudo
1/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

pseudo
1/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

pseudo
1/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 🎯

  1. Fibonacci avec mémoïsation — comprendre le cache (facile)
  2. Sac à dos — découverte du tableau 2D (moyen)
  3. LCS — manipulation de chaînes (moyen)
  4. Distance d'édition — 3 opérations possibles (moyen-difficile)
  5. Chemin dans une grille — obstacles et dénombrement (difficile)

Exécution pas à pas

OuiNon
0/6

État de la mémoire

6 variables
Variable
Valeur
Type
Addr.
dp[0]
0
int
0x4100
dp[1]
1
int
0x4104
dp[2]
1
int
0x4108
dp[3]
2
int
0x410C
dp[4]
3
int
0x4110
dp[5]
5
int
0x4114
Aucune variable active
dp[0]0x4100
0
int
dp[1]0x4104
1
int
dp[2]0x4108
1
int
dp[3]0x410C
2
int
dp[4]0x4110
3
int
dp[5]0x4114
5
int

⚡ Simulateur — Programmation dynamique

Étape 1/20
📝 Pseudo-code
1# Fibonacci — mémoïsation
2def fib_naif(n):
3 if n <= 1: return n
4 return fib_naif(n-1) + fib_naif(n-2)
5
6# Mémoïsation : O(n)
7def fib_memo(n, cache={}):
8 if n in cache: return cache[n]
9 if n <= 1: return n
10 cache[n] = fib_memo(n-1, cache) + fib_memo(n-2, cache)
11 return cache[n]
12
13# Tabulaire : O(n)
14def fib_iteratif(n):
15 if n <= 1: return n
16 dp = [0] * (n + 1)
17 dp[1] = 1
18 for i in range(2, n + 1):
19 dp[i] = dp[i-1] + dp[i-2]
20 return dp[n]
⚡ Programmation dynamique
Sélectionnez une approche ci-dessous
💡 Concepts clés
  • 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.