Cours 5 · Graphes et paradigmesLeçon 2 sur 2
Paradigmes algorithmiques
3 h de lecture8 sections Version PDF
Algorithme glouton et rendu de monnaie, retour sur trace, et première approche de la programmation dynamique par mémoïsation.
Rendre 6 avec des pièces de 1, 3 et 4. La méthode naturelle — prendre la plus grosse possible, recommencer — donne 4, puis 1, puis 1 : trois pièces. La solution optimale en emploie deux : 3 et 3.
Cette méthode naturelle a un nom, elle est employée partout, et elle vient de donner une réponse fausse sur un exemple de six pièces. Ce dernier chapitre prend de la hauteur sur trois manières de chercher une solution, sur ce que chacune garantit, et surtout sur ce qu'elle ne garantit pas.
L'algorithme glouton
Un algorithme glouton construit la solution par étapes, en faisant à chaque étape le choix qui paraît le meilleur sur le moment, et sans jamais revenir dessus.
Ses qualités sont évidentes : il est simple à écrire, rapide — généralement linéaire ou en après un tri — et il ne consomme presque rien. Son défaut l'est moins : rien ne garantit que la suite de choix localement optimaux donne un optimum global.
Le rendu de monnaie en est l'illustration parfaite, parce qu'il montre que la réponse dépend du système de pièces. Avec l'euro — 1, 2, 5, 10, 20, 50 — le glouton est toujours optimal. Avec 1, 3, 4, il échoue dès 6. Un système où le glouton est optimal est dit canonique, et le vérifier n'est pas trivial.
D'où la règle du chapitre, qui vaut pour toute votre pratique : un algorithme glouton se prouve ou se rejette, il ne se suppose jamais. Le fait qu'il donne la bonne réponse sur les exemples testés ne dit rien — l'échec de 1, 3, 4 n'apparaît qu'à 6, et il faudrait chercher pour le trouver par hasard.
Trois gloutons célèbres sont prouvés, et le semestre en a déjà croisé un.
Dijkstra (chapitre 9) extrait le sommet non traité le plus proche et le déclare définitif. La preuve tient à la positivité des poids : aucun détour ne peut raccourcir. Changez cette hypothèse, et le glouton devient faux.
Huffman construit un code de compression en fusionnant à chaque étape les deux symboles les moins fréquents. Kruskal construit un arbre couvrant minimal en ajoutant les arêtes par poids croissant, en sautant celles qui créeraient un cycle. Les deux se démontrent, et c'est ce qui les distingue du rendu de monnaie.
Le retour sur trace
Quand le glouton échoue, la solution suivante est d'essayer, mais intelligemment.
Le retour sur trace (backtracking) explore systématiquement l'espace des solutions en construisant une solution partielle, et en revenant en arrière dès qu'elle s'avère impossible à compléter.
fonction explorer(solutionPartielle) si complète alors enregistrer et retourner pour chaque choix possible à cette étape si le choix est compatible avec ce qui est déjà posé ajouter le choix explorer(solutionPartielle) RETIRER le choix ← le retour sur trace proprement ditDeux remarques rendent le procédé familier.
C'est un parcours en profondeur du chapitre 9, dans un arbre de choix qu'on ne construit jamais : chaque nœud est une solution partielle, chaque branche un choix, chaque feuille une solution complète ou une impasse. L'arbre n'existe qu'implicitement, sous forme de la pile d'appels du chapitre 1.
Et la ligne qui compte est la dernière : retirer le choix avant d'essayer le suivant. L'oublier laisse l'état pollué par la branche précédente, et c'est la faute numéro un du TD.
Ce qui rend le procédé viable est l'élagage : on abandonne une branche dès qu'elle est condamnée, sans descendre jusqu'aux feuilles. Aux huit dames — placer huit dames sur un échiquier sans qu'aucune n'en attaque une autre — l'exploration brute examinerait , soit plus de dispositions. En plaçant une dame par colonne et en abandonnant dès qu'une attaque apparaît, on descend à quelques milliers de nœuds. L'élagage ne change pas la nature exponentielle du problème ; il déplace la limite du praticable, et cela suffit souvent.
La programmation dynamique
Le retour sur trace essaie tout. La programmation dynamique évite d'essayer deux fois la même chose — et l'on a déjà vu le procédé au chapitre 2, sous le nom de mémoïsation.
Elle s'applique quand deux conditions sont réunies, et il faut les vérifier avant de se lancer.
Sous-structure optimale : la solution optimale du problème se construit à partir des solutions optimales de ses sous-problèmes. C'est ce qui permet de composer.
Chevauchement des sous-problèmes : les mêmes sous-problèmes reviennent un grand nombre de fois. C'est ce qui rend la mémorisation rentable — et c'est ce qui distingue la programmation dynamique de « diviser pour régner » du bloc II. Le tri fusion découpe en moitiés disjointes : aucune sous-question n'est posée deux fois, donc une table ne servirait à rien.
Deux mises en œuvre, pour un même résultat.
Descendante (mémoïsation) : la récursion naturelle, plus une table consultée avant tout
calcul. C'est le fibMemo du chapitre 2, et son avantage est de ne calculer que les
sous-problèmes réellement atteints.
Ascendante (tabulation) : on remplit la table dans l'ordre croissant des tailles, sans récursion. Plus économe — pas de pile d'appels — mais elle calcule tout, y compris l'inutile.
Reprenons le rendu de monnaie, correctement cette fois. Soit le nombre minimal de pièces pour rendre :
où parcourt les valeurs de pièces. Sur 1, 3, 4 avec , la table donne bien 2, en retenant plutôt que . Le glouton, lui, s'était engagé sur le 4 et ne pouvait plus revenir.
Pourquoi la programmation dynamique n'apporte-t-elle rien au tri fusion, alors qu'elle transforme Fibonacci ?
Les quatre paradigmes en regard
| Paradigme | Principe | Garantit l'optimum | Coût typique |
|---|---|---|---|
| Diviser pour régner | découper en parties disjointes | oui | |
| Glouton | choix localement optimal, sans retour | seulement si prouvé | ou |
| Retour sur trace | essayer, élaguer, revenir | oui, il explore tout | exponentiel |
| Programmation dynamique | mémoriser les sous-problèmes qui se répètent | oui | polynomial |
La lecture de ce tableau est la conclusion du semestre. On préfère toujours le glouton quand on peut le prouver, la programmation dynamique quand les sous-problèmes se répètent, et le retour sur trace quand rien d'autre ne s'applique — en sachant qu'on paie alors le prix exponentiel, et que l'élagage décide de la taille traitable.
Il existe une cinquième voie, hors programme mais qu'il faut connaître de nom : quand même le retour sur trace est hors de portée, on renonce à l'optimum et l'on emploie une heuristique ou un algorithme d'approximation, qui rend une solution correcte à un facteur près, en temps raisonnable. C'est la réponse pratique à la plupart des problèmes réellement difficiles.
Un algorithme glouton donne la bonne réponse sur les cinquante jeux de tests d'un TD. Que peut-on en conclure ?
À vous
L'exercice met les trois paradigmes sur le même problème, ce qui est la meilleure façon de les comparer.
Le rendu de monnaie d'abord : version gloutonne, puis version dynamique, puis recherche automatique du plus petit montant où les deux divergent sur un système donné. Vous retrouverez 6 pour 1, 3, 4 — et vous pourrez vérifier que le système de l'euro ne diverge jamais.
Les huit dames ensuite, en retour sur trace, avec un compteur de nœuds explorés. Vous comparerez le nombre de nœuds avec et sans élagage, sur des échiquiers de 4 à 8 cases de côté, et vous verrez la limite du praticable se déplacer.
Opposez glouton et programmation dynamique sur le rendu de monnaie, puis mesurez l'élagage aux n dames.
// ── 1. Rendu de monnaie, version gloutonne ──────────────────────────────── function gloutonRendu(pieces, montant) { const tri = [...pieces].sort((a, b) => b - a); // de la plus grosse const rendu = []; for (const p of tri) { while (montant >= p) { rendu.push(p); montant -= p; } } return montant === 0 ? rendu : null; } // ── 2. Rendu de monnaie, programmation dynamique ────────────────────────── // M[k] = nombre minimal de pièces pour rendre k. function dynamiqueRendu(pieces, montant) { const M = new Array(montant + 1).fill(Infinity); M[0] = 0; // ← à écrire : pour chaque k de 1 à montant, pour chaque pièce p <= k, // M[k] = min(M[k], 1 + M[k - p]) return M[montant] === Infinity ? null : M[montant]; } // ── 3. Cherche le plus petit montant où le glouton se trompe ────────────── function premierEcart(pieces, jusqua) { for (let k = 1; k <= jusqua; k++) { const g = gloutonRendu(pieces, k); const d = dynamiqueRendu(pieces, k); if (g !== null && d !== null && g.length !== d) { return { montant: k, glouton: g, gloutonN: g.length, optimal: d }; } } return null; } // ── 4. Les n dames, en retour sur trace ─────────────────────────────────── function dames(n, avecElagage) { let solutions = 0, noeuds = 0; const colonnes = []; // colonnes[i] = colonne de la dame de la ligne i function compatible(ligne, col) { for (let l = 0; l < ligne; l++) { const c = colonnes[l]; if (c === col || Math.abs(c - col) === ligne - l) return false; } return true; } function explorer(ligne) { noeuds++; if (ligne === n) { solutions++; return; } for (let col = 0; col < n; col++) { // ← sans élagage on descend même sur une position attaquée, et on ne // teste qu'arrivé au bout : écrivez les deux variantes. colonnes[ligne] = col; explorer(ligne + 1); colonnes.length = ligne; // LE retour sur trace : retirer le choix } } explorer(0); return { solutions, noeuds }; } // ── À VOUS ──────────────────────────────────────────────────────────────── // 1. Écrivez dynamiqueRendu. // 2. Trouvez le plus petit montant où le glouton échoue sur [1, 3, 4]. // Vérifiez que le système de l'euro ne diverge jamais. // 3. Complétez dames() pour comparer le nombre de nœuds avec et sans élagage. console.log("glouton [1,3,4] pour 6 :", gloutonRendu([1, 3, 4], 6));
En travaux pratiques
Le même problème, trois stratégies
Attaquer un unique problème par le glouton, le diviser-pour-régner et la programmation dynamique, puis constater lequel donne une solution optimale et à quel prix.
- Tous les TP précédents
- Le TP 2 : mémoïsation
- 1. Le glouton
Écrivez le rendu de monnaie glouton : prendre à chaque fois la plus grosse pièce possible. Testez-le sur le système de pièces courant.
- 2. Le faire échouer
Trouvez un système de pièces où le glouton ne donne PAS le minimum. Exhibez la somme et les deux solutions.
- 3. La programmation dynamique
Écrivez la version dynamique ascendante. Vérifiez qu'elle donne l'optimum sur le système où le glouton échouait, et comparez les temps.
- 4. Reconstituer la solution
Modifiez la version dynamique pour rendre non seulement le nombre de pièces mais la liste. Comparez à la reconstitution de chemin du TP 9.
- 5. Diviser pour régner
Sur un autre problème — la sous-séquence de somme maximale — écrivez la version en force brute, celle en diviser-pour-régner, puis la version linéaire. Mesurez les trois.
- 6. Reconnaître le paradigme
Pour cinq problèmes que vous choisirez, dites lequel des trois paradigmes s'applique et à quoi vous l'avez reconnu.
- 7. La limite
Écrivez le sac à dos en dynamique, mesurez son temps pour une capacité de 100, 10 000, un million. Expliquez pourquoi cette complexité n'est pas vraiment polynomiale.
- Vous exhibez un système de pièces et une somme où le glouton perd
- Votre version dynamique rend la LISTE des pièces, pas seulement leur nombre
- Vous nommez le critère qui rend un problème accessible à la programmation dynamique
Ce que ce semestre laisse
Dix chapitres plus tôt, la question était de passer de « comment écrire un algorithme » à « quelle structure choisir ». Le parcours a été le suivant.
La récursivité a donné le moyen de traiter des objets définis en fonction d'eux-mêmes, et la pile d'appels a montré ce que cela coûte. Diviser pour régner en a tiré une stratégie, et l'analyse des récurrences a permis de la chiffrer — jusqu'à démontrer que est une limite et non une performance. Les structures linéaires ont introduit le vrai sujet du semestre : une opération n'a pas de coût en soi, elle a un coût dans une structure donnée. Les arbres ont fait passer ce coût de à , sous condition d'équilibre. Les graphes ont montré que tout cela n'était que des cas particuliers.
Reste une idée qui traverse les cinq blocs, et c'est peut-être ce qu'il faut en garder : la plupart des gains du semestre viennent d'une meilleure organisation des données, pas d'un code plus astucieux. Le tri rapide bat le tri par insertion parce qu'il divise, pas parce qu'il compare plus vite. Un ABR bat une liste parce que sa forme élimine la moitié des candidats à chaque comparaison. Et le même parcours donne la profondeur ou la largeur selon qu'on lui donne une pile ou une file.
À retenir
Vous avez parcouru les 8 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.