Adloun

Diviser pour régner

Cours complet · informatique (MP2I/MPI), chapitre 15 · MP2I et MPI

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

15.1 Trois temps

Le chapitre précédent énumérait ; celui-ci découpe. La méthode se dit en trois temps :

Le gain vient d'un fait arithmétique : diviser la taille par deux à chaque étape ne laisse que étages. Encore faut-il que le troisième temps soit bon marché — c'est là que tout se joue, et c'est ce que la récurrence du chapitre chap:recursivite mesure :

15.2 La dichotomie

Définition 15.1Recherche dichotomique

Chercher dans un tableau trié : on compare à l'élément médian, ce qui élimine la moitié du tableau.


(* Renvoie Some i tel que t.(i) = v, ou None.
   Précondition : t est trié par ordre croissant. *)
let recherche t v =
  let rec aux g d =
    if g > d then None
    else
      let m = g + (d - g) / 2 in
      if t.(m) = v then Some m
      else if t.(m) < v then aux (m + 1) d
      else aux g (m - 1)
  in
  aux 0 (Array.length t - 1)

Ici : un seul sous-problème est résolu, et la combinaison est vide. .

ImportantCe que veut dire
recherche linéairedichotomie
1 00010
1 000 00020
1 000 000 00030

Multiplier la taille par mille n'ajoute que dix comparaisons. C'est le rendement le plus spectaculaire de tout le livre, et il ne coûte qu'une chose : que le tableau soit trié.

15.2.1 La dichotomie là où on ne l'attend pas

Le programme insiste : « on présente un exemple de dichotomie où son recours n'est pas évident : par exemple, la couverture de points de la droite par segments égaux de plus petite longueur ».

Méthode : Dichotomiser sur la RÉPONSE, et non sur les données

Le problème ne parle d'aucun tableau trié. L'idée est ailleurs : on ne cherche pas dans les données, on cherche la valeur de la réponse, et l'on s'appuie sur une propriété de monotonie.

Le problème. points sur une droite, segments de même longueur . Quelle est la plus petite permettant de tout couvrir ?

L'observation décisive. Si une longueur suffit, alors toute longueur suffit aussi. La propriété « suffit » est donc monotone en : fausse jusqu'à un seuil, vraie ensuite. Un seuil dans une suite monotone se cherche par dichotomie.

Le test, glouton et linéaire. Pour une donnée, on couvre au plus vite : le premier segment commence au premier point non couvert, et l'on recommence.


(* Nombre minimal de segments de longueur L couvrant les points triés x. *)
let segments_necessaires x l =
  let n = Array.length x in
  let compte = ref 0 and i = ref 0 in
  while !i < n do
    let debut = x.(!i) in
    incr compte;
    while !i < n && x.(!i) <= debut +. l do incr i done
  done;
  !compte

(* Plus petite longueur permettant de couvrir x avec k segments, à eps près.
   Précondition : x trié, non vide, k >= 1. *)
let plus_petite_longueur x k eps =
  let bas = ref 0.0 and haut = ref (x.(Array.length x - 1) -. x.(0)) in
  while !haut -. !bas > eps do
    let milieu = (!bas +. !haut) /. 2.0 in
    if segments_necessaires x milieu <= k then haut := milieu   (* ça suffit *)
    else bas := milieu                                          (* trop court *)
  done;
  !haut

Complexité. Chaque test est , et la dichotomie divise l'intervalle par deux : .

ImportantLa question à se poser

Ma réponse est-elle un nombre, et la propriété « telle valeur convient » est-elle monotone ? Si oui, on dichotomise sur la réponse — même si le problème ne ressemble en rien à une recherche.

Le schéma est le même pour « la plus petite capacité de camion permettant livraisons », « la plus grande vitesse permettant d'arriver à l'heure », « le plus petit délai tenable ». Il transforme un problème d'optimisation en une suite de problèmes de décision — un passage que le chapitre chap:decidabilite formalisera sous le nom de transformation par un seuil.

AttentionLa dichotomie sur les flottants ne se termine pas par égalité

La boucle s'arrête sur haut -. bas &gt; eps, jamais sur bas = haut : deux flottants ne deviennent pas égaux, et la boucle tournerait sans fin. C'est le défaut du chapitre chap:algo-prog, rencontré ici en situation.

15.3 Le tri par partition-fusion

Méthode : Trier en fusionnant

Diviser le tableau en deux moitiés, trier chacune, fusionner les deux moitiés triées.


(* Fusionne deux listes triées en une liste triée. Complexité : O(|a| + |b|). *)
let rec fusion a b =
  match a, b with
  | [], l | l, [] -> l
  | x :: ra, y :: rb ->
      if x <= y then x :: fusion ra b else y :: fusion a rb

(* Trie l par ordre croissant. Complexité : Theta(n log n) dans TOUS les cas. *)
let rec tri_fusion = function
  | ([] | [_]) as l -> l                    (* zéro ou un élément : déjà trié *)
  | l ->
      let rec couper = function
        | [] -> ([], [])
        | [x] -> ([x], [])
        | x :: y :: r -> let (a, b) = couper r in (x :: a, y :: b)
      in
      let (g, d) = couper l in
      fusion (tri_fusion g) (tri_fusion d)
◆Théorème 15.2Complexité du tri par partition-fusion

, dans le meilleur comme dans le pire cas.

Démonstration

La coupe est , la fusion aussi : chaque appel à fusion consomme un élément par appel récursif. Donc .

Le calcul est celui du chapitre chap:recursivite, et il se lit sur un dessin : à chaque étage de la récursion, les sous-problèmes totalisent éléments, donc de travail ; et il y a étages. Total : .

Rien dans ce raisonnement ne dépend des données : le tri par partition-fusion ne connaît pas de « mauvais » tableau.

iRemarqueCe qu'il coûte en mémoire

d'espace supplémentaire — la fusion construit une nouvelle liste. C'est son seul défaut face au tri rapide, qui trie en place mais dégénère en sur les pires entrées, et face au tri par tas (chapitre chap:tas), qui est en place et garanti.

15.3.1 Une variante qui compte : les inversions

Exemple 15.3Compter les inversions, exemple cité par le programme

Une inversion d'un tableau est un couple avec et . Leur nombre mesure le désordre : nul si le tableau est trié, s'il est à l'envers.

La force brute est . Or la fusion les compte gratuitement : quand on prend un élément de la moitié droite alors qu'il reste éléments à gauche, cet élément est plus petit que ces -là, tous situés avant lui — soit inversions d'un coup.


(* Coupe l en ses n/2 premiers et le reste. CONTIGUË, contrairement au
   « couper » du tri : le comptage exige que TOUTE la moitié gauche précède
   la droite dans la liste d'origine. *)
let couper_contigu l =
  let n = List.length l / 2 in
  let rec aux k = function
    | r when k = 0 -> ([], r)
    | x :: r -> let (a, b) = aux (k - 1) r in (x :: a, b)
    | [] -> ([], [])
  in aux n l

(* Renvoie (liste triée, nombre d'inversions). Complexité : Theta(n log n). *)
let rec tri_compte = function
  | ([] | [_]) as l -> (l, 0)
  | l ->
      let (g, d) = couper_contigu l in
      let (g', ig) = tri_compte g and (d', id) = tri_compte d in
      let rec fusion a na b =
        match a, b with
        | [], l -> (l, 0)
        | l, [] -> (l, 0)
        | x :: ra, y :: rb ->
            if x <= y then let (f, k) = fusion ra (na - 1) b in (x :: f, k)
            else
              (* y passe devant TOUS les éléments restants de a. On transporte
                 leur nombre en argument : « List.length a » ici rendrait la
                 fusion quadratique, et donc l'algorithme entier. *)
              let (f, k) = fusion a na rb in (y :: f, k + na)
      in
      let (f, ifus) = fusion g' (List.length g') d' in
      (f, ig + id + ifus)

Le même découpage, le même coût, une information de plus. C'est la marque d'une bonne méthode : on l'adapte sans la refaire.

AttentionDeux pièges, et le premier rend un résultat faux sur TOUTE entrée

La coupe doit être contiguë. Le couper du tri sépare un élément sur deux : pour trier, n'importe quelle coupe convient, et celle-là est la plus courte à écrire. Mais le comptage repose sur le fait que toute la moitié gauche précède la droite dans la liste d'origine — ce que la coupe alternée détruit. Mesuré sur les permutations de quatre éléments avec la coupe alternée : les sont mal comptées, et la liste déjà triée rend au lieu de .

Et List.length a dans la fusion la rendrait quadratique, donc l'algorithme entier aussi : on transporte la longueur en argument. Une complexité annoncée se vérifie en mesurant, pas en la recopiant du tri dont on est parti.

15.4 La rencontre au milieu

Définition 15.4Rencontre au milieu (meet in the middle)

Quand l'exhaustif coûte , on coupe l'ensemble en deux moitiés de taille , on énumère séparément les possibilités de chacune, puis on les apparie. Le coût passe de à — la racine carrée du précédent.

Exemple 15.5Somme d'un sous-ensemble, l'exemple du programme

Étant donné entiers et une cible , existe-t-il un sous-ensemble de somme ?


(* Vrai s'il existe un sous-ensemble de t de somme c. Complexité : O(2^(n/2) · n). *)
let somme_sous_ensemble t c =
  let n = Array.length t in
  let moitie = n / 2 in
  (* toutes les sommes atteignables avec les éléments d'indices [d, f[ *)
  let sommes d f =
    let acc = ref [0] in
    for i = d to f - 1 do
      acc := !acc @ List.map (fun s -> s + t.(i)) !acc
    done;
    !acc
  in
  let a = sommes 0 moitie in
  let b = List.sort compare (sommes moitie n) in
  let tab = Array.of_list b in
  (* pour chaque somme de la première moitié, on CHERCHE son complément
     dans la seconde, par DICHOTOMIE : c'est la « rencontre » *)
  List.exists (fun s -> recherche tab (c - s) <> None) a

Le gain, chiffré. Pour : l'exhaustif fait opérations — dix-huit minutes. La rencontre au milieu en fait de chaque côté, plus le tri et les recherches : quelques dixièmes de seconde. Le même problème, quatre ordres de grandeur d'écart.

ImportantDeux méthodes emboîtées

Remarquez ce que fait ce dernier algorithme : il divise pour régner (deux moitiés), puis se sert de la dichotomie pour apparier. Les méthodes de ce chapitre se composent — et c'est ainsi qu'on gagne des ordres de grandeur sur des problèmes que le chapitre chap:exploration déclarait hors de portée.

15.5 Les points les plus proches

Exemple 15.6Le troisième exemple cité par le programme

Étant donné points du plan, trouver la paire la plus proche. La force brute examine les paires : .

Diviser : on trie les points par abscisse et l'on coupe par une droite verticale médiane. Régner : on résout à gauche et à droite, obtenant . Combiner : il reste à examiner les paires à cheval sur la droite. Seuls comptent les points situés dans la bande de largeur centrée sur la coupure — et, une fois cette bande triée par ordonnée, chaque point n'a besoin d'être comparé qu'à un nombre borné de suivants, car deux points de la bande distants de plus de verticalement ne peuvent pas faire mieux que .

Complexité : , soit — la bande se traite en temps linéaire une fois les points triés par ordonnée.

15.6 Ce qu'il faut retenir

ImportantDiviser pour régner : quatre points
  • Diviser, régner, combiner — et c'est le coût de la combinaison qui décide de tout, via , résolue à la main.
  • La dichotomie vaut aussi sur la réponse, dès que la propriété « telle valeur convient » est monotone. C'est le cas le plus rentable, et le moins visible.
  • Le tri par partition-fusion est dans tous les cas, au prix de de mémoire — et il compte les inversions gratuitement.
  • La rencontre au milieu remplace par : sur , dix-huit minutes deviennent un dixième de seconde.

Continuer sur Adloun : animation, QCM, fiches, exercices