Adloun

Files de priorité : le tas binaire

Cours complet · OCaml (option informatique), chapitre 19 · prépas MPSI et MP, option informatique

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

<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>19.1 Introduction et motivation

Une file de priorité est une collection d'où l'on extrait toujours l'élément le plus prioritaire (ici, le plus petit) — comme un service d'urgences traite d'abord les cas les plus graves. C'est l'outil qui manquait au chapitre 16 : Dijkstra a besoin, à chaque étape, du sommet non traité de plus petite distance. Avec une liste ou un tableau, le trouver coûte ; une file de priorité bien faite le donne en .

La structure reine pour cela est le tas binaire, un arbre binaire presque complet rangé… dans un simple tableau (chapitre 5). Ce chapitre l'implémente — insertion, extraction du minimum, construction — puis en tire le tri par tas (chapitre 9) et une version efficace de Dijkstra (chapitre 16).

19.2 Le tas binaire

Définition 19.1Tas (min)

Un tas binaire est un arbre binaire presque complet (tous les niveaux remplis sauf peut-être le dernier, de gauche à droite) vérifiant la propriété de tas : tout nœud est ses enfants. Conséquence : le minimum est à la racine.

On range cet arbre dans un tableau, niveau par niveau : la racine en 0, et pour le nœud d'indice i,

parent de `i``(i - 1) / 2`
fils gauche de `i``2 i + 1`
fils droit de `i``2 i + 2`

La taille du tas évoluant, on l'encapsule dans un enregistrement à champ mutable (chapitre 7) :


type tas = { mutable taille : int; donnees : int array }

let cree capacite = { taille = 0; donnees = Array.make capacite 0 }

let est_vide t = t.taille = 0
let minimum t = if est_vide t then failwith "tas vide" else t.donnees.(0)

19.3 Insérer : percolation vers le haut

On place le nouvel élément à la fin (dernière feuille), puis on le fait remonter tant qu'il est plus petit que son parent — pour rétablir la propriété de tas.


let insere t x =
  let d = t.donnees in
  d.(t.taille) <- x;
  t.taille <- t.taille + 1;
  let i = ref (t.taille - 1) in
  while !i > 0 && d.((!i - 1) / 2) > d.(!i) do
    let p = (!i - 1) / 2 in
    let tmp = d.(!i) in d.(!i) <- d.(p); d.(p) <- tmp;   (* échange avec le parent *)
    i := p
  done

Complexité : Insertion

L'élément remonte d'au plus la hauteur de l'arbre, soit échanges (un arbre presque complet de nœuds a une hauteur ). Insertion en .

19.4 Extraire le minimum : percolation vers le bas

Le minimum est à la racine. On le retire, on met la dernière feuille à sa place, puis on la fait descendre en l'échangeant avec le plus petit de ses enfants tant qu'elle viole la propriété de tas.


let extrait_min t =
  if est_vide t then failwith "tas vide";
  let d = t.donnees in
  let mini = d.(0) in
  t.taille <- t.taille - 1;
  d.(0) <- d.(t.taille);                    (* la dernière feuille monte à la racine *)
  let i = ref 0 and continuer = ref true in
  while !continuer do
    let g = 2 * !i + 1 and dr = 2 * !i + 2 in
    let petit = ref !i in
    if g < t.taille && d.(g) < d.(!petit) then petit := g;
    if dr < t.taille && d.(dr) < d.(!petit) then petit := dr;
    if !petit <> !i then begin
      let tmp = d.(!i) in d.(!i) <- d.(!petit); d.(!petit) <- tmp;
      i := !petit
    end
    else continuer := false
  done;
  mini

Complexité : Extraction

L'élément déplacé descend d'au plus la hauteur de l'arbre : . La file de priorité offre donc minimum en , insere et extrait_min en — bien mieux qu'une liste triée (insertion ) ou non triée (extraction ).

<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>19.5 Exercices résolus

Niveau (application directe du cours)

Exercice 1 : Naviguer dans le tableau

Donner les indices du parent, du fils gauche et du fils droit du nœud 4. Dessiner l'arbre du tableau [| 1; 3; 2; 7; 5 |] et vérifier que c'est un tas.

Démonstration

Parent de 4 : (4-1)/2 = 1 ; fils gauche : 2*4+1 = 9 ; fils droit : 10. Pour [|1;3;2;7;5|] : racine 1, enfants 3 et 2 ; enfants de 3 (indice 1) : 7 et 5. Chaque nœud est ses enfants ( ; ) : c'est bien un tas, de minimum 1.

Exercice 2 : API de base

Écrire cree, est_vide et minimum.

Démonstration

let cree capacite = { taille = 0; donnees = Array.make capacite 0 }
let est_vide t = t.taille = 0
let minimum t = if est_vide t then failwith "tas vide" else t.donnees.(0)

minimum se lit en donnees.(0) (la racine) en temps constant : c'est l'invariant du tas. La capacité fixe le nombre maximal d'éléments (tableau de taille fixe).

Exercice 3 : Insérer

Écrire insere et dérouler l'insertion de 0 dans le tas [| 1; 3; 2 |].

Démonstration

let insere t x =
  let d = t.donnees in
  d.(t.taille) <- x;
  t.taille <- t.taille + 1;
  let i = ref (t.taille - 1) in
  while !i > 0 && d.((!i - 1) / 2) > d.(!i) do
    let p = (!i - 1) / 2 in
    let tmp = d.(!i) in d.(!i) <- d.(p); d.(p) <- tmp;
    i := p
  done

On place 0 en indice 3 : [|1;3;2;0|]. Son parent (indice 1) est 3 &gt; 0 : on échange → [|1;0;2;3|]. Le nouveau parent (indice 0) est 1 &gt; 0 : on échange → [|0;1;2;3|]. 0 est remonté à la racine : c'est le nouveau minimum.

Niveau (raisonnement intermédiaire)

Exercice 4 : Extraire le minimum

Écrire extrait_min.

Démonstration

let extrait_min t =
  if est_vide t then failwith "tas vide";
  let d = t.donnees in
  let mini = d.(0) in
  t.taille <- t.taille - 1;
  d.(0) <- d.(t.taille);
  let i = ref 0 and continuer = ref true in
  while !continuer do
    let g = 2 * !i + 1 and dr = 2 * !i + 2 in
    let petit = ref !i in
    if g < t.taille && d.(g) < d.(!petit) then petit := g;
    if dr < t.taille && d.(dr) < d.(!petit) then petit := dr;
    if !petit <> !i then begin
      let tmp = d.(!i) in d.(!i) <- d.(!petit); d.(!petit) <- tmp; i := !petit
    end
    else continuer := false
  done;
  mini

On renvoie la racine, on y remonte la dernière feuille, puis on la fait redescendre vers le plus petit enfant tant qu'elle est trop grande. Les tests g &lt; t.taille et dr &lt; t.taille vérifient l'existence des enfants.

Exercice 5 : Vérifier la propriété de tas

Écrire est_un_tas d n : le tableau d (longueur utile n) respecte-t-il la propriété de tas ?

Démonstration

let est_un_tas d n =
  let ok = ref true in
  for i = 1 to n - 1 do
    if d.((i - 1) / 2) > d.(i) then ok := false   (* parent > enfant : violé *)
  done;
  !ok

Il suffit de vérifier, pour chaque nœud i (sauf la racine), qu'il n'est pas plus petit que son parent. Parcourir une seule fois les indices 1..n-1 suffit : chaque relation parent-enfant est ainsi testée. Coût .

Exercice 6 : Tri par tas

Écrire tri_par_tas arr qui trie un tableau d'entiers à l'aide d'un tas.

Démonstration

let tri_par_tas arr =
  let n = Array.length arr in
  let t = cree n in
  for i = 0 to n - 1 do insere t arr.(i) done;       (* tout insérer *)
  for i = 0 to n - 1 do arr.(i) <- extrait_min t done (* extraire dans l'ordre *)

On insère tous les éléments dans un tas, puis on les extrait : l'extraction du minimum à répétition les rend dans l'ordre croissant. insertions et extractions, chacune en , soit — la même garantie que le tri fusion (chapitre 9).

Niveau (approfondissement)

Exercice 7 : Les plus petits éléments

Écrire k_plus_petits arr k : la liste des k plus petits éléments de arr, triés.

Démonstration

let k_plus_petits arr k =
  let n = Array.length arr in
  let t = cree n in
  for i = 0 to n - 1 do insere t arr.(i) done;
  let rec extraire j = if j = 0 then [] else extrait_min t :: extraire (j - 1) in
  extraire k

On bâtit un tas de tous les éléments, puis on en extrait les k plus petits (les premiers à sortir). Coût ; on pourrait viser avec une construction de tas en (exercice 8).

Exercice 8 : Construire un tas en

On peut transformer un tableau quelconque en tas plus vite qu'en insérant un à un. Écrire construit d n qui réorganise d.(0..n-1) en tas par percolations vers le bas, des nœuds internes vers la racine.

Démonstration

let descendre d n i0 =       (* percoler le nœud i0 vers le bas dans d.(0..n-1) *)
  let i = ref i0 and continuer = ref true in
  while !continuer do
    let g = 2 * !i + 1 and dr = 2 * !i + 2 in
    let petit = ref !i in
    if g < n && d.(g) < d.(!petit) then petit := g;
    if dr < n && d.(dr) < d.(!petit) then petit := dr;
    if !petit <> !i then begin
      let tmp = d.(!i) in d.(!i) <- d.(!petit); d.(!petit) <- tmp; i := !petit
    end
    else continuer := false
  done

let construit d n =
  for i = n / 2 - 1 downto 0 do descendre d n i done

On percole chaque nœud interne (indices n/2 - 1 à 0), des feuilles vers la racine : quand on traite i, ses sous-arbres sont déjà des tas. L'analyse fine donne un coût total (et non ). Remarque : OCaml propose downto ; si l'on s'en tient au seul to, on parcourt i de 0 à n/2-1 et l'on descend n/2 - 1 - i.

Exercice 9 : File de priorité de couples

Pour Dijkstra, on a besoin d'une file de priorité de couples (distance, sommet) ordonnés par distance. Adapter le tas.

Démonstration

type tas_couples = { mutable taille : int; donnees : (int * int) array }

let cree_c capacite = { taille = 0; donnees = Array.make capacite (0, 0) }

let insere_c t x =
  let d = t.donnees in
  d.(t.taille) <- x; t.taille <- t.taille + 1;
  let i = ref (t.taille - 1) in
  while !i > 0 && fst d.((!i - 1) / 2) > fst d.(!i) do
    let p = (!i - 1) / 2 in
    let tmp = d.(!i) in d.(!i) <- d.(p); d.(p) <- tmp; i := p
  done

Tout est identique au tas d'entiers, mais on compare la première composante (fst, la distance) : le couple de plus petite distance remonte à la racine. extrait_min s'adapte de même (comparer par fst). On dispose alors d'une file de priorité « distance, sommet ».

Exercice 10 : Dijkstra avec file de priorité

Esquisser Dijkstra (chapitre 16) en utilisant la file de priorité de couples, et donner sa complexité.

Démonstration

let dijkstra_tas adj depart n_aretes =
  let n = Array.length adj in
  let infini = 1_000_000 in
  let dist = Array.make n infini in
  let t = cree_c (n + n_aretes) in       (* capacité : assez de poussées *)
  dist.(depart) <- 0;
  insere_c t (0, depart);
  while t.taille > 0 do
    let (du, u) = extrait_min_c t in
    if du = dist.(u) then               (* ignorer les entrées périmées *)
      List.iter (fun (v, poids) ->
        if du + poids < dist.(v) then begin
          dist.(v) <- du + poids;
          insere_c t (dist.(v), v)       (* on pousse la nouvelle distance *)
        end
      ) adj.(u)
  done;
  dist

Au lieu de chercher le minimum en , on l'extrait du tas en . À chaque relâchement réussi, on pousse le couple (nouvelle_distance, v) ; une même destination peut être poussée plusieurs fois, d'où le test du = dist.(u) qui ignore les entrées périmées. Coût — bien meilleur que le du chapitre 16 sur les graphes creux. (extrait_min_c est la version « couples » de extrait_min.)

Synthèse du chapitre (à retenir)
  • File de priorité : extraire toujours le plus prioritaire (ici, le minimum).
  • Tas binaire : arbre binaire presque complet rangé dans un tableau ; nœud i : parent (i-1)/2, fils 2i+1 et 2i+2. Propriété : tout nœud ses enfants ; minimum à la racine.
  • minimum en ; insere (percolation vers le haut) et extrait_min (percolation vers le bas) en .
  • Tri par tas : tout insérer puis tout extraire, . Construction d'un tas à partir d'un tableau quelconque en (percolations des nœuds internes vers la racine).
  • Application : Dijkstra avec file de priorité passe de à — décisif sur les graphes creux.

19.6 Exercices d'entraînement

Légende : application directe, raisonnement intermédiaire, approfondissement ; signale un classique incontournable. La numérotation prolonge celle des dix exercices résolus.

Thème A — Manipulations de base.
Thème B — Autour du tri.
Thème C — Variantes de file de priorité.
Thème D — Applications.

Continuer sur Adloun : animation, QCM, fiches, exercices