Probleme – L'aller-retour de la sérialisation d'un arbre
Exercice · niveau 3 (difficile) · informatique (MP2I/MPI), chapitre 12 — Tableaux associatifs, hachage et sérialisation
Énoncé
- Écrire la désérialisation, et détecter les entrées mal formées.
- Démontrer que la sérialisation du chapitre est injective, donc que l'aller-retour est l'identité.
- Le
#est-il toujours nécessaire ? Étudier le cas d'un arbre binaire de recherche.
Corrigé
1. La désérialisation. Elle suit la structure de l'écriture : un jeton # rend , un nombre lit son fils gauche puis son fils droit.
exception Format_invalide
(* Reconstruit l'arbre écrit par serialise. Lève Format_invalide si la chaîne
ne code aucun arbre, ou en code un suivi de jetons superflus.
Complexité : Theta(nombre de jetons) = Theta(2n+1). *)
let deserialise s =
let reste = ref (List.filter (fun j -> j <> "")
(String.split_on_char ' ' s)) in
let suivant () = match !reste with
| [] -> raise Format_invalide (* la chaîne s'arrête trop tôt *)
| j :: r -> reste := r; j in
let rec lire () =
let j = suivant () in
if j = "#" then Vide
else
let e = int_of_string j in
let g = lire () in (* l'ORDRE compte : gauche AVANT droit *)
let d = lire () in
Noeud (g, e, d) in
let a = lire () in
if !reste <> [] then raise Format_invalide (* des jetons en trop *)
else a
Les deux contrôles ne sont pas décoratifs. Sans le premier, une chaîne tronquée provoquerait une exception obscure ; sans le second, "# 5 # #" serait acceptée en ne lisant que son premier jeton, et l'on perdrait silencieusement le reste. Un désérialiseur lit des données venues de l'extérieur : il doit refuser, pas deviner.
Mesuré : sur l'arbre du chapitre chap:arbres, deserialise (serialise a) = a rend true, et la chaîne compte jetons pour nœuds.
2. L'injectivité. On la démontre par un argument de code préfixe, plus fort et plus utile que l'induction directe.
Associons à chaque jeton un poids : pour une étiquette, pour un #. Posons et : compte les sous-arbres qu'il reste à écrire après jetons. Une étiquette consomme une place et en crée deux ( net) ; un # en consomme une ().
Affirmation : une suite de jetons est la sérialisation d'un arbre si et seulement si pour tout strictement inférieur à la longueur, et . La démonstration se fait par induction structurelle : la suite d'un arbre vide est #, avec ; celle de est la concaténation de , de celle de et de celle de , et le compteur y passe de à puis descend à à la fin de , puis à à la fin de — sans jamais s'annuler avant.
Conséquence immédiate : aucun préfixe strict d'une sérialisation n'est lui-même une sérialisation, puisque ne s'annule qu'à la fin. La lecture est donc déterministe — à chaque instant, un seul découpage est possible — et l'écriture est injective. Vérifié par programme : sur arbres tirés au hasard, le compteur reste strictement positif avant la fin et s'annule exactement à la fin ; et sur préfixes stricts, aucun n'est une sérialisation valide.
Ce compteur donne aussi le comptage de jetons : part de , monte de et descend de pour finir à , d'où jetons.
3. Le cas de l'arbre binaire de recherche. Ici le # devient inutile : le parcours préfixe seul détermine l'arbre.
Pourquoi. Soit le parcours préfixe d'un ABR. La racine est . Les étiquettes du sous-arbre gauche sont toutes , celles du droit toutes , et le préfixe visite le gauche entièrement avant le droit. Donc le sous-arbre gauche est exactement le préfixe des inférieurs à , et le droit le reste. La coupure est déterminée par les valeurs, là où l'arbre quelconque devait l'écrire.
(* Reconstruit un ABR à partir de son SEUL parcours préfixe.
Précondition : l est le parcours préfixe d'un ABR dont les étiquettes sont
deux à deux distinctes, et distinctes de min_int et de max_int.
Complexité : O(n) — chaque jeton est consommé une fois. *)
let abr_de_prefixe l =
let reste = ref l in
let rec lire bmin bmax =
match !reste with
| e :: r when bmin < e && e < bmax -> (* e appartient à CE sous-arbre *)
reste := r;
let g = lire bmin e in
let d = lire e bmax in
Noeud (g, e, d)
| _ -> Vide (* hors bornes : le sous-arbre finit *)
in lire min_int max_int
Vérification exhaustive : pour de à , on engendre tous les ABR sur les clés — leur nombre est le -ième nombre de Catalan, mesuré — et l'on contrôle que leurs parcours préfixes sont deux à deux distincts et que abr_de_prefixe redonne l'arbre. Les deux propriétés tiennent dans tous les cas.
Le gain, et pourquoi il ne change pas la règle. Le format passe de jetons à : plus de moitié économisée. Mais il ne vaut que pour un ABR à clés distinctes, et il exige de la lecture qu'elle connaisse cette hypothèse. Un format compact est un format qui délègue de l'information à la convention — ce qui est un gain tant que les deux bouts partagent la convention, et une catastrophe silencieuse dès qu'ils divergent. La règle du chapitre reste donc la bonne par défaut : écrire la forme.
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.