Les fonctions sont à écrire en OCaml. Chaque question de programmation est suivie de tests qui doivent passer sans erreur.
Palindromes¶
On dit qu'un tableau t est un palindrome quand il comporte les éléments dans
le même ordre qu'on le lise de droite à gauche ou de gauche à droite. Par
exemple [|0;1;2;1;0|] est un palindrome mais [|0;1;2|] n'en est pas un.
On vous demande ici de programmer de deux manières une fonction
palindrome : 'a array -> bool qui détermine si un tableau est un palindrome.
Question 1. Tout d'abord, on va programmer la fonction impérativement en s'appuyant sur une référence pour vérifier dans une boucle si le tableau est un palindrome.
Corrigé
let palindrome t =
let n = Array.length t in
let check = ref true in
for i = 0 to n / 2 - 1 do
check := !check && t.(i) = t.(n-i-1)
done;
!check
Tests :
assert(palindrome [|0;1;2;1;0|]);
assert(not (palindrome [|1;2|]));
assert(not (palindrome [|1;3;2|]));
assert(palindrome [||]);
assert(palindrome [|1|]);
assert(palindrome [|0;1;1;0|]);
assert(palindrome [|"a";"b";"b";"a"|])
Question 2. Prouver la correction de votre fonction et donner sa complexité.
Corrigé
Notons \(n\) la longueur de t. Le tableau est un palindrome si et seulement
si \(\forall k \in \range{0}{n-1},\ t_k = t_{n-1-k}\). Comme cette condition
est symétrique en \(k\) et \(n-1-k\), et triviale pour \(k = n-1-k\) (élément
central quand \(n\) est impair), il suffit de la vérifier pour \(k \in
\range{0}{\lfloor n/2 \rfloor - 1}\).
La boucle for termine. On montre l'invariant suivant : au début de
l'itération d'indice \(i\), !check vaut vrai si et seulement si \(\forall k
\in \range{0}{i-1},\ t_k = t_{n-1-k}\).
- Pour \(i = 0\),
!checkvauttrueet l'intervalle est vide. - Si l'invariant est vrai au début de l'itération \(i\), après l'affectation
!checkvaut vrai si et seulement si l'ancienne valeur est vraie et \(t_i = t_{n-1-i}\), c'est-à-dire si et seulement si \(\forall k \in \range{0}{i},\ t_k = t_{n-1-k}\).
En sortie de boucle, l'invariant est vérifié pour \(i = \lfloor n/2 \rfloor\) (y compris quand la boucle n'est pas exécutée, pour \(n \le 1\)), donc la fonction renvoie bien la bonne valeur.
Il y a \(\lfloor n/2 \rfloor\) itérations, chacune en temps constant pour des
éléments de taille bornée (la comparaison = sur des chaînes, par exemple,
a un coût proportionnel à leur longueur). La complexité est donc en \(O(n)\).
On remarque qu'on ne s'arrête pas à la première différence : on pourrait
utiliser une boucle while pour cela.
Question 3. Maintenant, on va programmer la fonction à l'aide d'une
fonction récursive (on peut la définir en dehors de la fonction palindrome ou
comme une variable locale à l'intérieur de palindrome).
Corrigé
Grâce à l'évaluation paresseuse de &&, on s'arrête ici à la première
différence.
let palindrome t =
let n = Array.length t in
let rec check i =
if i < n/2
then t.(i) = t.(n-i-1) && check (i+1)
else true
in check 0
Tests : les mêmes que pour la question 1.
Anagrammes¶
On dit qu'un tableau est un anagramme d'un autre s'ils contiennent les mêmes éléments à une permutation près.
Ainsi [|1;2;3;4|] et [|4;3;2;1|] sont des anagrammes. Cela se complique
quand on considère des tableaux ayant plus d'une fois la même valeur, par
exemple [|1;2;1|] et [|2;1;1|] sont des anagrammes.
Question 4. Dans un premier temps, on fait l'hypothèse que les tableaux ne
contiennent jamais deux fois la même valeur et qu'ils sont de même longueur.
Écrire une fonction anagramme : 'a array -> 'a array -> bool.
Corrigé
Sous ces hypothèses, il suffit de vérifier que chaque élément de t1 est
présent dans t2.
let anagramme t1 t2 =
let n = Array.length t1 in
let valide = ref true in
for i = 0 to n-1 do
let found = ref false in
for j = 0 to n-1 do
if t2.(j) = t1.(i)
then found := true
done;
valide := !found && !valide
done;
!valide
Tests :
assert(anagramme [|1;2;3;4|] [|1;2;3;4|]);
assert(anagramme [|1;2;3;4|] [|4;3;2;1|]);
assert(not (anagramme [|1;2;3;4|] [|1;2;3;5|]));
assert(anagramme [||] [||]);
assert(anagramme [|"c";"b";"a"|] [|"a";"b";"c"|])
Question 5. Quelle est la complexité de votre fonction anagramme ?
Corrigé
Les deux boucles imbriquées effectuent exactement \(n^2\) itérations en temps constant : la complexité est en \(O(n^2)\).
Question 6. On suppose maintenant que les valeurs peuvent se répéter, mais
on suppose également que les tableaux ne contiennent que des valeurs entières
positives strictement plus petites que 100. Écrire une nouvelle fonction
anagramme.
Indication : comptez les éléments.
Corrigé
let anagramme t1 t2 =
let n = Array.length t1 in
let compte = Array.make 100 0 in
let compte2 = Array.make 100 0 in
for i = 0 to n-1 do
compte.(t1.(i)) <- 1 + compte.(t1.(i));
compte2.(t2.(i)) <- 1 + compte2.(t2.(i))
done;
let valide = ref true in
for i = 0 to 99 do
valide := !valide && compte.(i) = compte2.(i)
done;
!valide
Tests :
assert(anagramme [|1;2;3;4|] [|1;2;3;4|]);
assert(anagramme [|1;2;3;4|] [|4;3;2;1|]);
assert(not (anagramme [|1;2;3;4|] [|1;2;3;5|]));
assert(anagramme [|1;2;2;4|] [|4;2;2;1|]);
assert(anagramme [|99;99;99;99|] [|99;99;99;99|]);
assert(not (anagramme [|1;2;1;1|] [|1;2;2;1|]));
assert(anagramme [||] [||])
Question 7. Quelle est la complexité de cette nouvelle fonction
anagramme ?
Corrigé
La création des deux tableaux de comptage et la seconde boucle coûtent \(O(100)\), la première boucle \(O(n)\) : la complexité est en \(O(n + 100) = O(n)\). Plus généralement, si les valeurs sont strictement inférieures à \(M\), on obtient \(O(n + M)\), comme pour le tri par comptage.
Question 8. On considère maintenant une 'a list plutôt qu'un 'a array,
mais dont les éléments sont uniques. Écrire une fonction
genere_anagramme : 'a list -> 'a list list permettant de renvoyer la liste de
tous les anagrammes de la liste donnée.
Par exemple, étant donné la liste [1;2;3], on devra renvoyer :
[ [1;2;3]; [1;3;2]; [2;1;3];
[2;3;1]; [3;1;2]; [3;2;1] ]
Corrigé
Pour chaque élément t de la liste, on génère les anagrammes de la liste
privée de t et on place t en tête de chacun d'eux.
let rec genere_anagramme l =
let rec enleve l x =
match l with
| [] -> []
| y::q -> if x = y then q else y :: enleve q x
in
let rec anag_aux l1 =
match l1 with
| [] -> [ ]
| t::q -> List.map (fun m -> t::m) (genere_anagramme (enleve l t))
@ anag_aux q
in
match l with
| [] -> [ [] ]
| _ -> anag_aux l
Une variante avec des fonctions mutuellement récursives :
let rec enleve l x =
match l with
| [] -> []
| t::q -> if t = x
then q
else t::enleve q x
let rec gen_aux l lorig =
match l with
| [] -> []
| x::q ->
List.map (fun r -> x::r)
(genere (enleve lorig x))
@ gen_aux q lorig
and genere l = if l = [] then [ [] ]
else gen_aux l l
Test :
assert(genere_anagramme [1;2;3] =
[[1; 2; 3]; [1; 3; 2]; [2; 1; 3]; [2; 3; 1]; [3; 1; 2]; [3; 2; 1]])