DS 3 — Exponentiation rapide et tri du crêpier¶
MP2I, année 2021-2022. Devoir surveillé 3 du 16 décembre 2021, partie écrite. Durée totale 2h. La partie pratique sur machine est sur une page séparée.
Tout document interdit. Tout dispositif électronique interdit.
On vous demande d'écrire lisiblement et de bien présenter. Les programmes en particulier doivent être bien indentés et lisibles. Utilisez de préférence plusieurs couleurs. Commentez votre code dès que ce qu'il fait n'est pas évident.
Pirelli¶
On considère la fonction suivante :
let expo_naive x n =
let p = ref 1 in
for i = 1 to n do
p := !p * x
done;
!p
Q01. Montrer que la fonction précédente calcule \(x^n\) en considérant sa terminaison acquise.
Corrigé
Ici, c'est une boucle for donc on a l'indice géré pour nous ce qui peut
compliquer la vérification de l'invariant. On pourrait le réécrire ainsi :
let expo_naive x n =
let p = ref 1 in
let i = ref 1 in
while !i <= n do
p := !p * x;
i := !i + 1
done;
!p
En pratique, on dira donc qu'on considère dans une boucle
for i = a to b que \(i\) vaut \(a\) avant la première itération, qu'on
incrémente \(i\) à la fin de l'itération et ainsi que \(i\) vaut \(b+1\) en
sortie.
En faisant cela, on considère la propriété \(I(i, p) := p = x^{i-1}\).
- Init : avant la boucle, on peut considérer que \(i = 1\) et on a bien \(p = 1 = x^0\) donc \(I(i,p)\) est vérifié.
- Her : si \(I(i,p)\) est vérifié en début d'itération on a \(p = x^{i-1}\) et donc \(p \times x = x^i\) donc on a bien \(I(i+1,p \times x)\) vérifié.
Ainsi, la propriété est vérifiée tout au long de la boucle : c'est un invariant. En sortie de boucle, on a \(p = x^{n+1-1} = x^n\).
Q02. Combien de multiplications effectue cette fonction pour calculer \(x^n\) ?
Corrigé
On effectue une multiplication par itération donc \(n\) multiplications.
On considère maintenant la fonction :
let expo_binaire x n =
let p = ref 1 in
let a = ref x in
let k = ref n in
while !k > 0 do
if !k mod 2 = 1
then p := !p * !a;
a := !a * !a;
k := !k / 2
done;
!p
Q03. Donner l'état des variables à chaque itération dans l'appel de
expo_binaire 3 15.
Corrigé
| \(p\) | \(a\) | \(k\) |
|---|---|---|
| 1 | 3 | 15 |
| 3 | 9 | 7 |
| 27 | 81 | 3 |
| 2187 | 6561 | 1 |
| 14348907 | 43046721 | 0 |
Q04. Prouver la correction de cette fonction :
- sa terminaison ;
- le fait qu'elle calcule \(x^n\).
Corrigé
- La quantité \(k\) est un variant de boucle car elle est entière, positive et dans une boucle, \(k > 0\) donc \(\lfloor k / 2 \rfloor < k\) et elle diminue strictement à chaque itération. Ainsi, la boucle termine car elle admet un variant.
-
On va montrer que la propriété \(I(a,p,k) := a^k p = x^n\) est un invariant de boucle.
Init : avant la première itération, on a \(a^k p = x^n \times 1 = x^n\) donc \(I(x,1,n)\) est vérifié.
Her : si \(I(a,p,k)\) est vérifié au début d'une itération :
- soit \(k = 2k'\) et \(x^n = a^k p = (a^2)^{k'} p\) donc \(I(a^2,p,k')\) est vérifié ;
- soit \(k = 2k'+1\) et \(x^n = a^k p = (a^2)^{k'} (a p)\) donc \(I(a^2,a p,k')\) est vérifié.
Dans tous les cas, \(I(a,p,k)\) est vérifié en fin d'itération avec les nouvelles valeurs des variables.
\(I\) est donc un invariant et en sortie de boucle, on a \(I(a,p,0)\) vérifié donc \(x^n = a^0 p = p\). On renvoie bien \(x^n\).
Soit \(n \in \N\), on note
- \(\lfloor \log_2 n \rfloor\) l'unique entier \(p\) tel que \(2^{p} \le n < 2^{p+1}\).
- \(\lceil \log_2 n \rceil\) l'unique entier \(p\) tel que \(2^{p-1} < n \le 2^{p}\).
- \(\nu(n)\) le nombre de \(1\) dans l'écriture binaire de \(n\). Plus précisément, si \(n = a_0 + a_1 2 + a_2 4 + \dots + a_{d-1} 2^{d-1}\) avec \(a_0,\dots,a_{d-1} \in \{0,1\}\) alors il y a exactement \(\nu(n)\) des \(a_i\) qui valent \(1\).
Q05. On suppose que \(n > 0\). Montrer que la fonction expo_binaire
effectue \(\lceil \log_2 n \rceil + \nu(n)\) multiplications pour calculer
\(x^n\).
Corrigé
On écrit \(n = \overline{1a_{d-2}\dots a_0}^2\) avec \(d\) chiffres en binaire, c'est-à-dire \(n = a_0 + a_1 2 + \dots + a_{d-2} 2^{d-2} + 2^{d-1}\) avec \(a_0,\dots,a_{d-2} \in \{0,1\}\). On a \(2^{d-1} \le n < 2^d\), donc \(d = \lfloor \log_2 n \rfloor + 1\).
À chaque étape, on décale dans \(k\) l'écriture binaire de \(n\) vers la droite :
On effectue donc exactement \(d\) itérations, et k mod 2 va prendre les
valeurs \(a_0, a_1, \dots, a_{d-2}, 1\) : on aura bien \(\nu(n)\) valeurs 1
entraînant une multiplication.
Au final, on a donc \(\nu(n)\) multiplications issues du test et \(d\) multiplications dues à la mise au carré de \(a\), soit \(\lfloor \log_2 n \rfloor + 1 + \nu(n)\) multiplications en tout.
- Si \(n\) n'est pas une puissance de 2, on a \(2^{d-1} < n < 2^d\), donc \(d = \lceil \log_2 n \rceil\) et on obtient bien \(\lceil \log_2 n \rceil + \nu(n)\) multiplications.
- Si \(n = 2^{d-1}\), on a \(\lceil \log_2 n \rceil = d-1\) et \(\nu(n) = 1\) : la fonction effectue \(d+1 = \lceil \log_2 n \rceil + \nu(n) + 1\) multiplications, une de plus que la formule de l'énoncé (par exemple 2 multiplications pour \(n = 1\) : \(1 \times x\) puis \(x \times x\)).
Note : c'est un \(O(\log_2 n)\).
L'algorithme précédent effectue des multiplications non nécessaires pour gérer les variables : le premier calcul \(1 \times x\) et la dernière mise au carré. Si on effectue ce principe à la main, pour calculer \(x^{15}\) on place \(x\) dans \(p\) (pas de multiplications cf. remarque), puis on calcule \(x^2\), on place \(x^3 = x \times x^2\) dans \(p\), puis on calcule \(x^4\), on place \(x^7 = x^3 \times x^4\) dans \(p\), on calcule \(x^8\) et on place \(x^{15} = x^7 \times x^8\) dans \(p\). Cette méthode de décomposition en binaire calcule ainsi \(x^{15}\) en six multiplications.
On peut faire mieux, en effet, on peut remarquer que \(15 = 3 \times 5\). Pour cela, on peut calculer \(x^3 = x \times x \times x = y\) en deux multiplications et \(x^{15} = y^5 = (y^2)^2 \times y\) en trois multiplications en calculant \(x^6 = x^3 \times x^3\), \(x^{15} = x^6 \times x^6 \times x^3\). Au total, on aura ainsi effectué cinq multiplications.
La méthode qu'on vient d'utiliser est appelée la méthode du facteur :
- si \(n\) est non premier et \(> 1\), on écrit \(n = p \times q\) avec \(p\) le plus petit facteur premier de \(n\). On a alors \(q > 1\). Pour calculer \(x^n\), on commence par calculer \(x^p = y\) puis \(y^q = (x^p)^q = x^{pq} = x^n\) par cette méthode.
- si \(n\) est premier, on écrit \(x^n = x \times x^{n-1}\) et on applique la méthode à \(n-1\).
- si \(n = 1\), on a directement \(x = x^1\).
Q06. Comparer les deux méthodes pour \(n = 55\) et \(n=33\). Que peut-on en conclure ?
Corrigé
Pour \(n = 55\) :
-
binaire : comme \(55 = \overline{110111}^2\), on calcule \(x^2\) qui place \(x^3 = x^2 \times x\) dans \(p\), \(x^4\) qui place \(x^7 = x^3 \times x^4\), \(x^8\), \(x^{16}\) qui place \(x^{23} = x^{16} \times x^7\) dans \(p\), \(x^{32}\) qui place \(x^{55} = x^{32} \times x^{23}\) dans \(p\). Au total, on a effectué \(5\) mises au carré et \(4\) mises à jour de \(p\), donc \(9\) multiplications. C'est bien \(\lceil \log_2 55 \rceil + \nu(55) - 2\) car on a omis les deux multiplications inutiles \(p = 1 \times x\) et la mise au carré finale.
Si on reprend les puissances calculées on a donc \(x^2, x^3, x^4, x^7, x^8, x^{16}, x^{23}, x^{32}, x^{55}\), à chaque fois avec une multiplication.
-
facteur : comme \(55 = 5 \times 11\) on calcule \(x^5 = x \times x^4 = x \times (x \times x)^2\), on a donc calculé \(x^2, x^4, x^5\) avec 3 multiplications. Pour calculer \(x^{55} = (x^5)^{11} = a^{11}\) on écrit \(a^{11} = a \times a^{10}= a\times (a^2)^{5}\), donc une fois calculé \(a^2\), pour calculer \((a^2)^{5}\) on doit encore effectuer 3 multiplications. On a donc, en tout, \(3 + 2 + 3 = 8\) multiplications.
Si on reprend les puissances calculées on a donc \(x^2, x^4, x^5, x^{10}, x^{20}, x^{40}, x^{50}, x^{55}\), à chaque fois avec une multiplication.
Pour \(n = 33\) :
- binaire : comme \(33 = \overline{100001}^2\), on calcule \(x^2\), \(x^4\), \(x^8\), \(x^{16}\), \(x^{32}\), \(x^{33}\) et on a donc effectué 6 multiplications.
- facteur : on a \(33 = 3 \times 11\) donc, en suivant le calcul précédent, on calcule \(x^2, x^3, x^6, x^{12}, x^{24}, x^{30}, x^{33}\) et on a donc effectué 7 multiplications.
Conclusion : aucune des méthodes n'est meilleure que l'autre.
Pour noter ces calculs de puissances, on peut considérer la suite finie des exposants utilisés. Ainsi, pour calculer \(x^{15}\) par la méthode binaire on obtient la chaine \((1, 2, 3, 4, 7, 8, 15)\) qui est de longueur 7. En effectuant la méthode du facteur, on obtient la chaine \((1,2,3,6,12,15)\) de longueur 6.
Q07. Donner, sans les justifier, les chaines correspondant aux deux méthodes pour 16, 27 et 125.
Corrigé
- pour 16 : \((1, 2, 4, 8, 16)\) dans les deux cas.
- pour 27 : \((1, 2, 3, 4, 8, 11, 16, 27)\) avec la méthode binaire et \((1, 2, 3, 6, 9, 18, 27)\) avec la méthode du facteur.
- pour 125 : \((1, 2, 4, 5, 8, 13, 16, 29, 32, 61, 64, 125)\) avec la méthode binaire et \((1, 2, 4, 5, 10, 20, 25, 50, 100, 125)\) avec la méthode du facteur.
Définition
Une suite finie \((n_0,\dots,n_r)\) est appelée une chaine de calcul pour \(x^n\) si :
- \(n_0 = 1\)
- \(n_r = n\)
- \(\forall k \in \range{1}{r}, \exists i,j \in \range{0}{k-1}, n_k = n_i + n_j\)
La dernière condition exprime le fait qu'on a pu calculer \(x^{n_k} = x^{n_i} \times x^{n_j}\).
Q08.
-
Montrer par récurrence sur \(r\) que toute chaine \((n_0,\dots,n_r)\) de calcul pour \(x^n\) vérifie
\[ \forall k \in \range{0}{r}, n_k \le 2^k \] -
En déduire que tout calcul de \(x^n\) effectue au moins \(\lceil \log_2 n \rceil\) multiplications.
- Donner une infinité de valeurs de \(n\) pour lesquelles ce minorant est atteint.
Corrigé
-
On raisonne par récurrence sur \(r\).
- Init : si \(r = 0\) la seule chaine est \((1)\) et on a bien \(1 \le 2^0\).
- Her : si c'est vrai pour \(r-1 \geqslant 0\), soit \((n_0,\dots,n_r)\) une chaine de calcul ; on a \((n_0,\dots,n_{r-1})\) chaine de calcul donc \(\forall k \in \range{0}{r-1}, n_k \le 2^{k}\) et ainsi par hypothèse on a une décomposition \(n_r = n_i + n_j \le 2^i + 2^j \le 2 \times 2^{r-1} = 2^r\). Donc, on a bien l'hypothèse au rang \(r\).
On a validé la récurrence.
-
On remarque que toute chaine de longueur \(r+1\) effectue \(r\) multiplications. Comme \(n_r = n \le 2^r\) on a \(\log_2 n \le r\) et comme \(r\) est un entier \(\lceil \log_2 n \rceil \le r\).
- C'est atteint pour les \(2^r\) où on a une chaine \((1,2,4,\cdots,2^{r-1},2^r)\) qui effectue \(r = \log_2 (2^r)\) multiplications.
Breizhsort¶
Si t est un tableau et \(0 \le i \le j < |t|\) des indices, on notera
t[i:j] le sous-tableau de t composé des valeurs entre les indices \(i\) et
\(j\) inclus.
On va définir dans la suite du sujet une fonction
retourner : 'a array -> int -> int -> unit
telle que retourner t i j retourne le sous-tableau t[i:j].
Ainsi, si t = [| 1; 2; 3; 4; 5; 6 |] alors retourner t 2 4 change t en
[| 1; 2; 5; 4; 3; 6 |] car t[2:4] = [| 3; 4; 5 |].
Q09. Quel est le contenu de t à l'issue du programme suivant ?
let t = [| 5; 8; 2; 0; 3; 6; 5; 2; 1 |];;
retourner t 1 7;;
Note : vous pouvez ignorer les ;; ils ne sont là que pour s'assurer qu'on
puisse avoir cette interaction dans un toplevel OCaml.
Corrigé
[| 5; 2; 5; 6; 3; 0; 2; 8; 1 |]
On considère les fonctions suivantes :
let echanger t i j =
let temp = t.(i) in
t.(i) <- t.(j);
t.(j) <- temp
let rec retourner t i j =
if i < j
then begin
echanger t i j;
retourner t (i+1) (j-1)
end
Q10. Montrer que retourner termine.
Corrigé
On remarque que \(j - i\) est entière ; si elle est négative ou nulle on termine, sinon on effectue un appel tel que \((j-1)-(i+1) = j-i - 2 < j-i\) donc elle diminue strictement.
Ainsi, il ne peut y avoir qu'un nombre fini d'appels récursifs, la fonction termine.
Q11. Montrer que retourner correspond bien à la spécification de
l'énoncé. On pourra montrer par récurrence sur \(j-i+1\) (le \(+1\) n'est là que
pour déplacer l'initialisation au cas de base) que
où t' est la valeur initiale de t.
Corrigé
On suit l'énoncé, par récurrence forte sur \(j-i+1\).
- Init : pour \(j-i+1 \le 1\) on a \(j \le i\), donc soit \(\range{i}{j} = \emptyset\), soit \(i = j\) et la propriété \(\texttt{t.(i)} = \texttt{t'.(i)}\) est vérifiée car on ne fait rien.
-
Her : si la propriété est vérifiée pour toutes les valeurs de \(j-i+1\) inférieures ou égales à \(n\), avec \(n \ge 1\), on considère \(i,j\) tels que \(j-i+1 = n+1\), c'est-à-dire que \(j-i = n \ge 1\) et donc \(i < j\). On échange les valeurs aux indices \(i\) et \(j\), ce qui donne la propriété pour \(k = i\) et \(k = j\), puis on appelle
retourner t (i+1) (j-1)avec \((j-1)-(i+1)+1 = n-1\) : par hypothèse, on a alors\[ \forall k \in \range{i+1}{j-1}, \texttt{t.(k)} = \texttt{t'.(i+1+j-1-k)} = \texttt{t'.(i+j-k)} \](les cases d'indices \(i+1\) à \(j-1\) n'ayant pas été modifiées par l'échange). On a donc la propriété voulue.
On considère maintenant les fonctions suivantes :
let indice_max t k =
let m = ref 0 in
for i = 1 to k do
if t.(!m) < t.(i)
then m := i
done;
!m
let placer t k =
let i = indice_max t k in
if i <> k
then begin
retourner t 0 i;
retourner t 0 k
end
Q12. Montrer qu'en sortie de l'appel à placer t k le tableau t
vérifie :
Corrigé
-
On commence par montrer que la fonction
indice_maxfait ce qu'on attend d'elle. Pour cela, on considère la propriété\[ I(i,m) := \texttt{t.(m)} = \max_{0 \le j < i} \texttt{t.(j)} \]dont on montre facilement que c'est un invariant de boucle. En sortie, on a donc \(I(k+1,m) := \texttt{t.(m)} = \max_{0 \le j \le k} \texttt{t.(j)}\) donc \(m\) est bien l'indice d'un maximum de \(t\) entre les indices \(0\) et \(k\).
-
Pas de boucle ici, donc on va juste utiliser les propriétés précédentes. Si \(i = k\), c'est direct,
t.(k)est le maximum.Sinon, c'est que le maximum est à l'indice \(i\). Suite au premier appel à
retourneron place ce maximum à l'indice 0 et suite au second on le place à l'indice \(k\), ce qui permet ainsi d'assurer la propriété voulue.
On en déduit alors un algorithme de tri, dit tri du crêpier car il consiste à faire des retournements successifs comme une pile de crêpes.
let rumman t =
for k = Array.length t - 1 downto 0 do
placer t k
done
Q13. Donner l'état du tableau à chaque itération dans l'appel à
rumman t où t = [| 0; 5; 7; 1; 2 |].
Corrigé
[| 0; 5; 7; 1; 2 |]- \(k=4\) : \(\rightarrow\)
[| 7; 5; 0; 1; 2 |]\(\rightarrow\)[| 2; 1; 0; 5; 7 |] - \(k=3\) : pas de changement
- \(k=2\) : \(\rightarrow\)
[| 2; 1; 0; 5; 7 |]\(\rightarrow\)[| 0; 1; 2; 5; 7 |] - \(k=1\) : pas de changement
- \(k=0\) : pas de changement
Q14. En utilisant un invariant de boucle, montrer que la fonction
rumman trie le tableau passé en argument dans l'ordre croissant.
Corrigé
On commence par remarquer que les seules modifications du tableau sont
effectuées par retourner et que cette modification ne fait que permuter
des éléments. Donc, à l'issue de rumman les valeurs ont juste été
permutées.
On montre alors que la propriété suivante est un invariant de boucle :
où \(|t|=n\), en raisonnant sur l'indice comme auparavant : on le décrémente en fin d'itération.
Note : on aurait pu simplifier l'invariant en indiquant que t.(k+1)
est plus grand que les valeurs précédentes, mais l'indice \(k+1\) est
invalide au départ.
- Init : au départ \(k = n-1\) et donc \(t[k+1:n-1] = \emptyset\) et la seconde partie de la propriété est triviale.
- Her : si la propriété est vérifiée au début d'une itération, on sait
que \(t[k+1:n-1]\) est trié et que suite à l'appel à
placer t kon va avoir à l'indice \(k\) le maximum des valeurs aux indices de 0 à \(k\) qui sont elles-mêmes plus petites quet.(k+1)par hypothèse. Donc on a bien la propriété en fin d'itération.
En sortie de boucle, on a donc \(t\) trié.
Q15. Donner, en les justifiant, les complexités temporelles dans le pire
des cas des fonctions retourner, placer, indice_max et rumman.
Corrigé
retourner t i jeffectue des appels en enlevant \(2\) à \(j-i\) à chaque appel, on a donc \(O(j-i)\) appels, chacun en temps constant.indice_max t kest directement en \(O(k)\) par la boucle.placerest en (indice, retourne, retourne) \(O(k)+O(i)+O(k) = O(k)\) car \(i \le k\).rummanest en \(\sum_{k=0}^{n-1} O(k) \le C \sum_{k=0}^{n-1} k = C \frac{n(n-1)}{2}\) donc en \(O(n^2)\).