L'usage de la calculatrice et de tout dispositif électronique est interdit.
Cette épreuve concerne uniquement las candidats de la filière MPI.
L'énoncé de cette épreuve comporte 12 pages de texte.
Si, au cours de l'épreuve, un candidat repère ce qui lui semble être une erreur d'énoncé, il le signale sur sa copie et poursuit sa composition en expliquant les raisons des initiatives qu'il est amené à prendre.
Préliminaires
L'épreuve est composée d'un problème unique, comportant 36 questions. Le problème est divisé en quatre sections. Dans la première section (page 1), nous introduisons une forme paresseuse de listes, les flots de données, et construisons quelques fonctions de base utiles tout au long du sujet. Dans la deuxième section (page 4), nous nous intéressons à un algorithme de détermination à la volée du cardinal d'un flot, dont l'inspiration est probabiliste. Dans la troisième section (page 7), nous nous appuyons sur des raisonnements de logique pour analyser le chiffrement par flot. Dans la quatrième section (page 9), nous traitons quelques questions de combinatoire énumérative à l'aide d'une grammaire ou simplement d'un procédé manuel de dérécursification.
Les sections 2,3 et 4 peuvent être vues comme des exercices indépendants qui ne dépendent que de la première section.
Dans tout l'énoncé, un même identificateur écrit dans deux polices de caractères différentes désigne la même entité, mais du point de vue mathématique pour la police en italique (par exemple ) et du point de vue informatique pour celle en romain avec espacement fixe (par exemple n).
Des rappels portant sur des extraits du manuel de documentation de OCaml et sur les règles de la déduction naturelle sont fournis en annexe.
Travail attendu
Pour répondre à une question, il est permis de réutiliser le résultat d'une question antérieure, même sans avoir réussi à établir ce résultat.
Il faudra coder des fonctions à l'aide du langage de programmation OCaml exclusivement, en reprenant l'en-tête de fonctions fourni par le sujet, sans s'obliger à recopier la déclaration des types. Il est permis d'utiliser la totalité du langage OCaml mais il est recommandé de s'en tenir aux fonctions les plus courantes afin de rester compréhensible. Quand l'énoncé demande de coder une fonction, sauf demande explicite, il n'est pas nécessaire de justifier que celle-ci est correcte ou de tester que des préconditions sont satisfaites.
Le barème tient compte de la clarté et de la concision des programmes : nous recommandons de choisir des noms de variables intelligibles ou encore de structurer de longs codes par des blocs ou par des fonctions auxiliaires dont on décrit le rôle.
1. Flots de données
1.1. Motivation
En langage OCaml, le type natif 'a list est défini comme point fixe de l'équation
type 'a list = [] | (::) of 'a * 'a list
Ce type permet de définir des listes finies tout comme certaines listes infinies, ainsi que l'expose l'exemple suivant, qui est parfaitement licite :
let list_finite = [ 1; 2; 3 ] (* liste finie a 3 elements *)
let rec list_ones = 1 :: list_ones (* liste infinie toute a 1 *)
- Rendre compte, sous la forme d'un croquis constitué de maillons chaînés entre eux, de l'organisation en mémoire des variables list_finite et list_ones. Dire quelle région de la mémoire d'un programme est utilisée pour ce stockage.
Les termes de la suite de Fibonacci obéissent aux relations :
Nous nous enhardissons à définir la liste des termes de la suite de Fibonacci en écrivant la déclaration récursive suivante :
let list_fibo : int list =
let rec list_fibo_with_internal_state (a:int) (b:int) : int list =
a :: (list_fibo_with_internal_state b (a+b))
in
list_fibo_with_internal_state 0 1
Cependant, à l'évaluation de cette expression par un interpréteur (ou toplevel ou encore ), le système renvoie le message d'erreur suivant :
9. Stack overflow during evaluation (looping recursion?).
2 - Traduire en français le mot stack. Expliquer le message d'erreur en présentant le déroulement des premières étapes de création de la variable list_fibo par l'interpéteur OCaml.
Nous recopions le code fautif dans un fichier fibo.ml et — horresco referens — nous nous apprêtons à appeler successivement depuis un terminal les commandes ocamlopt fibo.ml -o fibo pour la compilation de la source vers un exécutable puis ./fibo pour l'exécution.
3 - Dire si l'une des deux étapes, compilation ou exécution, renvoie une erreur et, le cas échéant, préciser laquelle des étapes.
1.2. Définition des flots de données
Définition: Un flot de données est une suite, finie ou infinie, d'éléments de même type.
Indication OCaml : Afin de représenter les flots de données, nous fixons le type suivant :
type 'a stream = Nil | Cons of 'a * (unit -> 'a stream)
Le constructeur Nil qualifie le flot vide. Définir la suite infinie constante égale à 1 en tant que int stream demeure possible. Nous parvenons de surcroît à déclarer la suite de Fibonacci en tant que int stream sans encombre. En voici le code :
let rec (ones : int stream) = Cons (1, fun () -> ones)
let fibo : int stream =
let rec fibo_with_internal_state a b =
Cons (a, fun () -> fibo_with_internal_state b (a+b))
in
fibo_with_internal_state 0 1
- Écrire une constante OCaml integers : int stream dont la valeur de retour est le flot des entiers naturels énumérés par ordre croissant. - Écrire une fonction OCaml range (a:int) (b:int) : int stream dont la valeur de retour est le flot des entiers compris entre l'entier inclus et l'entier exclu énumérés par ordre croissant. - Soit une suite quelconque de chaînes de caractères. Dire s'il existe forcément un flot de donnée, de type string stream, qui encode la suite et, le cas échéant, en préciser la construction en langage OCaml ou en démontrer l'inexistence.
1.3. Quelques manipulations de base des flots de données
- Écrire une fonction OCaml hd (u : 'a stream) : 'a dont la valeur de retour est le premier élément du flot de données et qui lève une exception Failure "hd" si le flot est vide. - Écrire une fonction OCaml tl (u : 'a stream) : 'a stream dont la valeur de retour est le flot de données privé de l'élément de tête et qui lève une exception Failure "tl" si le flot est vide. - Écrire une fonction of_list (l : 'a list) : 'a stream dont la valeur de retour est le flot des éléments de la liste . - Écrire une fonction OCaml iter ( 'a unit) ( t : int) (u : 'a stream) : unit qui applique la fonction aux premières valeurs existantes du flot de données si l'entier est positif et ne fait rien sinon. - Écrire une fonction OCaml map (f : 'a -> 'b) (u : 'a stream) : 'b stream dont la valeur de retour est le flot constitué de l'application de la fonction aux éléments de .
12 - Écrire une fonction zip (w : 'a stream array) : 'a array stream qui transforme un tableau de flots de données en un flot de données tabulaire . Si l'un des flots de est vide à partir d'un certain rang, la valeur de retour est aussi vide à partir de ce rang.
Par exemple, zip [lones; range 0100 ; fibol] désigne le flot fini des tableaux d'entiers de longueur trois . - Écrire une fonction intertwin (u : 'a stream) (v : 'a stream) : 'a stream dont la valeur de retour est le flot obtenu en prenant alternativement des éléments du flot et du flot , en cessant de prendre des éléments d'un flot une fois que celui-ci est vide. Par exemple, intertwin ones fibo désigne le flot d'entiers , etc., tandis que intertwin (range 02 ) fibo désigne le flot d'entiers , etc.
Nous nous proposons d'écrire une fonction product (u1 : 'a stream) (u2 : 'b stream) : (' 'b) stream dont la valeur de retour est un flot qui énumère tous les couples tirés du flot et du flot dans un ordre non spécifié. Nous écrivons le code suivant :
let rec product_wrong u1 u2 =
match u1,u2 with
| _, Nil | Nil, _ -> Nil
| Cons(hd1, tl_begetter1), Cons(hd2, tl_begetter2) ->
let part1 = product_wrong (tl_begetter1 ()) u2 in
let part2 = map (fun y -> (hd1, y)) (tl_begetter2 ()) in
Cons((hd1, hd2), fun () -> intertwin part1 part2)
14 - Montrer que l'appel product_wrong u1 u2 ne se termine pas toujours mais qu'il suffit de déplacer quelques mots au sein du code pour le rendre correct.
1.4. Filtre d'un flot
- À titre préliminaire, rappeler brièvement la signification de l'entier 0 dans l'instruction return 0 qui termine usuellement la fonction main en langage C ou dans l'instruction exit 0 du langage OCaml.
16 - Énoncer le théorème de Turing relatif au problème de l'arrêt.
Pour répondre à la question 17, on pourra admettre sans justification l'existence d'une machine universelle à chronomètre, c'est-à-dire une fonction OCaml run : string -> int -> int qui se termine toujours et telle que:
si la chaîne de caractères est le code source d'une expression OCaml et si l'exécution du code se termine sans erreur en moins de étapes élémentaires de calcul, alors run x n a pour valeur de retour l'entier 0 ;
sinon, run x n a pour valeur de retour l'entier 1.
17 - Décrire la construction ou bien réfuter l'existence d'une fonction OCaml filter (f : 'a -> bool) (u : 'a stream) -> 'a stream dont la valeur de retour est formé des éléments du flot satisfaisant le prédicat , l'ordre des éléments de étant préservé. Par exemple, let is_negative in filter is_negative ones vaut Nil.
2. Estimation du cardinal d'un flot de données
Définition : Nous appelons cardinal d'un flot de données le cardinal de l'ensemble des valeurs distinctes appartenant à ce flot. Ce cardinal peut être fini même si le flot est infini.
Dans cette section, nous cherchons à estimer le cardinal d'un flot de chaînes de caractères de cardinal fini avec peu de mémoire et en supposant que .
Définition : Nous appelons premier bit activé d'un entier , et notons , le plus grand entier tel que les bits les plus à droite dans la représentation binaire de sont tous nuls. Par convention, nous fixons .
Par exemple, nous avons , ou encore .
18 - Écrire une fonction OCaml ffs (h:int) : int dont la valeur de retour est le premier bit activé de l'entier . En anglais, ffs désigne find first set.
Indication OCaml : Nous disposons d'une fonction de hachage à valeur dans l'intervalle entier que nous réalisons à l'aide de String.hash : string -> int.
Soit un entier naturel non nul. Nous tirons chaînes de caractères aléatoirement et notons leur haché par la fonction de hachage . Pour tout indice compris entre 1 et , nous supposons que chaque variable aléatoire est distribuée selon une loi uniforme dans l'intervalle entier . Nous posons
Une analyse mathématique, que nous admettons, montre alors que les variables aléatoires sont tirées selon des lois géométriques et que la variable est proche de , si bien que la quantité est une bonne approximation de .
Soit un flot de chaînes de caractères de cardinal , l'entier étant inconnu. Nous proposons deux algorithmes qui estiment le cardinal par l'observation des premiers termes du flot, en supposant que les valeurs distinctes apparaissent parmi les premiers termes.
Algorithme naïf : garnir à la volée une table de hachage avec les chaînes , puis renvoyer le compte des éléments présents dans la table. En voici le code :
let cardinality_nf (t:int) (u: string stream) : float =
let htb = Hashtbl.create 1 in
iter (fun x-> Hashtbl.add htb x ()) t u;
float_of_int (Hashtbl.length htb)
où l'on réutilise la fonction iter de la question 10 et des fonctions du module Hashtbl rappelées en annexe.
Algorithme de Flajolet-Martin, inspiré par les observations ci-dessus. En voici le code :
let cardinality_fm (t:int) (u: string stream) : float =
let r = ref 0 in
let update x = r:= max !r (ffs (String.hash x)) in
iter update t u;
float_of_int (1 lsl !r) (* calcule 2^r en flottant *)
19 - Comparer la complexité en espace des algorithmes cardinality_nf et cardinality_fm.
Nous rappelons que la moyenne harmonique de réels strictement positifs se calcule par la formule
- Écrire une fonction OCaml harmonic_mean (z : float array) : float dont la valeur de retour est la moyenne harmonique des termes du tableau .
La suite de chaînes de caractères et l'entier étant fixés, pour tout entier compris entre 0 et et pour tout entier naturel , nous notons l'ensemble de chaînes de caractères
ù
Afin d'obtenir une estimation de cardinal du flot plus robuste et de diminuer la variabilité associée à l'unicité du point de mesure, nous proposons une amélioration de la fonction cardinality_fm qui utilise un effet de moyenne.
Algorithme HyperLogLog : Nous divisons les premiers termes du flot en sous-flots disjoints . Nous estimons le cardinal de chaque sous-flot comme le fait l'algorithme de Flajolet-Martin et obtenons des valeurs , où
qui, intuitivement, forment chacune une estimation grossière du quotient . Nous calculons la moyenne harmonique , qui, intuitivement, constitue une estimation robuste du quotient .
Une analyse mathématique poussée confirme notre intuition. Il s'avère que le cardinal est très bien approché par la quantité
où est une constante, valant ici . On utilisera l'équation (%) sans la démontrer durant l'épreuve.
Indication OCaml : Nous fixons les constantes globales pour le reste de la section 2.
let m = 16
let alpha16 = 0.6731 (* calcul numerique admis *)
L'algorithme HyperLogLog se présente alors sous la forme
let cardinality_hll (t:int) (u : string stream) : float =
let r = Array.make m 0 in
iter (hll_update r) t u;
mean_estimate r
où les fonctions hll_update et mean_estimate seront écrites aux questions 21 et 22.
21 - Écrire une fonction OCaml hll_update (r : int array) (x : string) : unit ainsi spécifiée :
Précondition : Il existe un flot de chaînes de caractères , des entiers et tels que et le tableau contient .
Postcondition : Le tableau contient . - Écrire une fonction OCaml mean_estimate ( : int array) : float ainsi spécifiée Précondition : Le tableau contient les valeurs avec
pour un certain flot de chaînes de caractères et un certain entier .
Valeur de retour : Estimation du cardinal du flot d'après l'équation (%).
Nous nous plaçons finalement dans la situation où deux flots de chaînes de caractères et nous parviennent simultanément. Nous chargeons deux fils d'exécution auxiliaires distincts de construire indéfiniment leurs tableaux respectifs de maxima de bit activés tandis que le fil d'exécution principal déclenche toute les cinq secondes l'affichage du cardinal. Voici une ébauche de code :
let cardinality_threadedhll u v =
let r_u = Array.make m 0 in
let f1 () = iter (hll_update r_u) Int.max_int u in
let _ = Thread.create f1 () in
let r_v = Array.make m 0 in
let f2 () = iter (hll_update r_v) Int.max_int v in
let _ = Thread.create f2 () in
while true do
let r = Array.init m (fun i -> max r_u.(i) r_v.(i)) in
print_float (mean_estimate r);
Thread.delay 5. (* mise en pause de 5 secondes *)
done
dans laquelle Int.max_int représente le plus grand entier représentable. - Identifier les portions de code sujettes à des courses critiques. Indiquer un mécanisme qui permet de corriger l'ébauche de code.
3. Chiffrement par flot de booléens
Dans cette partie, nous nous appuyons sur des variables booléennes et sur les constantes (tautologie) et (antilogie) pour former des formules, à l'aide des trois connecteurs (négation), (conjonction) et (disjonction). Pour toutes formules de logique et , nous notons la nouvelle formule
Les règles d'inférence de la déduction naturelle sont rappelées dans l'annexe B .
24 - Soit une formule. Construire des arbres de preuve qui démontrent les séquents
à partir des règles d'inférence de la déduction naturelle.
25 - Soit une formule. Construire des arbres de preuve qui démontrent les séquents
Nous admettons que, pour toutes formules de logique et , il existe des arbres de preuve qui démontrent les séquents et .
Définitions : Nous notons l'ensemble des valeurs de vérité. Nous introduisons la fonction booléenne telle que est l'évaluation de la formule . - Justifier que les fonctions booléennes suivantes et sont égales.
Nous admettons, plus généralement, que la fonction booléenne est commutative et associative. Pour tout entier et pour tous booléens , la somme est définie comme mais peut être calculée dans n'importe quel ordre. La somme vide vaut, par convention, la valeur de vérité .
Définition : Nous appelons registre à décalage à rétroaction linéaire (ou encore LFSR d'après l'anglais linear feedback shift register) de longueur , de germe et de connexion , le flot de booléens défini par la relation de récurrence
Indication OCaml : Nous utilisons le type bool array pour représenter le germe et le type int list pour représenter la connexion d'un LFSR. La longueur s'obtient avec Array. length. - Écrire une fonction OCaml lfsr (g:bool array) (c:int list) : bool stream dont la valeur de retour est le flot de données constitué par le LFSR de germe et de connexion .
Indication OCaml : Nous représentons les formules de logique à l'aide du type
type formula = | Var of int
| Not of formula
| And of formula * formula
| Or of formula * formula
Pour toute formule de logique définie à partir des variables booléennes et pour tout -upplet de valeurs de vérité , nous notons l'évaluation de la formule en .
28 - Écrire une fonction OCaml logical_map (phi : formula) (w : bool stream array) : bool stream ainsi spécifiée :
Précondition : La formule ne dépend que des variables . Le tableau est de longueur . Nous notons son contenu.
Valeur de retour : Flot des évaluations booléennes .
Dans la question 29, deux personnages, Alice et Bob, se sont accordés, lors de leur tout premier tête-à-tête et à l'abri de toute écoute par un tiers, au sujet de germes et de connexions de LFSR ainsi qu'au sujet d'une formule de logique . Nous notons les flots de données booléens associés aux LFRS. À présent, Alice souhaite communiquer à Bob la suite de bits via un canal public. Elle transmet à Bob la suite des évaluations . - Dire si Bob peut toujours parvenir à reconstituer le flot et, le cas échéant, expliquer comment.
4. Énumération d'objets combinatoires
4.1. Flot des mots de Dyck
Soit l'alphabet binaire . Nous nous intéressons à la constante OCaml dyck définie par le code suivant
let rec dyck =
let f (x,y) = "a" ^ x ^ "b" ~ y in
Cons("", fun () -> map f (product dyck dyck))
où la fonction product a été introduite à la question 14. Nous notons le langage constitué des mots appartenant au flot de données dyck. - Exhiber une grammaire non contextuelle telle que le langage engendré par la grammaire coïncide avec le langage .
31 - Énoncer une propriété de la grammaire de la question 30 qui permet d'établir que le flot de chaînes de caractères dyck n'énumère jamais deux fois le même mot. La démontrer.
4.2. Flot des permutations d'un tableau
Nous partageons une variante d'un algorithme dû à B. R. Heap sous la forme du pseudocode suivant. Dans ce pseudo-code, les positions de tableau sont numérotées à partir de l'indice 0 .
Algorithme 1 : Algorithme de B. R. Heap
Entrées : Entier $k$, tableau $t$ de longueur $\geqslant k$.
Effet : Effectue des échanges en place dans le tableau $t$ et des
affichages de $t$.
Sortie : Aucune valeur de retour.
si $k=1$ alors
Afficher le tableau $t$.
sinon
pour $i=0$ à $k-1$ (inclus) faire
Exécuter Heap $((k-1), t)$.
si $k$ est pair alors
Échanger $t[i]$ et $t[k-1]$
sinon
Échanger $t[0]$ et $t[k-1]$
32 - Soient un entier et un tableau de longueur supérieure à . Dénombrer le nombre d'appels à la fonction «afficher» lorsque l'on exécute Heap .
33 - Soient un entier et un tableau de longueur supérieure à . Démontrer, pour tout entier naturel non nul , la proposition suivante:
Après l'exécution de Heap , si est un entier impair, alors le tableau est inchangé; en revanche, si est un entier pair, alors les premiers coefficients du tableau ont subi une permutation circulaire vers la droite : autrement dit, le nouvel état du tableau est
34 - Soient un entier et un tableau de longueur supérieure à . Démontrer que l'exécution de affiche, pour toute permutation des premiers alvéoles du tableau , le tableau dans lequel les premiers alvéoles ont été permutés et dans lequel les derniers alvéoles ont été laissés inchangés.
35 - Écrire une fonction OCaml pivot (k:int) (i:int) (t:'a array) : unit dont l'effet est d'appliquer au tableau les opérations des lignes 6 à 9 de l'algorithme 1 .
36 - Écrire une fonction OCaml permstream_of_array ( :'a array) : 'a array stream dont la valeur de retour est un flot de données fini constitué de toutes les permutations du tableau énumérées dans l'ordre d'affichage de l'algorithme de Heap et qui utilise efficacement la mémoire.
Opérations générales : Le module Stdlib, chargé par défaut, offre les fonctions suivantes :
max : 'a -> 'a -> 'a
Return the greater of the two arguments. The result is unspecified if one of the arguments contains the float value nan.
float_of_int : int -> float
Convert an integer to floating-point.
(lsl) : int -> int -> int
n lsl m shifts to the left by bits.
Opérations sur les tableaux : Le module Array offre les fonctions suivantes :
make : int -> 'a -> 'a array
make n x returns a fresh array of length , initialized with .
init : int -> (int -> 'a) -> 'a array
init n f returns a fresh array of length , with element number initialized to the result of .
exists : ('a -> bool) -> 'a array -> bool
exists f [|a1; ...; an |] checks if at least one element of the array satisfies the predicate. That is, it returns (f a1) || (f a2) || ... || (f an).
Opérations sur les tables de hachage : Le module Hashtbl offre les fonctions suivantes :
create : int -> ('a, 'b)
Hashtbl. create n creates a new, empty hash table, with initial size .
add : ('a, 'b) t -> 'a -> 'b -> unit
Hashtbl. add adds a binding of key to data in table .
length : ('a, 'b) t -> int
Hashtbl.length tt returns the number of bindings in . It takes constant time. Multiple bindings are counted once each.
Opérations sur les fils d'exécution : Le module Thread offre les fonctions suivantes :
create : ('a -> 'b) -> 'a -> t
Thread. create f x creates a new thread of control, in which the function application is executed concurrently with the other threads of the domain. The application of Thread.create returns the handle of the newly created thread.
delay : float -> unit
delay d suspends the execution of the calling thread for seconds. The other program threads continue to run during this time.
Opérations sur les verrous : Le module Mutex offre les fonctions suivantes :
create : unit -> t
Return a new mutex.
lock : t -> unit
Lock the given mutex. Only one thread can have the mutex locked at any time. A thread that attempts to lock a mutex already locked by another thread will suspend until the other thread unlocks the mutex.
unlock : t -> unit
Unlock the given mutex. Other threads suspended trying to lock the mutex will restart. The mutex must have been previously locked by the thread that calls Mutex.unlock.
B. Annexe : règles de la déduction naturelle
Dans les tableaux suivants, la lettre désigne un ensemble de formules de logique; les lettres et désignent des formules de logique.
Axiome
Introduction
Élimination
T
V
Fin de l'épreuve
Les sujets sont la propriété du GIP CCMP. Ils sont publiés sous les termes de la licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Pas de Modification 3.0 France.
Tout autre usage est soumis à une autorisation préalable du Concours commun Mines Ponts.
Mines Informatique 2 MPI 2024 - Version Web LaTeX | WikiPrépa | WikiPrépa