Aller au contenu principal

Informatique · Terminale · 6 min de lecture

Programmation dynamique : mémoriser pour ne pas recalculer

Résoudre un problème en combinant les solutions de sous-problèmes qui se chevauchent, chacune calculée une seule fois et mémorisée (de haut en bas avec mémoïsation, ou de bas en haut en remplissant un tableau) ; exemples du rendu de monnaie optimal (nombre minimal de pièces) et de l'alignement de séquences ; coût en mémoire de la table.

Chapitre du programme : Numérique et sciences informatiques (spécialité)

Bloc 1 sur 7 · Comprendre

Comprendre : des sous-problèmes qui se répètent

Rendu de monnaie optimal. Avec des pièces de valeurs données (en quantité illimitée), quel est le nombre minimal de pièces pour obtenir une somme s ? Si l'on utilise d'abord une pièce p, il reste à obtenir s − p de façon optimale. D'où une solution récursive : essayer chaque pièce, et garder le meilleur résultat.

appels = 0

def nb_min_naif(s, pieces):
    global appels
    appels = appels + 1
    if s == 0:
        return 0
    meilleur = s
    for p in pieces:
        if p <= s:
            meilleur = min(meilleur, 1 + nb_min_naif(s - p, pieces))
    return meilleur

print(nb_min_naif(30, [1, 3, 4]), appels)
8 2550408

La ligne global appels (hors programme) permet seulement à la fonction de modifier la variable appels créée en dehors d'elle : on s'en sert ici pour compter les appels. Partir de meilleur = s est possible parce que la pièce de 1 fait partie du système : s pièces de 1 donnent toujours la somme s.

La réponse (8 pièces) est juste, mais il a fallu plus de deux millions et demi d'appels : les mêmes sous-problèmes (la somme 20, la somme 10…) sont recalculés un très grand nombre de fois, parce que les sous-problèmes se chevauchent.

La programmation dynamique calcule chaque sous-problème une seule fois et mémorise son résultat.

Bloc 2 sur 7 · Approfondir

Approfondir : mémoïsation ou tableau de bas en haut

De haut en bas, avec mémoïsation. On garde la récursion, mais on range chaque résultat dans un dictionnaire memo ; avant de calculer, on regarde s'il y est déjà. Le compteur appels fonctionne comme ci-dessus.

appels = 0

def nb_min_memo(s, pieces, memo):
    global appels
    appels = appels + 1
    if s == 0:
        return 0
    if s in memo:
        return memo[s]
    meilleur = s
    for p in pieces:
        if p <= s:
            meilleur = min(meilleur, 1 + nb_min_memo(s - p, pieces, memo))
    memo[s] = meilleur
    return meilleur

print(nb_min_memo(30, [1, 3, 4], {}), appels)
8 86

Même réponse, avec 86 appels au lieu de 2 550 408.

De bas en haut, en remplissant un tableau. t[x] contient le nombre minimal de pièces pour la somme x. On remplit t de gauche à droite : t[0] = 0, et chaque case utilise des cases déjà calculées, t[x] = 1 + le plus petit des t[x − p], pour les pièces p ⩽ x.

def nb_min(s, pieces):
    t = [0]
    for x in range(1, s + 1):
        meilleur = x
        for p in pieces:
            if p <= x and t[x - p] + 1 < meilleur:
                meilleur = t[x - p] + 1
        t.append(meilleur)
    return t[s]

print(nb_min(8, [1, 4, 5]))
2

Pour 8 avec des pièces de 1, 4 et 5, l'algorithme glouton donne 5 + 1 + 1 + 1 (4 pièces) ; la programmation dynamique trouve 4 + 4 (2 pièces).

Le prix à payer est la mémoire : un tableau (ou un dictionnaire) de taille s + 1. La même idée sert pour d'autres problèmes, comme l'alignement de séquences (comparer deux mots ou deux brins d'ADN).

Bloc 3 sur 7 · Exemple

Exemple guidé : nb_min(13, [1, 4, 5])

Énoncé. Quel nombre ce programme affiche-t-il ?

def nb_min(s, pieces):
    t = [0]
    for x in range(1, s + 1):
        meilleur = x
        for p in pieces:
            if p <= x and t[x - p] + 1 < meilleur:
                meilleur = t[x - p] + 1
        t.append(meilleur)
    return t[s]
print(nb_min(13, [1, 4, 5]))

Méthode : remplir le tableau à la main.

  1. De 1 à 3, seule la pièce de 1 convient : t[x] = x.
  2. t[4] = 1 + t[0] = 1 (une pièce de 4) ; t[5] = 1 + t[0] = 1 (une pièce de 5).
  3. t[6] = 1 + t[5] = 2 ; t[7] = 3 ; t[8] = 1 + t[4] = 2 ; t[9] = 1 + t[5] = 2 ; t[10] = 1 + t[5] = 2 ; t[11] = 3 ; t[12] = 1 + t[8] = 3.
  4. t[13] : avec 1, 1 + t[12] = 4 ; avec 4, 1 + t[9] = 3 ; avec 5, 1 + t[8] = 3. Le minimum est 3 : 5 + 4 + 4.
3

Le glouton aurait pris 5, 5, puis trois pièces de 1 : 5 pièces.

Bloc 4 sur 7 · Visualiser

Voir : le tableau t pour les pièces 1, 4 et 5

pieces = [1, 4, 5]
t = [0]
for x in range(1, 14):
    meilleur = x
    for p in pieces:
        if p <= x and t[x - p] + 1 < meilleur:
            meilleur = t[x - p] + 1
    t.append(meilleur)
print(t)
[0, 1, 2, 3, 1, 1, 2, 3, 2, 2, 2, 3, 3, 3]
Tableau : Somme x, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13
Somme x012345678910111213
t[x]01231123222333

Continuez sur votre lancée

Vous avez découvert l’essentiel et un exemple. Retrouvez la suite du cours et les points à retenir, puis passez à l’entraînement guidé dans votre compte.

  • Les formules et les repères à retenir
  • Les erreurs fréquentes expliquées
  • Un exercice avec des aides et sa correction

L’accès gratuit comprend les cours et exercices de votre niveau choisi.

Sources

  • BO spécial n°8 du 25 juillet 2019 — programme de spécialité numérique et sciences informatiques de terminale générale, annexe (https://eduscol.education.fr/document/30010/download) — rubrique « Algorithmique », programmation dynamique
  • T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Algorithmique (Introduction to Algorithms), 3e édition française, Dunod — chapitre 15, « Programmation dynamique »

Vous pensez avoir trouvé une erreur dans cette leçon ? Signalez-la par e-mail.

Explorer les autres cours

Accès rapide