Une ligne qui coûte un ordre de grandeur
Exercice · niveau 2 · informatique (MP2I/MPI), chapitre 15 — Diviser pour régner
Énoncé
Dans la fusion qui compte les inversions, le nombre d'inversions créées est obtenu par List.length a. Quelle est la complexité réelle de l'algorithme ? Corriger.
Corrigé
List.length est en : elle parcourt. L'appeler à chaque prise d'un élément de droite transforme une fusion linéaire en fusion quadratique. Sur une liste strictement décroissante, où chaque prise vient de droite, la fusion de deux moitiés de taille coûte , d'où
et non .
La correction consiste à transporter la longueur au lieu de la recalculer — c'est une information que l'appelant possède déjà :
(* Renvoie (liste triée, longueur, nombre d'inversions).
Précondition : couper_contigu sépare préfixe et suffixe (exercice précédent).
Complexité : Theta(n log n) dans tous les cas. *)
let compte_inversions l0 =
let rec aux l n =
if n <= 1 then (l, 0)
else
let (g, d) = couper_contigu l in
let ng = n / 2 in
let (g', ig) = aux g ng and (d', id) = aux d (n - ng) in
(* na est la longueur de a, transportée, jamais recalculée *)
let rec fusion a na b =
match a, b with
| [], m -> (m, 0)
| m, [] -> (m, 0)
| x :: ra, y :: rb ->
if x <= y then let (f, k) = fusion ra (na - 1) b in (x :: f, k)
else let (f, k) = fusion a na rb in (y :: f, k + na)
in
let (f, i) = fusion g' ng d' in
(f, ig + id + i)
in snd (aux l0 (List.length l0))
La mesure — opérations comptées sur une liste strictement décroissante, le pire cas :
| avec `List.length` | sans | |||
|---|---|---|---|---|
| 16 | 152 | 32 | 64 | 120 |
| 256 | ||||
Les deux premières colonnes diffèrent exactement de : , , . Ce n'est pas un hasard — chaque appel à List.length a coûte exactement le nombre d'inversions qu'il compte, et la somme de ces nombres est le nombre total d'inversions, ici . Le surcoût de la ligne fautive est le résultat qu'elle calcule. Et les temps, mesurés :
| avec | sans | rapport | |
|---|---|---|---|
| s | s | ||
| s | s | ||
| s | s | ||
| s | s | 178 |
Quand double, la version lente est multipliée par — donc par , la signature du quadratique — et la rapide par .
Ce qu'il faut retenir. La complexité d'un algorithme n'est pas celle de son schéma : elle est celle de son code. Ici le schéma « diviser, régner, fusionner en temps linéaire » est parfaitement juste, et une seule ligne l'a démenti. La règle est la même qu'au chapitre chap:sequentielles : sur une liste chaînée, connaître la longueur est gratuit, la calculer est linéaire — et l'on ne calcule jamais dans une boucle ce qu'on peut transporter.
Les autres exercices de ce chapitre Le cours du chapitre
Un blocage sur cet exercice ? Le tuteur d'Adloun guide par questions, sans donner la réponse.