Probleme – Engendrer toutes les parties d'un ensemble
Exercice · niveau 3 (difficile) · informatique (MP2I/MPI), chapitre 5 — Récursivité
Énoncé
- Écrire
parties : 'a list -> 'a list listqui rend la liste de toutes les sous-listes d'une liste sans répétition. - Prouver la correction par récurrence, et compter les parties produites.
- Quelle est la place occupée par le résultat ? Et la profondeur de pile ?
- Comment énumérer les parties sans les stocker toutes ?
Corrigé
1. La fonction.
(* parties l renvoie la liste de toutes les sous-listes de l, l'ordre
des elements etant conserve dans chacune.
Precondition : aucune. Postcondition : le resultat a 2^|l| elements. *)
let rec parties = function
| [] -> [[]] (* l'ensemble vide a UNE partie : lui-meme *)
| x :: r ->
let p = parties r in
List.map (fun s -> x :: s) p @ p (* avec x, puis sans x *)
Mesuré : parties [1;2;3] rend les huit listes [1;2;3], [1;2], [1;3], [1], [2;3], [2], [3], [].
Le cas de base est le piège de l'exercice. Écrire [] -> [] donnerait la liste vide de parties, et par propagation la fonction rendrait toujours []. L'ensemble vide a exactement une partie, l'ensemble vide : le cas de base est [[]], une liste contenant une liste vide.
2. Correction et compte. Par récurrence sur . Pour vide, la seule partie est . Pour , toute partie de contient ou ne le contient pas ; celles qui ne le contiennent pas sont exactement les parties de ; celles qui le contiennent sont exactement les pour partie de . La réunion est donc complète et sans doublon, et
Mesuré : parties pour , et pour .
3. La place et la pile. La somme des tailles des parties est
mesuré : pour et pour , conformes. Le résultat occupe donc maillons — mais le partage de queue en économise une partie, puisque @ p réutilise tel quel.
La profondeur de pile, elle, n'est que : un seul appel récursif par niveau, niveaux. Encore une fois, temps exponentiel et espace de pile linéaire.
4. Énumérer sans stocker. On passe une fonction à appliquer à chaque partie, et l'on construit la partie courante dans un accumulateur :
(* pour_chaque_partie f l applique f a chaque sous-liste de l.
Espace : Theta(n) au lieu de Theta(n 2^n). *)
let pour_chaque_partie f l =
let rec aux acc = function
| [] -> f (List.rev acc)
| x :: r -> aux (x :: acc) r; aux acc r
in aux [] l
Vérifié : elle énumère bien parties, pour et pour , dans le même ordre que la précédente. Le gain est massif : pour , la première version demanderait maillons, soit plus de milliards de maillons, tandis que celle-ci se contente de cases de pile — le temps reste , incompressible puisqu'il y a objets à produire.
C'est le schéma du retour sur trace, qui structurera l'exploration du chapitre chap:exploration : à chaque étape, un choix binaire ; on descend, on remonte, on essaie l'autre branche.
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.