(* This OCaml script was exported from a Jupyter notebook using an open-source software (under the MIT License) written by @Naereen from https://github.com/Naereen/Jupyter-Notebook-OCaml This software is still in development, please notify me of a bug at https://github.com/Naereen/Jupyter-Notebook-OCaml/issues/new if you find one *) (* # Table of Contents
*) (* # Texte d'oral de modélisation - Agrégation Option Informatique ## Préparation à l'agrégation - ENS de Rennes, 2016-17 - *Date* : 09 mai 2017 - *Auteur* : [Lilian Besson](https://GitHub.com/Naereen/notebooks/) - *Texte*: Annale 2012, ["Mots bien formés"](http://agreg.org/Textes/public2012-D6.pdf) *) (* ## À propos de ce document - Ceci est une *proposition* de correction, partielle et probablement non-optimale, pour la partie implémentation d'un [texte d'annale de l'agrégation de mathématiques, option informatique](http://Agreg.org/Textes/). - Ce document est un [notebook Jupyter](https://www.Jupyter.org/), et [est open-source sous Licence MIT sur GitHub](https://github.com/Naereen/notebooks/tree/master/agreg/), comme les autres solutions de textes de modélisation que [j](https://GitHub.com/Naereen)'ai écrite cette année. - L'implémentation sera faite en OCaml, version 4+ : *) (* In[1]: *) Sys.command "ocaml -version";; (* ---- ## Question de programmation La question de programmation pour ce texte était donnée à la fin, en page 7 : > « Programmer la reconnaissance des mots bien formés en écriture préfixe sur la signature de l’exemple du texte à l’aide du critère fourni par le théorème 3 page 5. Il est conseillé de représenter le mot à valider sous forme d’un tableau ou d’une liste de caractères. » Mathématiquement, l'énoncé à implémenter sous forme d'un critère est le suivant : ### Théorème 3 > Pour que la suite de symboles $s_1,\dots,s_n \in \Omega$ soit l'écriture *préfixe* d'un terme, il faut et il suffit que les $h_p := \sum_{i=1}^{p} (1 - \alpha(s_i))$ vérifient : > $$ h_1,\dots,h_{n-1} \leq 0 \; \text{et} \; h_n = 1. $$ ### Choix d'implémentation - Ce critère numérique va être très simple à implémenter. - Le choix des structures de données à utiliser n'est cependant pas évident. + Le sujet suggérait d'utiliser un tableau ou une liste de caractères pour représenter la "suite de symboles" $s_1,\dots,s_n \in \Omega$ + On va définir un type formel pour l'alphabet $\Omega$, avec l'exemple du texte, $\Omega = \{V, F, \mathrm{Non}, \mathrm{Ou}, \mathrm{Et}, \mathrm{ITE}\}$. + On doit ensuite représenter l'arité, sous forme de tableau, table d'association ou de hashage, ou une fonction, qui permette d'associer un entier $\alpha(s)$ à chaque symbole $s$. Par simplicité, on choisit d'utiliser une fonction : $\alpha : \Omega \to \mathbb{N}, s \mapsto \alpha(s)$. + Enfin, la fonction demandée sera récursive, et très rapide à écrire. *) (* ---- ## Réponse à l'exercice requis *) (* ### Symboles et arités On définit : - les symboles du calcul propositionnel par un type énumération (abstrait) : $$ \Omega = \{V, F, \mathrm{Non}, \mathrm{Ou}, \mathrm{Et}, \mathrm{ITE}\} $$ $\mathrm{ITE}$ correspond au "If then else" ternaire (`a ? b : c` en C, `if a then b else c` en OCaml, ou `b if a else c` en Python). - Et les formules du calcul propositionnel comme une *liste* de symboles. *) (* In[2]: *) type symbole_calcul_prop = F | V | Ou | Et | Non | ITE ;; type formule_calcul_prop = symbole_calcul_prop list ;; (* Quelques exemples de formules du calcul propositionnel, écrites sous forme préfixes, bien formées ou non : - "$\mathrm{Ou} \; \mathrm{Non} \; \mathrm{Ou} \; F \; V \; \mathrm{Ou} \; \mathrm{Non} \; V \; F"$ $\equiv (\neg(F \vee V)) \vee ((\neg V) \vee F)$ est bien formé, - "$\mathrm{Ou} \; \mathrm{Non} \; F \; \mathrm{Non}$" est mal formé, - "$\mathrm{ITE} \; V \; F \; V$" un exemple de "If then else" bien formé, - "$\;$" la formule vide. *) (* In[3]: *) let ex_correct = [Ou; Non; Ou; F; V; Ou; Non; V; F];; let ex_incorrect = [Ou; Non; F; Non];; let ex_ite = [ITE; V; F; V];; let ex_vide = [];; (* Cette fonction donne l'arité du symbole du calcul propositionnel pris en argument. - $V$ et $F$ ont arités $\alpha = 0$, - $\mathrm{Non}$ a arité $\alpha = 1$, - $\mathrm{Et}$ et $\mathrm{Ou}$ ont arités $\alpha = 2$, - $\mathrm{ITE}$ a arité $\alpha = 3$. Autrement dit, avec les notations du texte : $$ S = \Omega = S_0 \sqcup S_1 \sqcup S_2 \sqcup S_3 \\ \begin{cases} S_0 &= \{ V, F \}, \\ S_1 &= \{ \mathrm{Non} \}, \\ S_2 &= \{ \mathrm{Et}, \mathrm{Ou} \}, \\ S_3 &= \{ \mathrm{ITE} \}. \end{cases} $$ > Notez qu'on peut supposer que l'appel à cette fonction d'arité se fait en $\mathcal{O}(1)$ dans les calculs de complexité, puisqu'après compilation, cette fonction sera une simple lecture d'un tableau. *) (* In[4]: *) let arite_calcul_prop (s : symbole_calcul_prop) : int = match s with | F -> 0 | V -> 0 | Non -> 1 | Ou -> 2 | Et -> 2 | ITE -> 3 ;; (* ### Vérification de l'écriture préfixe Cette fonction prend en argument une liste de symboles `l` et une fonction d'arité sur ces symboles et renvoie "vrai" (`true`) si `l` est l'écriture préfixe d'un terme et "faux" sinon (`false`). Notez que le "et" en Caml est paresseux, i.e., `x && y` n'évalue pas `y` si `x` est `false`. Donc cette fonction peut s'arrêter avant d'avoir parcouru tous les symboles, dès qu'elle trouve un symbole qui contredit le critère sur les hauteurs $h_p$. Et cela permet aussi d'obtenir une indication sur le premier symbole à contredire le critère, comme implémenté ensuite. *) (* In[5]: *) let ecriture_prefixe_valide (l : 'a list) (arite : 'a -> int) : bool = let rec aux (l : 'a list) (h : int) : bool = match l with | [] -> false | [t] -> ((h + 1 - (arite t)) = 1) | t :: q -> let h_suivant = (h + 1 - (arite t)) in (h_suivant <= 0) && (aux q h_suivant) in aux l 0 ;; (* On vérifie tout de suite sur les 4 exemples définis ci-dessus : *) (* In[6]: *) let _ = ecriture_prefixe_valide ex_correct arite_calcul_prop;; (* true *) let _ = ecriture_prefixe_valide ex_incorrect arite_calcul_prop;; (* false *) let _ = ecriture_prefixe_valide ex_ite arite_calcul_prop;; (* true *) let _ = ecriture_prefixe_valide ex_vide arite_calcul_prop;; (* false *);; (* ### Vérification et localisation de la première erreur Avec la remarque précédente, on peut écrire une fonction très similaire, mais qui donnera une indication sur la position (i.e., l'indice) du premier symbole qui fait que le mot n'est pas bien équilibré, si le mot n'est pas bien formé. Si le mot est bien formé, `None` est renvoyé. *) (* In[7]: *) let ecriture_prefixe_valide_info (l : 'a list) (arite : 'a -> int) : int option = let rec aux (l : 'a list) (compteur : int) (h : int) : int option = match l with | [] -> Some compteur | [t] -> if (h + 1 - (arite t)) = 1 (* h = arite(t) *) then None else Some compteur | t :: q -> (* h est l'accumulateur qui contient la somme des 1 - arite(t) *) let h_suivant = (h + 1 - (arite t)) in if h_suivant > 0 then Some compteur else aux q (compteur + 1) h_suivant in aux l 0 0 ;; (* On vérifie tout de suite sur les 4 exemples définis ci-dessus : *) (* In[8]: *) let _ = ecriture_prefixe_valide_info ex_correct arite_calcul_prop;; (* None *) let _ = ecriture_prefixe_valide_info ex_incorrect arite_calcul_prop;; (* Some 3 *) let _ = ecriture_prefixe_valide_info ex_ite arite_calcul_prop;; (* None *) let _ = ecriture_prefixe_valide_info ex_vide arite_calcul_prop;; (* Some 0 *);; (* Cela permet de voir que sur la formule "$\mathrm{Ou} \; \mathrm{Non} \; F \; \mathrm{Non}$", le premier symbole à poser problème est le dernier symbole. Et effectivement, le critère est vérifié jusqu'au dernier symbole $\mathrm{Non}$. *) (* ### Exemples Avec les mêmes exemples : *) (* In[9]: *) let _ = ecriture_prefixe_valide ex_correct arite_calcul_prop;; (* true *) let _ = ecriture_prefixe_valide ex_incorrect arite_calcul_prop;; (* false *) let _ = ecriture_prefixe_valide ex_ite arite_calcul_prop;; (* true *) let _ = ecriture_prefixe_valide ex_vide arite_calcul_prop;; (* false *);; (* On peut aussi transformer un peu la deuxième formule pour la rendre valide. *) (* In[10]: *) let ex_correct_2 = [Ou; Non; F; V];; let _ = ecriture_prefixe_valide ex_correct_2 arite_calcul_prop;; (* true *);; (* ## Bonus : évaluation d'un terme L'objectif est de construire la fonction d'évaluation d'un terme en écriture postfixe présentée dans le texte (Algorithme 2, page 4) : ![images/algorithme_evaluation_terme.png](images/algorithme_evaluation_terme.png) - La pile utilisée dans l'algorithme sera implémentée par une simple liste. - Ce que le texte appelle "valeur" et "omegabarre" sont regroupés dans une liste de couples tels que le premier élément du couple est un symbole `s` et le deuxième la fonction `f_s` permettant d'interpréter ce symbole ; on considère que les constante sont des fonctions d'arité 0. Cette fonction `f_s` prend en arguments une liste d'éléments du domaine et renvoie un élément du domaine. *) (* ### Manipulation basique sur une pile On a besoin de savoir dépiler plus d'une valeur, pour récupérer les $k$ valeurs successives à donner à l'interprétation d'un symbole d'arité $k \geq 1$. Cette fonction renvoie un couple de listes : - la liste des `k` éléments en sommet de la pile `p` de sorte que le sommet de la pile `p` se trouve en dernière position dans cette liste, - et la pile `p` une fois qu'on a dépilé ses `k` éléments du sommet. *) (* In[11]: *) let depile (k : int) (p : 'a list) : ('a list * 'a list) = let rec aux (k : int) (p : 'a list) (sommet_pile : 'a list) : ('a list * 'a list) = match k with | 0 -> (sommet_pile, p) | _ when p = [] -> failwith "Liste vide" | i -> let sommet_modif = (List.hd p) :: sommet_pile in let p_modif = List.tl p in aux (i - 1) p_modif sommet_modif in aux k p [] ;; (* Il est absolument crucial de faire *au moins un test* à ce moment là : *) (* In[12]: *) depile 0 [0; 1; 2; 3; 4; 5; 6; 7];; depile 1 [0; 1; 2; 3; 4; 5; 6; 7];; depile 3 [0; 1; 2; 3; 4; 5; 6; 7];; depile 8 [0; 1; 2; 3; 4; 5; 6; 7];; (* Ça semble bien fonctionner. Notez que la première liste a été retournée ("renversée"), puisque les valeurs ont été empilées dans le sens inverse lors de leurs lectures. On peut aussi proposer une implémentation alternative, moins élégante mais plus rapide à écrire, avec des tableaux, et deux appels à `Array.sub` pour découper le tableau, et `Array.of_list` et `Array.to_list` pour passer d'une liste à un tableau puis de deux tableaux à deux listes. *) (* In[13]: *) let depile_2 (k : int) (p : 'a list) : ('a list * 'a list) = let pa = Array.of_list p in let debut = Array.sub pa 0 k in let fin = Array.sub pa k ((Array.length pa) - k) in (List.rev (Array.to_list debut)), (Array.to_list fin) ;; (* In[14]: *) depile_2 0 [0; 1; 2; 3; 4; 5; 6; 7];; depile_2 1 [0; 1; 2; 3; 4; 5; 6; 7];; depile_2 3 [0; 1; 2; 3; 4; 5; 6; 7];; depile_2 8 [0; 1; 2; 3; 4; 5; 6; 7];; (* ### Interprétation des symboles Cette fonction prend en entrée une liste `interpretation` de couples (symbole, fonction interprétant ce symbole), un symbole `t` et une liste d'arguments `arg` et renvoie `f_t(arg)`, où `f_t` est la fonction associée au sympole `t` via `interpretation`. Avec les notations mathématiques du texte, le symbole `t` est $\omega$ et `f_t` est $\overline{\omega}$ ("omegabarre" dans l'algorithme). *) (* In[15]: *) let interprete (interpretation : ('a * ('d list -> 'd)) list) (t : 'a) (arg : 'd list) : 'd = (List.assoc t interpretation) arg;; (* L'interprétation choisie consiste à évaluer la valeur de vérité d'une formule du calcul propositionnel, avec l'interprétation classique. Notez que pour faciliter le typage, ces fonctions recoivent la pile, i.e., une *liste* d'arguments (de taille arbitraire) et s'occupent elles-mêmes de récupérer le sommet de pile et les valeurs suivantes. > Les fonctions échouent si la pile donnée n'est pas assez profonde, bien évidemment. *) (* In[16]: *) let interp_V _ = true;; let interp_F _ = false;; let interp_Non l = not (List.hd l);; let interp_Ou l = (List.hd l) || (List.hd (List.tl l));; let interp_Et l = (List.hd l) && (List.hd (List.tl l));; let interp_ITE l = if (List.hd l) then (List.hd (List.tl l)) else (List.hd (List.tl (List.tl l)));; (* Ensuite, on crée cette liste d'association, qui permet d'associer à un symbole $\omega$ sa fonction $\overline{\omega}$ : *) (* In[17]: *) let interp_calcul_prop = [ (V, interp_V); (F, interp_F); (* Arité 0 *) (Non, interp_Non); (* Arité 1 *) (Ou, interp_Ou); (Et, interp_Et); (* Arité 2 *) (ITE, interp_ITE) (* Arité 3 *) ];; (* ### Évaluation d'un terme Cette fonction prend une liste `l`, de symboles que l'on suppose correspondre à l'écriture postfixe *correcte* d'un terme, une fonction `arite : symbole -> entier` et une liste `interpretation` d'interprétation des symboles. Elle renvoie le résultat de l'évaluation du terme `l` pour l'interpretation `interpretation`. L'algorithme est annoncé correct par le théorème 2, non prouvé dans le texte (ça peut être une idée de développement à faire au tableau). *) (* In[18]: *) let evalue (l : 'a list) (arite : 'a -> int) (interpretation : ('a * ('d list -> 'd)) list) : 'd = let rec aux (l : 'a list) (p : 'd list) : 'd = match l with | [] -> List.hd p | t :: q -> let k = arite t in let (arguments, p_temp) = depile k p in let valeur = interprete interpretation t arguments in let p_fin = valeur :: p_temp in (aux q p_fin) in aux l [];; (* Si la lecture de l'arité d'un symbole `t`, `k = arite t`, est en $\mathcal{O}(1)$, alors l'algorithme `evalue` a une complexité en $\mathcal{O}(n)$ où $n$ est le nombre de symbole du terme donné en entrée. En d'autres termes, l'évaluation d'un terme postfixe par la méthode naïve, utilisée pour la [notation polonaise inversée](https://en.wikipedia.org/wiki/Polish_notation), est *linéaire* dans la taille du terme. Chouette ! *) (* ### Un exemple d'évaluation d'un terme du calcul propositionnel *) (* On considère le terme "$\mathrm{Ou} \; \mathrm{Non} \; V \; \mathrm{Ou} \; V \; F$" $= \vee \neg V \vee V F \equiv (\neg V) \vee (V \vee F)$ suivant : ![images/exemple_terme.png](images/exemple_terme.png) *) (* In[19]: *) let ex1 = List.rev [ Ou; Non; V; Ou; V; F ];; (* écriture préfixe *) let _ = evalue ex1 arite_calcul_prop interp_calcul_prop;; (* On vient de passer de l'écriture préfixe à postfixe, simplement en inversant l'ordre des termes (`List.rev`). >