Adloun

La programmation dynamique

Cours complet · informatique (tronc commun des prépas scientifiques), chapitre 18 · prépas scientifiques, tronc commun

Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre

<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>18.1 Introduction et motivation

Le chapitre 6 s'était achevé sur un avertissement : la fonction de Fibonacci récursive à deux appels explose en — non parce que le problème est dur, mais parce qu'elle recalcule sans fin les mêmes sous-problèmes. Le chapitre 7 avait laissé une blessure ouverte : le rendu de monnaie glouton, optimal sur les pièces usuelles, rend cinq pièces pour avec le système quand deux suffisent — le choix local sûr n'existait pas. Ce chapitre soigne les deux maux d'un seul remède, l'une des méthodes les plus puissantes de l'algorithmique : la programmation dynamique.

L'idée tient en une phrase : quand on ne sait pas quel choix est le bon, on les essaie tous — mais on ne calcule chaque sous-problème qu'une fois, en mémorisant sa réponse. Deux ingrédients la rendent possible : une sous-structure optimale (l'optimum se construit à partir d'optimums de sous-problèmes) et un chevauchement des sous-problèmes (les mêmes reviennent sans cesse — sinon mémoriser ne sert à rien). Quand les deux sont réunis, l'exponentielle des essais s'effondre en un produit raisonnable : nombre d'états coût par état. Au menu : la monnaie enfin rendue exactement, des partitions équilibrées, des sous-suites communes, la distance entre deux mots, et les distances dans un graphe par une triple boucle de trois lignes — avec, à chaque fois, la reconstruction de la solution, pas seulement sa valeur.

18.2 Le mal et le remède : recalculer ou mémoriser

18.2.1 Le chevauchement des sous-problèmes

Exemple 18.1Fibonacci, autopsie d'une explosion

L'arbre des appels de fib(5) appelle fib(3) deux fois, fib(2) trois fois, fib(1) cinq fois — les mêmes calculs, refaits. Pour fib(30) : environ millions d'appels pour… valeurs distinctes. Le problème n'a que sous-problèmes ( à ) ; c'est leur chevauchement — chacun requis par une myriade de chemins de l'arbre — qui fabrique l'exponentielle.

Définition 18.2Mémoïsation, calcul de bas en haut

Deux implémentations du même remède :

  • la mémoïsation (du haut vers le bas) : garder la récursion, mais consigner chaque résultat dans un dictionnaire (chapitre 17 !) — au second appel, la réponse est servie sans calcul ;
  • le calcul de bas en haut : remplir un tableau des sous-problèmes du plus petit au plus grand, chaque case se déduisant des précédentes — plus de récursion du tout.

def fib_memo(n: int, memo: dict) -> int:     # du haut vers le bas
    if n <= 1:
        return n
    if n not in memo:
        memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
    return memo[n]

def fib_table(n: int) -> int:                # de bas en haut
    if n == 0:
        return 0
    F = [0] * (n + 1)
    F[1] = 1
    for k in range(2, n + 1):
        F[k] = F[k - 1] + F[k - 2]
    return F[n]

Les deux coûtent — un calcul par sous-problème. La mémoïsation suit la structure naturelle de la récurrence et ne calcule que les états réellement atteints ; le bas en haut évite la pile de récursion (chapitre 6 : limite à !) et rend les optimisations de mémoire possibles. Choisir est affaire de goût et de contraintes — savoir écrire les deux est au programme.

18.2.2 La sous-structure optimale

Définition 18.3Sous-structure optimale

Un problème d'optimisation a la propriété de sous-structure optimale si toute solution optimale se décompose en solutions optimales de sous-problèmes du même type. Le rendu de monnaie l'illustre : si un rendu optimal de commence par une pièce de , alors le reste doit être un rendu optimal de — sinon, en remplaçant ce reste par un rendu meilleur, on améliorerait le total, contredisant son optimalité. (C'est l'argument « couper-coller » : toute preuve de sous-structure optimale a cette forme.)

Exemple 18.4Le rendu de monnaie, enfin exact

Notons le nombre minimal de pièces pour rendre la somme avec le système . La sous-structure optimale donne la récurrence : un rendu optimal de commence par une pièce — on ne sait pas laquelle, on les essaie toutes :


def rendu_exact(s: int, pieces: list) -> list:
    """Le rendu en nombre minimal de pièces (liste), pour tout système."""
    INFINI = float("inf")
    N = [0] + [INFINI] * s            # N[v] : nb minimal de pièces pour v
    choix = [None] * (s + 1)          # la pièce qui réalise le minimum
    for v in range(1, s + 1):
        for p in pieces:
            if p <= v and N[v - p] + 1 < N[v]:
                N[v] = N[v - p] + 1
                choix[v] = p
    rendu = []                        # reconstruction : suivre les choix
    while s > 0:
        rendu.append(choix[s])
        s -= choix[s]
    return rendu

Sur le système piège du chapitre 7 : rendu_exact(14, [10, 7, 1]) déroule la table jusqu'à et reconstruit — le glouton rendait , cinq pièces. La blessure du chapitre 7 est refermée : là où aucun choix local n'est sûr, on les essaie tous, et la mémorisation rend l'essai exhaustif abordable. Si N[s] reste infini, la somme est inatteignable avec ces pièces — la reconstruction ne doit alors pas être lancée (partitionner les cas, chapitre 10).

Complexité : Le compte général

Coût d'une programmation dynamique (nombre d'états) (coût de la récurrence par état). Rendu de monnaie : états essais . Fibonacci : . La mémoire suit le nombre d'états — et c'est elle, souvent, la vraie contrainte (section suivante). Noter que dépend de la valeur , pas de sa taille en chiffres : pour , l'algorithme est impraticable — une subtilité de modèle de coût dans l'esprit du chapitre 11.

18.2.3 La méthode, et sa frontière avec le glouton

Méthode : Concevoir une programmation dynamique

  • Définir le sous-problème : quels paramètres décrivent un état ? (« : nombre minimal de pièces pour ».) C'est l'étape créative — tout le reste en découle.
  • Écrire la récurrence : exprimer l'optimum d'un état par un / sur le premier choix, appliqué à des états strictement plus petits ; prouver la sous-structure optimale (couper-coller) ; donner les cas de base.
  • Ordonner le calcul : de bas en haut dans un ordre où chaque état ne dépend que d'états déjà remplis — ou mémoïser et laisser la récursion trouver l'ordre.
  • Compter : états coût par état (temps), états (mémoire).
  • Reconstruire : mémoriser le choix gagnant de chaque état (ou le retrouver en re-testant la récurrence), puis remonter du problème complet aux cas de base.
ImportantGlouton et programmation dynamique : la ligne de partage

Les deux stratégies exigent la même propriété de sous-structure optimale — c'est leur socle commun. Le glouton (chapitre 7) ajoute une exigence de plus : un choix local sûr, prouvable par échange, qui permet de ne suivre qu'une branche — coût minuscule, mais la preuve d'échange est obligatoire, et souvent elle n'existe pas. La programmation dynamique renonce au choix sûr et explore toutes les branches, en mutualisant par mémorisation — coût plus élevé, validité générale. La démarche professionnelle les enchaîne : chercher un glouton prouvable ; à défaut, écrire la récurrence et mémoriser ; et le banc de force brute du chapitre 7 reste l'arbitre des deux.

iRemarqueLa mémoire, l'autre facture

Le programme insiste : la programmation dynamique paie en mémoire ce qu'elle gagne en temps — , qui peut être (tables à deux indices) ou pire. Deux atténuations classiques : ne garder que les états encore utiles (Fibonacci : deux variables suffisent — ; les tables ligne par ligne : une ou deux lignes — exercice 9) ; mais attention, la reconstruction exige en général la table entière — économiser la mémoire, c'est souvent renoncer au chemin et ne garder que la valeur. Un arbitrage à expliciter, pas à subir.

<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>18.3 Exercices résolus

Niveau (Application directe du cours)

Exercice 1 : Mesurer le chevauchement

Instrumenter fib naïve et fib_memo pour compter les appels ; donner les comptes pour , la loi de croissance de chacun, et vérifier l'affirmation du cours (« millions d'appels pour valeurs »).

Démonstration (Solution)

def fib_compte(n, compteur):
    compteur[0] += 1
    if n <= 1:
        return n
    return fib_compte(n - 1, compteur) + fib_compte(n - 2, compteur)
# n = 10 :       177 appels ; n = 20 :    21 891 ; n = 30 : 2 692 537
# fib_memo :      19 appels ;             39     ;          59  (= 2n - 1)

La version naïve suit (chaque cran de multiplie par le nombre d'or — c'est la récurrence de Fibonacci appliquée à son propre coût) ; la mémoïsée fait appels : chaque état est calculé une fois, puis servi depuis le dictionnaire. Le rapport pour : . (L'instrumentation par compteur — une liste mutée, chapitre 10 — est l'outil de diagnostic à retenir : avant d'optimiser une récursion, compter ses appels distincts et totaux ; un grand écart entre les deux signe le chevauchement, donc un candidat à la mémoïsation.)

Exercice 2 : L'escalier et la grille

(a) De combien de façons monter un escalier de marches par pas de ou ? Donner la récurrence, le calcul de bas en haut, et la valeur pour . (b) Combien de chemins mènent du coin au coin d'une grille en n'allant que vers le bas ou la droite ? Vérifier sur la grille la valeur du chapitre 13.

Démonstration (Solution)

(a) Le dernier pas vaut ou : , — Fibonacci décalé ; de bas en haut, . (b) On n'entre dans une case que par le haut ou la gauche : , (un seul chemin le long d'un bord).


def nb_chemins(p: int, q: int) -> int:
    C = [[1] * q for _ in range(p)]
    for i in range(1, p):
        for j in range(1, q):
            C[i][j] = C[i - 1][j] + C[i][j - 1]
    return C[p - 1][q - 1]

assert nb_chemins(3, 3) == 6          # le BFS du chapitre 13 confirmait déjà

(Deux dénombrements sans optimisation : la programmation dynamique compte aussi — la récurrence additionne au lieu de minimiser, tout le reste est identique ; et relie la table au triangle de Pascal, qui est… une table de programmation dynamique née trois siècles avant l'informatique.)

Exercice 3 : Dérouler la monnaie

Pour le système et : remplir à la main la table et les choix, reconstruire le rendu, et comparer au glouton pas à pas. Que rend le système pour ?

Démonstration (Solution)
01234567891011121314
012345612312342
choix—111111777101010107

Lecture de : essais , , — minimum par la pièce . Reconstruction : , rendu . Le glouton prenait (laissant , soit pièces — il s'enfermait). Pour : () — le glouton, ici, aurait bon : le système piège ne piège pas partout, et c'est bien pourquoi trois tests ne prouvent rien (chapitre 7). (Remplir une table à la main une fois dans sa vie est le meilleur vaccin contre les indices décalés ; et la ligne « choix » montre que la reconstruction ne coûte presque rien à condition d'y penser pendant le remplissage.)

Niveau (Application avec raisonnement intermédiaire)

Exercice 4 : La partition équilibrée

Partager le tableau en deux paquets de sommes aussi proches que possible. (a) Définir le sous-problème booléen : « une partie du tableau somme exactement à » ; écrire la récurrence par élément. (b) Implémenter, trouver la meilleure somme , reconstruire le paquet. (c) Donner la complexité et son caractère pseudo-polynomial.

Démonstration (Solution)

(a) En traitant les éléments un à un : après l'élément , les sommes atteignables sont celles d'avant, plus celles d'avant décalées de (prendre ou ne pas prendre — pas de choix sûr : on garde les deux). (b)


def partition_equilibree(t: list) -> tuple:
    S = sum(t)
    atteignable = [True] + [False] * (S // 2)
    temoin = [None] * (S // 2 + 1)            # l'élément qui a atteint s
    for x in t:
        for s in range(S // 2, x - 1, -1):    # à rebours : x utilisé 1 fois !
            if not atteignable[s] and atteignable[s - x]:
                atteignable[s] = True
                temoin[s] = x
    meilleur = max(s for s in range(S // 2 + 1) if atteignable[s])
    paquet, s = [], meilleur                  # reconstruction par témoins
    while s > 0:
        paquet.append(temoin[s])
        s -= temoin[s]
    return (paquet, meilleur, S - meilleur)

# partition_equilibree([5, 8, 13, 4, 6]) : somme 36, moitié 18
# -> paquet [13, 5] (somme 18) contre [8, 4, 6] (somme 18) : équilibre parfait

(c) Temps , mémoire : polynomial en la valeur , exponentiel en sa taille en bits — « pseudo-polynomial » : très praticable pour des sommes modestes (, : dix millions d'opérations), impraticable pour des entiers de cinquante chiffres ; le problème général est réputé dur, et cette table est exactement ce qu'on sait faire de mieux en pratique courante. (Le parcours à rebours de la boucle interne est le piège technique : à l'endroit, l'élément pourrait servir deux fois ( déjà mis à jour au même tour) — une ligne d'écart entre « chaque élément au plus une fois » et « réutilisable à volonté », à connaître car les deux variantes existent et se ressemblent trait pour trait.)

Exercice 5 : L'ordonnancement de tâches pondérées

Des tâches : , , , , incompatibles si elles se chevauchent. (a) Montrer que le glouton « par fin croissante » du chapitre 7 échoue dès que les gains diffèrent. (b) Construire la programmation dynamique : tri par fin, prédécesseur compatible , récurrence ; calculer et reconstruire. (c) Coût ?

Démonstration (Solution)

(a) Le glouton par fin croissante (optimal pour compter les tâches, prouvé au chapitre 7) choisit , puis , puis : gain — en bloquant et ses . La preuve d'échange du chapitre 7 s'effondre : remplacer une tâche par une autre qui finit plus tôt ne préserve plus le gain. Sous-structure optimale : toujours là ; choix sûr : disparu — cap sur la table. (b) Tâches triées par fin : . Prédécesseurs compatibles (la dernière tâche finissant avant le début) : aucune, , aucune, . Récurrence — la tâche est dans l'optimum ou non :

Optimum ; reconstruction depuis : donc exclue ; donc prise, saut à : vide — solution . (c) Tri , recherche des par dichotomie (chapitre 5 !) , table : total . (Le schéma « (sans moi, moi meilleur compatible) » est l'un des plus réutilisés de la programmation dynamique — et l'exemple type du programme pour confronter glouton et table sur le même énoncé : seule la fonction objectif a changé, et tout l'édifice de preuve du glouton s'est écroulé.)

Exercice 6 : La plus longue sous-suite commune

La PLSC de deux chaînes est la plus longue suite de caractères apparaissant dans l'ordre (pas forcément contigus) dans les deux. (a) Récurrence sur les préfixes : pour les et premiers caractères. (b) Implémenter, calculer pour &quot;BATEAU&quot; et &quot;TABLEAU&quot;, reconstruire. (c) À quoi sert cet algorithme tous les jours ?

Démonstration (Solution)

(a) Comparer les derniers caractères : s'ils sont égaux, ils peuvent conclure la sous-suite commune ; sinon, l'un des deux au moins n'en fait pas partie :

(b)


def plsc(u: str, v: str) -> str:
    n, m = len(u), len(v)
    L = [[0] * (m + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            if u[i - 1] == v[j - 1]:
                L[i][j] = L[i - 1][j - 1] + 1
            else:
                L[i][j] = max(L[i - 1][j], L[i][j - 1])
    i, j, mot = n, m, []                      # reconstruction, à rebours
    while i > 0 and j > 0:
        if u[i - 1] == v[j - 1]:
            mot.append(u[i - 1]); i -= 1; j -= 1
        elif L[i - 1][j] >= L[i][j - 1]:
            i -= 1
        else:
            j -= 1
    return "".join(reversed(mot))

# plsc("BATEAU", "TABLEAU") -> "BEAU" (longueur 4)

: la reconstruction rend la sous-suite &quot;BEAU&quot; (et &quot;TEAU&quot; marche aussi — l'optimum n'est pas unique, la reconstruction en choisit un selon ses départages). (c) C'est l'algorithme de diff et des gestionnaires de versions : la PLSC de deux fichiers est leur partie commune maximale, et tout le reste — affiché en et — est le changement ; deux mille ans de copistes auraient aimé l'avoir. (Coût temps et mémoire : la table à deux indices est le format standard des problèmes à deux séquences — et le candidat parfait pour l'économie de mémoire de l'exercice 9, si l'on accepte de perdre la reconstruction.)

Exercice 7 : La distance d'édition (Levenshtein)

La distance d'édition entre deux mots est le nombre minimal d'opérations — insérer, supprimer ou substituer un caractère — pour transformer l'un en l'autre. (a) Récurrence sur les préfixes. (b) Implémenter et calculer . (c) Donner deux usages réels.

Démonstration (Solution)

(a) Même grille que la PLSC, autre récurrence — trois gestes possibles sur les derniers caractères :

(b)


def levenshtein(u: str, v: str) -> int:
    n, m = len(u), len(v)
    D = [[0] * (m + 1) for _ in range(n + 1)]
    for i in range(n + 1):
        D[i][0] = i
    for j in range(m + 1):
        D[0][j] = j
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            cout = 0 if u[i - 1] == v[j - 1] else 1
            D[i][j] = min(D[i - 1][j] + 1, D[i][j - 1] + 1,
                          D[i - 1][j - 1] + cout)
    return D[n][m]

assert levenshtein("CHAT", "CHIEN") == 3      # A->I, T->E, +N

Trois opérations : substituer A en I, T en E, insérer N — et la table prouve qu'on ne peut pas faire mieux, ce qu'aucun bricolage manuel ne garantit. (c) Les correcteurs orthographiques (proposer les mots du dictionnaire à distance de la faute) ; la bio-informatique (aligner deux séquences d'ADN, où insertions et mutations sont les opérations biologiques — les matrices de ce chapitre y tournent sur des génomes entiers). (PLSC et Levenshtein sont sœurs : même grille de préfixes, récurrences cousines — apprendre à reconnaître la famille « deux séquences table » fait gagner la moitié du travail de conception, l'étape 1 de la méthode.)

Niveau (Raisonnement subtil ou plusieurs étapes)

Exercice 8 : Floyd-Warshall — toutes les distances d'un graphe en trois lignes

(a) Pour un graphe pondéré à sommets numérotés, définir : la distance de à en n'utilisant que des sommets intermédiaires ; écrire la récurrence. (b) Implémenter sur matrice et vérifier sur le graphe piège du chapitre 14 (, , ) que la distance vaut bien . (c) Comparer à exécutions de Dijkstra.

Démonstration (Solution)

(a) Le plus court chemin de à n'utilisant que des intermédiaires passe par le sommet , ou non :

Sous-structure optimale sur un paramètre inattendu — l'ensemble des intermédiaires autorisés : c'est l'étape 1 de la méthode dans toute sa créativité. (b)


def floyd_warshall(M: list) -> list:
    """M : matrice des poids (inf si pas d'arc, 0 en diagonale). En place."""
    n = len(M)
    for k in range(n):
        for i in range(n):
            for j in range(n):
                if M[i][k] + M[k][j] < M[i][j]:
                    M[i][j] = M[i][k] + M[k][j]
    return M

INF = float("inf")
M = [[0, 2, 3], [INF, 0, INF], [INF, -2, 0]]   # s, a, b numérotés 0, 1, 2
floyd_warshall(M)
assert M[0][1] == 1                             # s -> b -> a : 3 - 2 = 1 !

Là où Dijkstra mentait en silence (chapitre 14 : il figeait à ), Floyd-Warshall rend : sa récurrence n'utilise jamais l'argument « le reste du chemin pèse » — les poids négatifs lui sont indifférents, tant qu'aucun cycle de poids total négatif n'existe (sinon « la distance » elle-même n'a plus de sens : on tournerait pour gagner). (c) Floyd-Warshall : , trois lignes, toutes les paires, poids négatifs admis. Dijkstra : aussi en version naïve — mais avec une bonne file : mieux sur les graphes creux, et poids positifs exigés. Règle pratique : petit graphe dense ou poids négatifs Floyd-Warshall ; grand graphe creux positif Dijkstra répété. (La boucle sur est extérieure — l'inverser avec casse la récurrence : l'ordre de remplissage est un théorème, pas un style ; le vérifier fait partie de l'étape 3 de la méthode.)

Exercice 9 : La mémoire, ligne par ligne

(a) Réduire Fibonacci à de mémoire. (b) Réduire Levenshtein à deux lignes (), vérifier l'accord avec la table complète. (c) Expliquer ce que cette économie sacrifie, et pourquoi ; que faire quand on veut et la mémoire réduite et la solution ?

Démonstration (Solution)

(a) ne dépend que de et : deux variables glissantes — a, b = b, a + b — et la table disparaît. (b) ne dépend que de : conserver la ligne précédente et la courante,


def levenshtein_2lignes(u: str, v: str) -> int:
    if len(v) > len(u):
        u, v = v, u                            # la ligne courte en mémoire
    prec = list(range(len(v) + 1))
    for i in range(1, len(u) + 1):
        cour = [i] + [0] * len(v)
        for j in range(1, len(v) + 1):
            cout = 0 if u[i - 1] == v[j - 1] else 1
            cour[j] = min(prec[j] + 1, cour[j - 1] + 1, prec[j - 1] + cout)
        prec = cour
    return prec[len(v)]

— même résultat que la table pleine sur tous les tests, mémoire au lieu de : pour deux chromosomes de caractères, c'est la différence entre mégaoctets et téraoctets. (c) Le sacrifice : la reconstruction. Remonter le chemin des choix exige de relire les cases abandonnées — la valeur optimale survit, la solution optimale non. Réponses possibles : garder la table pleine quand elle tient (la voie simple) ; recalculer localement à rebours ; ou les algorithmes « diviser pour régner » qui retrouvent le chemin en mémoire réduite au prix d'un facteur de temps (hors programme, mais l'existence mérite d'être sue : l'arbitrage temps-mémoire-solution est un triangle, pas un segment). (L'enjeu de mémoire est celui que le programme nomme explicitement : une programmation dynamique se dimensionne — états, octets, et ce qu'on veut à la fin — avant de coder, comme les complexités en espace du chapitre 10.)

Exercice 10 : Glouton ou table ? Le discernement final

Pour chacun des problèmes suivants, dire si un glouton prouvé existe ou si la programmation dynamique s'impose, et donner l'argument en deux phrases : (a) rendu de monnaie en système canonique ; (b) rendu en système quelconque ; (c) maximiser le nombre de tâches compatibles ; (d) maximiser le gain de tâches pondérées ; (e) plus courts chemins à poids positifs ; (f) à poids quelconques.

Démonstration (Solution)

(a) Glouton : la preuve d'échange du chapitre 7 passe (toute pièce se rembourse en pièces plus petites sans allonger le rendu) — choix local sûr établi. (b) Table : le contre-exemple tue tout choix sûr ; la récurrence du cours, elle, ne suppose rien du système. (c) Glouton : « par fin croissante », prouvé par échange au chapitre 7 — la tâche qui finit le plus tôt laisse le plus de place, quel que soit le reste. (d) Table : l'exercice 5 — le gain casse la preuve d'échange, le schéma (sans moi, moi compatible) s'impose. (e) Glouton : Dijkstra est un glouton prouvé (chapitre 14 — figer le minimum est un choix local sûr, et sa preuve est exactement un argument d'échange sur les chemins). (f) Table : Floyd-Warshall — le choix sûr de Dijkstra meurt avec les poids négatifs, la récurrence sur les intermédiaires survit. Le motif est constant : sous-structure optimale partout ; le glouton chaque fois qu'un échange se prouve ; la table dès qu'un contre-exemple le réfute — et le banc de force brute pour départager les jours de doute. (C'est la mise en rapport que demande le programme, et une grille de lecture pour toute l'algorithmique d'optimisation : les deux méthodes ne s'opposent pas, elles s'étagent — le glouton est une programmation dynamique dont le s'est réduit à un seul terme, certificat à l'appui.)

Synthèse du chapitre (à retenir)
  • Deux ingrédients : chevauchement des sous-problèmes (les mêmes états reviennent — compter appels totaux vs distincts pour le diagnostiquer) sous-structure optimale (l'optimum se compose d'optimums — preuve par couper-coller).
  • Deux implémentations : mémoïsation (récursion dictionnaire des résultats, chapitre 17 ; ne calcule que les états atteints, mais pile de récursion) ; bas en haut (table remplie des petits états vers les grands ; pas de pile, optimisations de mémoire possibles). Coût états coût par état.
  • La méthode en 5 temps : définir l'état (l'étape créative) ; récurrence par « premier choix » cas de base ; ordre de remplissage (un théorème, pas un style) ; compter temps et mémoire ; reconstruire en mémorisant les choix puis en remontant.
  • Le bestiaire : monnaie (, pseudo-polynomial) ; partition équilibrée (sommes atteignables, boucle à rebours pour « chaque élément une fois ») ; tâches pondérées ((sans moi, moi ), tri par fin) ; famille « deux séquences » : PLSC et Levenshtein (table sur les préfixes, — diff, correcteurs, ADN) ; Floyd-Warshall (intermédiaires autorisés , , poids négatifs admis sans cycle négatif).
  • Mémoire : — réductible en lignes glissantes ( pour Fibonacci, deux lignes pour Levenshtein) au prix de la reconstruction : valeur sans chemin ; arbitrage à déclarer.
  • Glouton vs table : même socle (sous-structure optimale) ; glouton un choix local prouvé par échange (monnaie canonique, tâches en nombre, Dijkstra) ; table essayer tous les choix quand l'échange échoue (système quelconque, tâches pondérées, poids négatifs) ; force brute en arbitre.

18.4 Exercices d'entraînement

Cette banque d'exercices, classée par thème, couvre l'intégralité du chapitre. La numérotation prolonge celle des dix exercices résolus. Légende : application directe, raisonnement intermédiaire, approfondissement ; le symbole signale un classique incontournable.

A. Premiers pas

B. Séquences

C. Graphes et tables à deux dimensions

D. Études

Continuer sur Adloun : animation, QCM, fiches, exercices