Algorithmique 1 · L2 · Section 4/6
Stratégies fondamentales
Progression
#Stratégies algorithmiques fondamentales
Prérequis: récursion et récurrences (Master Theorem), tris (chapitre précédent), notations O/Θ.
Objectifs:
- Identifier le paradigme adapté à un problème nouveau, avec des critères décidables.
- Énoncer pour chaque paradigme ce qui doit être prouvé pour garantir l'optimalité.
- Connaître les exemples canoniques et la structure de leurs preuves.
Un algorithme résout un problème; un paradigme est une manière de construire la solution. Trois grands paradigmes couvrent l'essentiel des algorithmes efficaces, et chacun a sa condition d'application, qui se vérifie plutôt qu'elle ne se devine.
#Diviser pour régner
#Schéma
- Diviser l'instance en sous-instances plus petites.
- Régner: résoudre récursivement chaque sous-instance (cas de base pour les petites).
- Combiner les solutions partielles.
Coût générique:
où a est le nombre de sous-problèmes, n/b leur taille, f(n) le coût de division et combinaison. Le Master Theorem tranche (voir chapitre Complexité des algorithmes).
Condition d'application: les sous-problèmes sont indépendants (aucun chevauchement). C'est ce qui distingue ce paradigme de la programmation dynamique.
#Exemples canoniques
- Tri fusion: a = 2, b = 2, f(n) = Θ(n) → Θ(n log n). La combinaison (fusion) fait tout le travail.
- Recherche binaire: a = 1, b = 2, f(n) = Θ(1) → Θ(log n). On ne suit qu'une seule branche.
- Karatsuba (grands entiers): découpe chaque nombre en deux moitiés; la méthode scolaire (4 produits) se remplace par 3 produits bien choisis: a = 3, b = 2 → Θ(n^(log₂ 3)) ≈ Θ(n^(1.58)), mieux que Θ(n²).
- Strassen (matrices): 7 produits au lieu de 8: a = 7, b = 2 → Θ(n^(log₂ 7)) ≈ Θ(n^(2.81)), battant le Θ(n³) scolaire. En pratique, les constantes ne le rendent rentable que pour de grandes matrices.
#Trace: recherche binaire dans [1, 2, ..., 100], cible 73
| Étape | Intervalle | Milieu testé | Décision |
|---|---|---|---|
| 1 | [1, 100] | 50 | 73 > 50, garder [51, 100] |
| 2 | [51, 100] | 75 | 73 < 75, garder [51, 74] |
| 3 | [51, 74] | 62 | 73 > 62, garder [63, 74] |
| 4 | [63, 74] | 68 | 73 > 68, garder [69, 74] |
| 5 | [69, 74] | 71 | 73 > 71, garder [72, 74] |
| 6 | [72, 74] | 73 | trouvé |
Invariant: la cible, si présente, est toujours dans l'intervalle courant. Six comparaisons, soit ⌈log₂ 100⌉ = 7 au plus: chaque test divise l'espace par deux.
#Algorithmes gloutons
#Schéma
Construire la solution pas à pas: à chaque étape, faire le choix localement optimal selon un critère, et ne jamais revenir dessus.
Le paradigme est séduisant parce qu'il est simple et rapide. Il est correct uniquement sous deux conditions à prouver:
- Propriété du choix glouton: il existe une solution optimale qui commence par le choix glouton. (Preuve type: argument d'échange: partir d'une solution optimale et y substituer le choix glouton sans dégrader le coût.)
- Sous-structure optimale: une fois le choix glouton fait, le reste de la solution optimale est une solution optimale du sous-problème restant.
Sans ces preuves, un glouton est une heuristique: il peut être faux, pas seulement lent.
#Contre-exemple: rendu de monnaie
Avec les pièces européennes 200, rendre toujours la plus grande pièce possible est optimal. Avec 4 et 6 à rendre: le glouton donne 4 + 1 + 1 (trois pièces), l'optimal est 3 + 3 (deux pièces). Même stratégie, système différent: l'optimalité n'appartient pas à l'algorithme mais au couple (algorithme, système), d'où l'obligation de la preuve.
#Exemples canoniques prouvés
- Sélection d'activités: parmi des activités qui se chevauchent, maximiser le nombre d'activités retenues. Glouton: prendre celle qui termine le plus tôt, retirer les incompatibles, répéter. Preuve par échange: toute solution optimale peut voir sa première activité remplacée par celle qui finit plus tôt sans rien casser.
- Huffman: fusionner itérativement les deux symboles de plus faible fréquence; optimal pour le codage préfixe sans perte.
- Kruskal (arbre couvrant minimal): ajouter l'arête de poids minimal qui ne crée pas de cycle. Prouvé par la propriété de coupe.
- Ordonnancement pour minimiser le retard: trier les tâches par date limite croissante.
#Trace: sélection d'activités
Sélection d'activités
Maximiser le nombre d'activités compatibles dans une journée.
(début, fin): A(1,4), B(3,5), C(0,6), D(5,7), E(3,9), F(5,9), G(6,10), H(8,11), I(8,12), J(2,14), K(12,16)
#Programmation dynamique
#Schéma
- Identifier des sous-problèmes qui se chevauchent (le même sous-problème revient plusieurs fois dans la récursion naïve).
- Définir la valeur d'une solution optimale récursivement sur ces sous-problèmes (sous-structure optimale).
- Calculer chaque sous-problème une seule fois: de haut en bas avec mémoïsation, ou de bas en haut en remplissant une table.
- Reconstruire la solution (pas seulement sa valeur) en relisant la table si besoin.
Condition d'application: sous-problèmes chevauchants et sous-structure optimale. Sans chevauchement, c'est du diviser pour régner, et la table n'apporte rien.
#Différence avec diviser pour régner
| Critère | Diviser pour régner | Programmation dynamique |
|---|---|---|
| Sous-problèmes | disjoints | chevauchants |
| Mémoïsation | inutile | le point entier |
| Direction | récursion top-down naturelle | top-down mémoïsé ou bottom-up tabulaire |
#Trace: Fibonacci, l'implémentation naïve
, et l'arbre d'appels de F(5):
1 F(5)2 / \3 F(4) F(3)4 / \ / \5 F(3) F(2) F(2) F(1)6 / \ ... ...7 F(2) F(1)F(3) est calculé deux fois, F(2) trois fois, et le phénomène s'amplifie: le coût naïf est (φ ≈ 1.618), exponentiel, alors qu'il n'existe que n sous-problèmes distincts.
Mémoïsation (top-down): chaque F(k) n'est calculé qu'une fois, les suivants se servent du cache: O(n) temps, O(n) espace pour le cache et la pile.
1def fib_memo(n, memo=None):2 if memo is None:3 memo = {0: 0, 1: 1}4 if n not in memo:5 memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)6 return memo[n]Bottom-up (tabulaire), puis optimisation d'espace: seuls les deux derniers états servent.
1def fib_iter(n):2 a, b = 0, 13 for _ in range(n):4 a, b = b, a + b5 return a # O(n) temps, O(1) espaceMême récurrence, deux ordres de calcul, un seul sous-problème par valeur de k: c'est tout le paradigme.
#Exemples canoniques
Sac à dos 0/1. Objets avec poids et valeurs , capacité W. Sous-problème: = meilleure valeur avec les i premiers objets et capacité w.
prendre ou ne pas prendre l'objet i. Table (n+1) × (W+1): O(nW) temps et espace, O(W) espace si l'on ne garde qu'une ligne. Ce coût est dit pseudo-polynomial: il est polynomial en la valeur numérique de W, exponentiel en le nombre de chiffres de W; c'est ce détail qui empêche le sac à dos d'être dans P au sens de la théorie des problèmes, alors qu'il se résout bien en pratique pour des capacités raisonnables.
Plus longue sous-séquence commune (LCS).
Table n × m: O(nm) temps et espace. De git diff à la comparaison d'ADN.
Distance d'édition (Levenshtein). Minimum d'insertions, suppressions, substitutions pour transformer une chaîne en une autre: même table que LCS, récurrence enrichie du cas substitution. O(nm).
Sous-séquence croissante maximale (LIS/PMI). Récurrence directe en O(n²); avec une structure auxiliaire (table des queues par longueur, recherche dichotomique), O(n log n).
#Comparaison des paradigmes
| Critère | Diviser pour régner | Glouton | Programmation dynamique |
|---|---|---|---|
| Sous-problèmes | disjoints, indépendants | un seul par étape | chevauchants |
| Optimalité | héritée de la combinaison | à prouver (échange/sous-structure) | garantie par la récurrence exacte |
| Revenir sur ses pas | pas de pas à revenir | jamais (irréversible) | la table explore les alternatives |
| Complexité typique | O(n log n) | O(n log n) (tri initial) | O(n²), O(nm) ou O(nW) |
| Exemples | fusion, recherche binaire, Karatsuba | Huffman, Kruskal, activités | LCS, sac à dos, distance d'édition |
#Exercice 1: monnaie, quand le glouton échoue
Soit le système 10. Rendez 14 avec le glouton (plus grande pièce d'abord), puis optimalement. Prouvez que le résultat glouton n'est pas optimal, et donnez la table DP qui conduit à l'optimum.
Correction: glouton: 10 + 1 + 1 + 1 + 1 = 14 en cinq pièces. Optimal: 7 + 7, deux pièces. Le glouton n'est pas optimal: solution exhibée, contre-exemple suffit (inutile de chercher une preuve générale là où un contre-exemple tranche). Table DP M[w] = nombre minimal de pièces pour w, , M[0] = 0: M[14] = 1 + min(M[13], M[7], M[4]) = 1 + min(5, 1, 4) = 2, atteint par deux pièces de 7. La table explore tous les systèmes de pièces d'un coup: c'est la robustesse de la DP contre le caractère exact du glouton qui dépend du système.
#Exercice 2: nombre de chemins dans une grille
Une grille n × m, on part d'en haut à gauche et on avance uniquement vers la droite ou vers le bas. Comptez les chemins vers en bas à droite: (a) par récursion naïve, (b) par DP. Comparez les coûts.
Correction: soit C(i, j) le nombre de chemins de (i, j) vers l'arrivée: , avec C = 1 sur la dernière ligne et la dernière colonne (un seul chemin tout droit). (a) La récursion naïve recalcule massivement les mêmes cases; le nombre d'appels suit le nombre de chemins eux-mêmes, exponentiel (de l'ordre de ). (b) Chaque case ne dépend que de deux voisines déjà calculées si l'on remplit depuis l'arrivée: table n × m remplie en O(nm), chaque cellule en O(1). Les deux retournent le même entier (vérifiable sur des petites grilles: 3 × 3 donne 6 chemins: ): correction identique, complexités exponentiellement différentes. C'est l'exemple le plus net du chapitre: mêmes sous-problèmes, avec ou sans table, deux mondes de coût.