Programmation dynamique en Python : principes, Fibonacci, chemins et sac à dos

Programmation dynamique en Python : principes, Fibonacci, chemins et sac à dos

La programmation dynamique est une technique d’algorithmique qui consiste à résoudre un problème en réutilisant les résultats de sous-problèmes déjà calculés. En Python, elle se traduit souvent par deux approches : la mémoïsation avec un cache, ou la tabulation avec un tableau rempli progressivement.

Elle est utile dès qu’un problème présente deux propriétés : des sous-problèmes qui se répètent, et une solution globale qui dépend de solutions plus petites. Fibonacci, les chemins dans une grille, le sac à dos, les coefficients binomiaux ou certains jeux à deux joueurs sont de bons exemples.

La réponse courte

La programmation dynamique en Python suit toujours la même logique :

  1. définir l’état du problème ;
  2. écrire la transition entre un état et les états plus petits ;
  3. choisir une condition de départ ;
  4. mémoriser les résultats pour éviter les recalculs ;
  5. vérifier la complexité obtenue.

Version mémoïsée de Fibonacci :

from functools import lru_cache


@lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)


print(fibonacci(40))

La version récursive naïve recalcule les mêmes valeurs de nombreuses fois. Avec lru_cache, chaque fibonacci(k) est calculé une seule fois.

Mémoïsation ou tabulation

La mémoïsation part souvent d’une fonction récursive. On écrit la solution naturelle, puis on ajoute un cache.

La tabulation part de la base. On remplit un tableau dans l’ordre nécessaire, jusqu’à obtenir la réponse finale.

Approche Idée Avantage Limite
Mémoïsation récursion + cache très lisible quand la relation est naturelle attention à la profondeur de récursion
Tabulation tableau rempli progressivement contrôle clair de l’ordre et de la mémoire demande parfois plus de réflexion au départ

Version tabulée de Fibonacci :

def fibonacci_iteratif(n):
    if n < 2:
        return n

    precedent = 0
    courant = 1

    for _ in range(2, n + 1):
        precedent, courant = courant, precedent + courant

    return courant


print(fibonacci_iteratif(40))

Cette version est en O(n) en temps et O(1) en mémoire. Elle est souvent préférable en production pour ce problème précis.

Exemple : compter les chemins dans une grille

Supposons une grille de lignes x colonnes. On part en haut à gauche et on veut arriver en bas à droite. À chaque étape, on peut seulement aller vers la droite ou vers le bas.

Le nombre de chemins jusqu’à une case est la somme des chemins venant de la case du haut et de la case de gauche.

def compter_chemins(lignes, colonnes):
    dp = [[0] * colonnes for _ in range(lignes)]

    for ligne in range(lignes):
        dp[ligne][0] = 1

    for colonne in range(colonnes):
        dp[0][colonne] = 1

    for ligne in range(1, lignes):
        for colonne in range(1, colonnes):
            dp[ligne][colonne] = dp[ligne - 1][colonne] + dp[ligne][colonne - 1]

    return dp[-1][-1]


print(compter_chemins(3, 4))

Ici, l’état est dp[ligne][colonne]. La transition est :

dp[ligne][colonne] = dp[ligne - 1][colonne] + dp[ligne][colonne - 1]

La complexité est O(lignes * colonnes).

Exemple : le sac à dos 0/1

Le problème du sac à dos illustre bien l’intérêt de la programmation dynamique. On a des objets, chacun avec un poids et une valeur. On veut maximiser la valeur totale sans dépasser une capacité.

Dans la version 0/1, chaque objet est pris ou ignoré.

def sac_a_dos(objets, capacite):
    dp = [0] * (capacite + 1)

    for poids, valeur in objets:
        for c in range(capacite, poids - 1, -1):
            dp = max(dp, dp + valeur)

    return dp[capacite]


objets = [(2, 3), (3, 4), (4, 5), (5, 8)]
print(sac_a_dos(objets, 8))

La boucle sur les capacités se fait à l’envers pour éviter de réutiliser plusieurs fois le même objet dans la même étape. C’est une erreur fréquente : si vous parcourez dans le mauvais sens, vous résolvez sans le vouloir une autre variante du problème.

Comment reconnaître un problème de programmation dynamique

Un problème est un bon candidat si vous voyez ces signaux :

  • une solution récursive évidente, mais trop lente ;
  • des sous-problèmes identiques qui reviennent ;
  • une phrase comme “meilleur résultat jusqu’à cette position” ;
  • une grille, une séquence, un préfixe, une capacité ou un score ;
  • une décision répétée : prendre ou ne pas prendre, avancer ou rester, choisir un coup.

Exemples :

Problème État possible
Fibonacci n
Chemins dans une grille (ligne, colonne)
Sac à dos (index_objet, capacite_restante)
Coefficients binomiaux (n, k)
Distance d’édition (i, j)
Jeu à deux joueurs (position, joueur, profondeur)

Pour les coefficients binomiaux, vous pouvez lire aussi le guide math.comb et triangle de Pascal.

Les erreurs fréquentes

La première erreur est de choisir un état trop large. Si votre clé de cache contient toute une liste alors qu’un simple index suffit, le programme devient lourd.

La deuxième erreur est d’oublier les conditions de départ. Une transition correcte ne sert à rien si les premières cases du tableau sont mal initialisées.

La troisième erreur est de confondre récursion et programmation dynamique. Une fonction récursive sans cache peut rester exponentielle.

La quatrième erreur est de ne pas mesurer la mémoire. Certains tableaux DP en deux dimensions peuvent être réduits en une seule dimension.

La cinquième erreur est de vouloir utiliser la programmation dynamique partout. Si un problème n’a pas de sous-problèmes qui se répètent, un algorithme glouton, un tri ou une structure comme heapq peut être plus simple.

Où placer cette notion dans votre apprentissage

La programmation dynamique arrive après la complexité, les structures de données, les tris et les graphes de base. Elle demande de raisonner sur l’état, pas seulement d’écrire une boucle.

Pour continuer dans le bon ordre :

La règle à retenir : définissez l’état, écrivez la transition, posez les cas de base, puis seulement ensuite codez. Si ces quatre éléments sont clairs, la programmation dynamique devient beaucoup moins mystérieuse.

Références

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.