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
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.
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
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.)
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.
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.
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)
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.)
(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.)
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)
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 1 | 2 | 3 | 1 | 2 | 3 | 4 | 2 | |
| choix | — | 1 | 1 | 1 | 1 | 1 | 1 | 7 | 7 | 7 | 10 | 10 | 10 | 10 | 7 |
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)
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.)
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é.)
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 "BATEAU" et "TABLEAU", 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 "BEAU" (et "TEAU" 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.)
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)
(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.)
(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.)
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.)
- 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
- () Mémoïser le nombre de chemins de la grille (exercice 2) et comparer au calcul naïf pour une grille (compter les appels).
- () Le rendu de monnaie : tables et rendus pour avec puis — où le glouton se trompe-t-il sur le second (essayer : contre ) ?
- ( ) Le nombre de façons de rendre (et non le minimum de pièces) : adapter la récurrence pour compter sans double-comptage des ordres (boucler sur les pièces à l'extérieur) — la subtilité combinations/permutations.
- () La plus longue sous-suite croissante d'un tableau : sur les d'élément plus petit ; , reconstruction.
- () Le sac à dos indivisible du chapitre 7, enfin exact : — implémenter, reconstruire, et battre le glouton sur son contre-exemple.
B. Séquences
- () Dérouler la table de Levenshtein à la main pour
"AMI""RAMER"et lire la suite d'opérations. - ( ) Le plus long palindrome contenu dans une chaîne (sous-suite) : remarquer que c'est la PLSC de la chaîne et de son renversée — justifier, implémenter.
- () Aligner les tarifs : un texte et sa version corrigée — afficher un
diffminimal (lignes communes par PLSC, / pour le reste) sur deux petits fichiers. - () La distance d'édition avec coûts (substitution 2, insertion/suppression 1) : adapter, et discuter ce que les coûts changent aux corrections proposées par un correcteur.
C. Graphes et tables à deux dimensions
- () Floyd-Warshall sur le graphe routier du chapitre 12 : la matrice complète des distances entre les cinq villes ; vérifier trois valeurs contre Dijkstra.
- ( ) Reconstruire les chemins de Floyd-Warshall : maintenir
suivant[i][j](le premier pas du chemin optimal) et écrirechemin(i, j). - () Détecter un cycle négatif avec Floyd-Warshall : que devient la diagonale ? Construire un exemple et le test.
- () Le plus court chemin dans une grille de coûts (chaque case un prix, déplacements bas/droite) : table, reconstruction, et comparaison avec Dijkstra sur la même grille — pourquoi la table suffit-elle ici (graphe sans cycle, ordre topologique naturel) ?
D. Études
- () Le découpage de barre : une barre de longueur , des prix par longueur — maximiser le revenu de la découpe ; table, reconstruction, et le glouton « la pièce au meilleur prix au mètre » battu par un contre-exemple.
- ( ) La pyramide de nombres (descendre du sommet en additionnant, gauche ou droite) : de bas en haut en — et pourquoi le glouton « la plus grosse case suivante » échoue.
- () Mesurer le pseudo-polynomial : chronométrer la partition équilibrée pour à fixé — vérifier la linéarité en et conclure sur « polynomial en la valeur, exponentiel en la taille ».
- ( ) Dossier de synthèse « du glouton à la table » : reprendre les quatre problèmes du chapitre 7 (monnaie, activités, sac à dos, plus proche voisin du TSP), classer chacun (glouton prouvé / table nécessaire / aucun des deux ne garantit), implémenter les tables manquantes, et arbitrer au banc de force brute — selon les six compétences du programme.