Probleme – Unir \& trouver avec annulation
Exercice · niveau 3 (difficile) · informatique (MP2I/MPI), chapitre 23 — Unir & trouver, arbres couvrants
Énoncé
La séparation et évaluation du chapitre chap:probabilistes explore un arbre de choix : on descend dans une branche, puis on revient. Si la structure unir & trouver sert à l'élagage, il faut savoir défaire une union.
- Montrer par une mesure que la compression de chemin rend l'annulation impossible.
- Écrire une variante qui sait annuler, et donner sa complexité.
- Que coûte-t-on, et que gagne-t-on ?
Corrigé
1. La compression détruit ce qu'il faudrait remettre. Reprenons la forêt de l'exercice « Union par rang : dérouler », obtenue par sept unions en tournoi sur huit éléments :
parent = [| 0; 0; 0; 2; 0; 4; 4; 6 |] rang = [| 3; 0; 1; 0; 2; 0; 1; 0 |]
Un trouver(7) avec compression modifie deux cases — et pointent désormais sur — sans qu'aucun historique d'unions n'en garde trace. Si l'on annule ensuite la dernière union, celle de et , on obtient (mesuré) :
parent = [| 0; 0; 0; 2; 4; 4; 0; 0 |] classes = 2
Le compteur annonce classes, et il y en a bien deux — mais ce ne sont pas les bonnes : et ont atterri dans la classe de alors qu'ils appartiennent à celle de . La structure est corrompue en silence, et le compteur, lui, reste juste : le contrôle le plus naturel ne voit rien.
2. La variante avec historique. On renonce à la compression, et l'on empile ce qu'il faut pour revenir.
(* Unir & trouver avec annulation. Union par rang, SANS compression.
hist retient, pour chaque union effective : (fils accroche, nouveau pere,
le rang du pere a-t-il augmente ?). *)
type ufa = { p : int array; r : int array;
mutable classes : int;
mutable hist : (int * int * bool) list }
let creer n = { p = Array.init n (fun i -> i); r = Array.make n 0;
classes = n; hist = [] }
(* Racine de x. PAS de compression : on ne modifie rien. Cout : O(log n). *)
let rec trouver u x = if u.p.(x) = x then x else trouver u u.p.(x)
let unir u x y =
let a = trouver u x and b = trouver u y in
if a = b then false
else begin
let a, b = if u.r.(a) < u.r.(b) then (b, a) else (a, b) in
let monte = (u.r.(a) = u.r.(b)) in
u.p.(b) <- a;
if monte then u.r.(a) <- u.r.(a) + 1;
u.classes <- u.classes - 1;
u.hist <- (b, a, monte) :: u.hist;
true
end
(* Defait la derniere union effective. Precondition : hist non vide. *)
let annuler u = match u.hist with
| [] -> failwith "rien a annuler"
| (b, a, monte) :: reste ->
u.p.(b) <- b; (* b redevient racine *)
if monte then u.r.(a) <- u.r.(a) - 1;
u.classes <- u.classes + 1;
u.hist <- reste
Correction de annuler : une union effective ne modifie que deux cases — p.(b) et, conditionnellement, r.(a) — et l'historique retient exactement ces deux informations. Comme aucune autre opération n'écrit dans les tableaux (c'est tout l'intérêt d'avoir supprimé la compression), remettre ces deux cases rétablit l'état antérieur, à l'identique.
Vérification, sur cinq éléments : après unir(0,1), unir(2,3) et unir(1,3), puis trois annuler, on retrouve exactement l'état de départ.
depart : p = [|0;1;2;3;4|] r = [|0;0;0;0;0|] classes = 5
2 unions : p = [|0;0;2;2;4|] r = [|1;0;1;0;0|] classes = 3
3e union : p = [|0;0;0;2;4|] r = [|2;0;1;0;0|] classes = 2
tout defait : p = [|0;1;2;3;4|] r = [|0;0;0;0;0|] classes = 5 <-- identique
Complexité : annuler est en ; trouver et unir sont en au pire, par la proposition du chapitre — et cette borne, elle, ne dépendait pas de la compression.
3. L'arbitrage. On perd le coût amorti quasi constant : on passe de à , mesuré plus haut à sauts par opération contre . On gagne la possibilité de revenir en arrière en , sans recopier la structure — ce qui, dans un parcours d'arbre de recherche à nœuds, est la différence entre praticable et impossible.
Et c'est un cas d'école du chapitre chap:abstraction : la même signature, deux réalisations, deux profils de coût. L'appelant choisit selon qu'il a besoin ou non de défaire.
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.