You also want an ePaper? Increase the reach of your titles
YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.
<strong>Lycée</strong> <strong>Thiers</strong><br />
<strong>mpsi</strong> <strong>123</strong><br />
CORRECTION DS 2<br />
Exercice 1 - Diviser pour mieux régner ?<br />
1) Calcul de la somme des termes d’une liste d’entiers :<br />
let sum l = aux 0 l where<br />
rec aux sp = function (* sp pour Somme Partielle *)<br />
| [] -> sp<br />
| h::t -> aux (h+sp) t<br />
;;<br />
La fonction aux est récursive terminale, comme demandé.<br />
2) Partage d’une liste en deux demi-listes :<br />
let rec partage = function<br />
| [] -> [],[]<br />
| [x] -> [x],[]<br />
| x::y::r -> let (g,d) = partage r in (x::g,y::d)<br />
;;<br />
3) Si la liste est vide, la somme est nulle (par convention). Si la liste est réduite à un élément, la somme<br />
est donnée par cet élément. Enfin, si la liste est de longueur 2, on la partage en deux demi-listes (avec<br />
la fonction partage ci-dessus), dont on calcule récursivement la somme des termes, puis on ajoute les<br />
résultats.<br />
4) Calcul de la somme des termes d’une liste d’entiers, en divisant pour régner :<br />
let rec sumbis = function<br />
| [] -> 0<br />
| [x] -> x<br />
| l -> let (g,d) = partage l in (sumbis g) + (sumbis d)<br />
;;<br />
5) Montrons par récurrence forte que, pour tout n 1 :<br />
T (n) = n − 1<br />
C’est le cas pour n = 1. Supposons le résultat établi aux rangs inférieurs à n, pour un certain n 2. Deux<br />
cas se présentent ...<br />
Si n est pair, alors :<br />
et si n est impair, alors :<br />
( ) n − 1<br />
T (n) = T + T<br />
2<br />
L’hérédité est établie.<br />
( ) ( )<br />
n n<br />
T (n) = 2T + 1 = 2<br />
2 2 − 1 + 1 = n − 1<br />
( n + 1<br />
2<br />
) ( ) ( )<br />
n − 1 n + 1<br />
+ 1 = − 1 + − 1 + 1 = n − 1<br />
2<br />
2<br />
6) Si l’on note T (n) le nombre d’additions engendrées par l’appel sumbis l, lorsque l est une liste de longueur<br />
n, on a T (0) = T (1) = 0 et, pour tout n 2 :<br />
(⌊ ⌋) (⌈ ⌉)<br />
n n<br />
T (n) = T + T + 1<br />
2 2<br />
D’après le 5°), cette méthode dichotomique n’est donc pas meilleure que la méthode “naïve” de la question<br />
1°). On peut même affirmer qu’elle est moins performante, car la mise en œuvre de la récursion possède<br />
un coût ! La preuve en chiffres :
CORRECTION DS 2 2<br />
#open "sys";;<br />
let rec jusque = function<br />
| 0 -> []<br />
| n -> n :: (jusque (n - 1))<br />
;;<br />
let L = jusque 100000 in<br />
let t0 = time() in<br />
let s0 = sum L in<br />
let t1 = time() in<br />
let s1 = sumbis L in<br />
let t2 = time() in<br />
(t1 -. t0, t2 -. t1)<br />
;;<br />
- : float * float = 0.0, 0.533333333333<br />
Exercice 2 - Vecteurs “presque” triés<br />
1) Le tri d’un vecteur peut modifier physiquement le vecteur en question (sa structure initiale est perdue) ou<br />
bien s’effectuer sur une copie de celui-ci. Dans le premier cas, on parle de tri “en place” et dans le second,<br />
de tri “hors place”. Le principal avantage d’un tri en-place est évidemment l’économie de mémoire.<br />
2) Le tri par fusion est un tri récursif par comparaison. Une séquence vide ou de longueur 1 est déjà triée.<br />
Pour une séquence de longueur n 2, on partage celle-ci en deux séquences de longueur moitié (plus<br />
précisément, l’une de longueur ⌊ ⌋ ⌈ ⌉<br />
n<br />
2 et l’autre de longueur n<br />
2 ), ces deux séquences sont ensuite triées<br />
récursivement, et leurs versions triées sont alors fusionnées.<br />
La mise en œuvre de cet algorithme repose donc sur l’écriture de trois fonctions : la première partage<br />
une séquence quelconque en deux moitiés, la seconde fusionne deux séquences triées en une seule, la<br />
troisième effectue le tri proprement dit en faisant appel aux deux précédentes.<br />
La complexité (en nombre de comparaisons) est “quasi-linéaire” : elle vérifie T (n) = Θ (n ln (n)) .<br />
3) En mettant côte à côte le vecteur v et sa version triée v ′ :<br />
v : 3 1 2 0 5 4 9 8 7 6<br />
v ′ : 0 1 2 3 4 5 6 7 8 9<br />
on constate que la distance maximum entre un terme de v et le terme de même valeur dans v ′ est d = 3.<br />
Ainsi, le vecteur v est 3−quasi-trié. Il n’est pas 2−quasi-trié puisque, par exemple : v. (0) = v ′ . (3) .<br />
4) Dans la définition proposée, le problème vient du fait que plusieurs termes du vecteur peuvent être égaux.<br />
Il faut donc plutôt considérer qu’un vecteur v est d−quasi-trié lorsqu’en notant v ′ sa version triée :<br />
∀i ∈ {0, · · · , n − 1} , ∃j ∈ {0, · · · , n − 1} ; ( v. (i) = v ′ . ( j ) et ∣ ∣ ∣i − j<br />
∣ ∣∣ d<br />
)<br />
5) Une idée est de trier successivement (par fusion) les sous-vecteurs v [0 : 2d[ , v [d : 3d[ , etc ... en terminant<br />
éventuellement avec v [kd : n[ (si n n’est pas multiple de d et en posant k = ⌊ n<br />
d<br />
⌋<br />
).<br />
Lorsque v [0 : 2d[ est trié, on est sûr que chaque terme v [0 : d[ a trouvé sa place définitive.<br />
Puis, lorsque v [d : 3d[ est trié, même chose pour v [d : 2d[ et ainsi de suite.<br />
On effectue donc Θ ( n<br />
d<br />
)<br />
tris par fusion de vecteurs de longueur 2d (sauf éventuellement un dernier vecteur<br />
plus court) et le nombre total de comparaisons est donc Θ (n ln (d)) .
CORRECTION DS 2 3<br />
Exercice 3 - Suite de Conway<br />
1) Fonction separe :<br />
Type : ’a list → int * ’a * ’a list<br />
En entrée : un liste non vide h::...::h::y (h répété n fois)<br />
En sortie : le triplet n,h,y<br />
let separe = function<br />
| [] -> failwith "liste vide"<br />
| h::t -> aux 1 t where<br />
rec aux count = function<br />
| [] -> (count, h, [])<br />
| head::tail -> if h = head then aux (count + 1) tail<br />
else (count, h, head::tail)<br />
;;<br />
Exemple :<br />
separe [1;1;1;2;2;3];;<br />
- : int * int * int list = 3, 1, [2; 2; 3]<br />
2) Fonction lire :<br />
Type : int list → int list<br />
En entrée : une liste d’entiers l<br />
En sortie : la liste obtenue en “lisant” l<br />
let rec lire = function<br />
| [] -> []<br />
| l -> let (x,n,t) = separe l in x :: n :: lire t<br />
;;<br />
Exemple :<br />
lire [1;1;1;2;2;3];;<br />
- : int list = [3; 1; 2; 2; 1; 3]<br />
3) Fonction conway :<br />
Type : int → int list<br />
En entrée : un entier n 0<br />
En sortie : le n−ème terme de la suite de Conway, sous la forme d’une liste de chiffres<br />
let rec conway = function<br />
| 0 -> [1]<br />
| n -> lire (conway (n-1))<br />
;;<br />
Exemple :<br />
conway 5;;<br />
- : int list = [3; 1; 2; 2; 1; 1]<br />
Remarque. Cette suite possède de curieuses propriétés. Pour en savoir plus, on pourra consulter la page web<br />
http://fr.wikipedia.org/wiki/Suite_de_Conway
CORRECTION DS 2 4<br />
Exercice 4 - Somme maximale d’un sous-vecteur<br />
1) D’évidence, si tous les termes de v sont positifs ou nuls, alors σ ⋆ (v) est donné par σ (v) .<br />
2)<br />
2.a) Pour tout couple ( i, j ) tel que 0 i j n − 1, on doit calculer la somme<br />
j − i additions. Au total :<br />
⎛ ⎞ ⎛ ⎞<br />
∑n−1<br />
∑n−1<br />
∑n−1<br />
n−1−i ∑<br />
T (n) = j − i ⎜⎝ ⎟⎠ = ⎜⎝<br />
k⎟⎠ = 1 ∑n−1<br />
(n − 1 − i) (n − i) = 1 2<br />
2<br />
i=0<br />
j=i<br />
i=0<br />
k=0<br />
i=0<br />
j∑<br />
v. (k), ce qui représente<br />
k=i<br />
n∑<br />
k (k − 1)<br />
Comme k (k − 1) = 1 3<br />
[(k + 1) k (k − 1) − k (k − 1) (k − 2)] , on obtient (sommation télescopique) :<br />
Il est alors clair que T (n) = Θ ( n 3) .<br />
2.b) Implémentation de la méthode 0 :<br />
let mssp_0 v =<br />
let n = vect_length v in<br />
let left = ref 0 in<br />
let right = ref 0 in<br />
let smax = ref v.(0) in<br />
for i = 0 to n - 1 do<br />
for j = i to n - 1 do<br />
let s = ref 0 in<br />
for k = i to j do<br />
s := !s + v.(k)<br />
done;<br />
if !s > !smax then (<br />
left := i;<br />
right := j;<br />
smax := !s<br />
)<br />
done<br />
done;<br />
(!smax, !left, !right)<br />
;;<br />
T (n) = 1 (n + 1) n (n − 1)<br />
6<br />
3) Pour chaque i ∈ {0, · · · , n − 1} , l’algorithme ci-dessus calcule toutes les sommes v. (i) , v. (i) + v. (i + 1) , etc ...<br />
v. (i) + · · · + v. (n − 1) . De nombreuses additions sont donc effectuées plusieurs. On peut donc améliorer les<br />
choses ainsi :<br />
let mssp_1 v =<br />
let n = vect_length v in<br />
let left = ref 0 in<br />
let right = ref 0 in<br />
let smax = ref v.(0) in<br />
for i = 0 to n - 1 do<br />
let s = ref 0 in<br />
for j = i to n - 1 do<br />
s := !s + v.(j);<br />
if !s > !smax then (<br />
left := i;<br />
right := j;<br />
smax := !s<br />
)<br />
done<br />
done;<br />
(!smax, !left, !right)<br />
;;<br />
k=1
CORRECTION DS 2 5<br />
4)<br />
A présent, le nombre d’additions effectuées est :<br />
∑n−1<br />
∑n−1<br />
T (n) = (n − 1 − i) = k =<br />
et la complexité est donc passé à T (n) = Θ ( n 2) .<br />
4.a) On a s 0 = v. (0) et, pour :<br />
Il est par ailleurs clair que :<br />
i=0<br />
k=0<br />
⎧<br />
v. ( j ) si s j−1 < 0<br />
⎪⎨<br />
s j =<br />
⎪⎩ s j−1 + v. ( j ) sinon<br />
σ ⋆ (v) = max {s 0 , · · · , s n−1 }<br />
n (n − 1)<br />
2<br />
4.b) Ce qui précède montre qu’on peut calculer σ ⋆ (v) comme suit :<br />
let mssp_2 v =<br />
let n = vect_length v in<br />
let max_local = ref v.(0) in<br />
let max_global = ref v.(0) in<br />
for j = 1 to n - 1 do<br />
if !max_local < 0<br />
then max_local := v.(j)<br />
else max_local := !max_local + v.(j);<br />
;;<br />
if !max_local > !max_global then max_global := !max_local;<br />
done;<br />
!max_global<br />
Dans la fonction ci-dessus, max_global est une référence sur ce qui sera au final la plus grande somme,<br />
c’est-à-dire σ ⋆ (v) . Tandis que max_local est une référence sur la plus grande somme parmi celles qui<br />
se terminent par le terme d’indice courant (à savoir j).<br />
On constate que la complexité est à présent O (n) ; en effet, on compte une seule boucle de longueur<br />
n − 1 et au plus une addition par tour de boucle.<br />
On peut étoffer cette fonction pour qu’elle calcule aussi, comme les deux précédentes, les indices<br />
extrêmes d’un sous-vecteur qui réalise la somme maximale :<br />
let mssp_lin v =<br />
let n = vect_length v in<br />
let left_local = ref 0 in<br />
let left_global = ref 0 in<br />
let right = ref 0 in<br />
let max_local = ref v.(0) in<br />
let max_global = ref v.(0) in<br />
for j = 1 to n - 1 do<br />
if !max_local < 0 then (<br />
max_local := v.(j);<br />
left_local := j<br />
)<br />
else max_local := !max_local + v.(j);<br />
;;<br />
if !max_local > !max_global then (<br />
max_global := !max_local;<br />
left_global := !left_local;<br />
right := j<br />
)<br />
done;<br />
(!max_global, !left_global, !right)
CORRECTION DS 2 6<br />
5)<br />
5.a) Le principe “diviser pour régner” consiste, étant donné un problème de taille n, à le fractionner en un<br />
certain nombre a de problèmes de taille n/b.<br />
Ces derniers sont traités récursivement et leurs solutions respectives sont alors assemblées d’une<br />
manière appropriée, pour produire une solution au problème posé.<br />
5.b) Pour résoudre le MSSP via le principe diviser pour régner, on a besoin d’une fonction qui détermine<br />
la somme maximale pour les vecteurs “à cheval”.<br />
Si l’on note a, b les indices de début et de fin et i l’indice intermédiaire, il est clair que σ (v [a : i])<br />
doit être maximale parmi les sommes de sous-vecteurs qui se terminent avec le terme d’indice i et que<br />
σ (v [i : b]) doit être maximale parmi les sommes de sous-vecteurs qui démarrent avec ce même terme.<br />
En en déduit le code suivant :<br />
let mss_a_cheval v debut interm fin =<br />
let left = ref interm in<br />
let sLeftMax = ref 0 in<br />
let s = ref 0 in<br />
for i = interm downto debut do<br />
s := !s + v.(i);<br />
if !s > !sLeftMax then (<br />
left := i;<br />
sLeftMax := !s<br />
)<br />
done;<br />
let right = ref interm in<br />
let sRightMax = ref 0 in<br />
s := 0;<br />
for i = interm + 1 to fin do<br />
s := !s + v.(i);<br />
if !s > !sRightMax then (<br />
right := i;<br />
sRightMax := !s<br />
)<br />
done;<br />
(! sLeftMax + !sRightMax, !left, !right)<br />
;;
CORRECTION DS 2 7<br />
Ensuite, la fonction de résolution proprement dite :<br />
let mss_dpr v =<br />
let n = vect_length v in mss_aux 0 (n - 1) where<br />
rec mss_aux a b =<br />
if a = b then (v.(a), a, a)<br />
else if b = a + 1 then let s = v.(a) + v.(b) in (<br />
if v.(a) > v.(b) then (<br />
if v.(a) > v.(a) + v.(b) then (v.(a), a, a)<br />
else (s, a, b)<br />
)<br />
else (<br />
if v.(b) > v.(a) + v.(b) then (v.(b), b, b)<br />
else (s, a, b)<br />
)<br />
)<br />
else let m = (a + b) / 2 in<br />
let (smax0, left_0, right_0) = mss_aux a m in<br />
let (smax1, left_1, right_1) = mss_aux m b in<br />
let (smax01, left_01, right_01) = mss_a_cheval v a m b in<br />
if smax0 > smax1 then (<br />
if smax01 > smax0 then (smax01, left_01, right_01)<br />
else (smax0, left_0, right_0)<br />
)<br />
else (<br />
if smax01 > smax1 then (smax01, left_01, right_01)<br />
else (smax1, left_1, right_1)<br />
)<br />
;;<br />
5.c) On a la relation de récurrence suivante :<br />
(⌊ ⌋) (⌈ ⌉)<br />
n n<br />
T (n) = T + T + O (n)<br />
2 2<br />
d’où, d’après le second cas du Master Theorem :<br />
T (n) = Θ (n ln (n))<br />
Cet algorithme est quasi-linéaire. Il est donc moins performant que celui vu à la question 4° (qui est<br />
linéaire et optimal : il est connu sous le nom d’algorithme de Kadane).