#!/usr/bin/env python # coding: utf-8 #
#
TP n°13 (algorithmique, complexité) : Éléments de correction
# $\require{color}$ # $\require{xcolor}$ # $\newcommand{\myfbox}[1]{\fcolorbox{red}{white}{$\textrm{#1}$}}$ # $\require{stmaryrd}$ # $\newcommand{\ient}{[\![}$ # $\newcommand{\fient}{]\!]}$ # $\newcommand{\R}{\mathbb R}$ # $\newcommand{\K}{\mathbb K}$ # $\newcommand{\N}{\mathbb N}$ # $\newcommand{\C}{\mathbb C}$ # $\newcommand{\sh}{\operatorname{sh}}$ # $\newcommand{\ch}{\operatorname{ch}}$ # $\newcommand{\id}{\operatorname{Id}}$ # $\newcommand{\mat}{\operatorname{mat}}$ # $\newcommand{\sp}{\operatorname{Sp}}$ # $\newcommand{\In}{\operatorname{I}}$ # $\newcommand{\vect}{\operatorname{Vect}\ }$ # $\newcommand{\rg}{\operatorname{rg}}$ # $\newcommand{\tr}{\operatorname{Tr}}$ # $\newcommand{\dis}{\displaystyle}$ # $\renewcommand{\Im}{\operatorname{Im}}$ # $\newcommand{\im}{\operatorname{Im}}$ # $\newcommand{\bordermatrix}[3]{ \begin{matrix} \begin{matrix}#1\end{matrix} & \\ #3 & \hspace{-1em} \begin{matrix}#2\end{matrix} \end{matrix}}$ # $\newcommand{\fonction}[5]{#1\ \colon \left\{\begin{array}{ccl}#2 & \longrightarrow & #3\\#4 & \longmapsto & #5\end{array}\right.}$ # $\newcommand{\revdots}{\mathinner{\mkern1mu\raise1pt{\kern7pt\hbox{.}}\mkern2mu\raise4pt\hbox{.}\mkern2mu\raise7pt\hbox{.}\mkern1mu}}$ # $\newcommand{\tendvers}[2]{\xrightarrow[#1\to#2]{}}$ # $\newcommand{\q}[1]{\mathbf{Q#1}\ \triangleright}$ # In[1]: import random as rd import numpy as np from time import perf_counter as toc import matplotlib.pyplot as plt from scipy import stats #
# Exercice 1 : tri par sélection #
# $\q{1}$ Il n'y a que des boucles ```for``` dans cet algorithme. Cela assure sa $\myfbox{terminaison.}$ # $\q2$ On note $\mathscr P_{i} \colon$ "La liste ```L[0 : i]``` est triée et tous ses éléments sont plus petits ou égaux que les éléments de la liste ```L[i : n]```". Montrons qu'il s'agit d'un invariant de la boucle ```for``` dans l'algorithme de tri par sélection : # # - au début de la première itération, quand ```i=0```, la liste ```L[0 : i]``` est vide, donc triée, et tous ses éléments sont plus petits ou égaux que les éléments de la liste ```L[0 : n]```, qui est en fait la liste ```L```. $\mathscr P_0$ est donc vérifiée. # # - si $\mathscr P_{i}$ est vérifiée au début d'une itération, la liste ```L[0 : i]``` est triée et tous ses éléments sont plus petits ou égaux que les éléments de la liste ```L[i : n]```. Puisque ```j``` est l'indice du plus petit élément de la liste ```L[i : n]``` et que l'on vient le placer en position ```i``` grâce à l'échange ```L[i], L[j] = L[j], L[i]```, en fin d'itération, la liste ```L[0 : i+1]``` est triée et tous ses éléments sont plus petits ou égaux que les éléments de la liste ```L[i+1 : n]```. # À la fin de la dernière itération, la liste ```L[0 : n-1]``` est triée et tous ses éléments sont plus petits ou égaux que les éléments de la liste ```L[n-1 : n]```, qui est juste composée d'un unique élément ```L[n-1]```. # # Ainsi, $\myfbox{la liste L que l'on renvoie est triée}$. # $\q3$ Dans tous les cas (et donc en particulier dans le pire des cas), il y a $n-1$ itérations dans la boucle ```for``` lors de l'appel ```select(L)``` ( pour $i\in\ient 0,n-2\fient$). Chacune de ces itérations donne lieu à un appel à ```minpos(L,i)```, qui effectue $n-i-1$ comparaisons (une par itération pour chacun des $k\in\ient i+1,n-1\fient$). # # Il y a donc en tout : # $$ # \sum_{i=0}^{n-2} (n-i-1) = \sum_{i=1}^{n-1} i = \frac{n(n-1)}{2} = \myfbox{$O\big(n^2\big)$} \ \text{comparaisons.} # $$ # $\q4$ On reprend l'algorithme donné dans l'énoncé : # In[2]: def minpos(L, i): j = i m = L[i] for k in range(i + 1, len(L)): if L[k] < m: j = k m = L[k] return j # In[3]: def select(L): n = len(L) for i in range(n - 1): j = minpos(L, i) L[i], L[j] = L[j], L[i] return None # On reprend également la fonction ```gen(n, N)``` vue dans le dernier TP : # In[4]: def gen(n, N): return [rd.randint(0, N) for i in range(n)] # On peut maintenant écrire le script demandé : # In[5]: X = [np.log(i) for i in range(500, 1001, 5)] Y = [] for i in range(500, 1001, 5): L = gen(i, i) t1 = toc() select(L) t2 = toc() Y.append(np.log(t2 - t1)) X = X[10:] Y = Y[10:] fit = stats.linregress(X, Y) a, b, r = fit[:3] # on ne garde que les 3 premiers elements de la liste fit line = [a * X[i] + b for i in range(len(X))] plt.plot(X, Y, 'o', X, line, 'r-') plt.show() # In[6]: a, b # On a donc en particulier $a\simeq 2$, d'où : # $$ \ln(Y) \simeq 2\ln(X) + b$$ # donc : # $$ Y \simeq e^{b} \times X^2$$ # # Si l'on considère que la complexité est proportionnelle au temps de calcul, on obtient une complexité (en moyenne) quadratique : # $$ # \myfbox{$C(n) \simeq \text{cste}\times n^2$} # $$ #
# Exercice 2 : tri par insertion #
# $\q1$ ```i``` est un variant de la boucle ```while``` : il s'agit en effet d'une quantité à valeurs entières, strictement décroissante et minorée par $0$. Ceci assure la terminaison de la boucle ```while```. Puisque l'autre boucle est une boucle ```for```, cela assure la terminaison de l'algorithme. # $\q2$ On note $\mathscr P_k\colon$ "La liste ```L[0 : k]``` est triée". Montrons qu'il s'agit d'un invariant de la boucle ```for``` : # - au début de la première itération, quand ```k=1```, la liste ```L[0 : k]``` ne possède qu'un seul élément, donc est triée. # - si $\mathscr P_k$ est vérifiée au début de la $k^{\text{ème}}$ itération, alors la liste ```L[0 : k]``` est triée. La boucle ```while``` permet alors d'insérer ```L[k]``` dans sa bonne position dans la liste ```L[0 : k]```. La liste ```L[0 : k+1]``` est donc bien triée à la fin de l'itération. # $\q3$ En termes de nombres de comparaisons, le cas le pire est celui où la liste ```L``` est déjà triée par ordre décroissant. Il y a alors : # - $n-1$ itérations sur $k$ dans la boucle ```for``` (pour $k\in\ient 1,n-1\fient$) # - à chaque itération sur $k$, il y a $k$ itérations dans la boucle ```while``` (la condition ```L[i - 1] > elt``` étant toujours vérifiée), chacune des itérations donnant lieu à une comparaison (pour le ```if```). # # Le nombre total de comparaisons est donc : # $$ # \sum_{k=1}^{n-1} k = \frac{n(n-1)}{2} = \myfbox{$O\big(n^2\big)$} # $$ # In[7]: def insertion(L): n = len(L) for k in range(1, n): i = k elt = L[k] while i > 0 and L[i - 1] > elt: L[i] = L[i - 1] i -= 1 L[i] = elt return L # In[8]: X = [i for i in range(500,3001,50)] Y1 = [] Y2 = [] for i in range(500,3001,50): L = list(range(i)) t1 = toc() insertion(L) t2 = toc() Y1.append((t2-t1)) L.reverse() # on inverse la liste t1 = toc() insertion(L) t2 = toc() Y2.append((t2-t1)) X = X[10:] Y1 = Y1[10:] Y2 = Y2[10:] plt.plot(X, Y1, 'bo', label="liste triée par ordre croissant") plt.legend() plt.show() plt.plot(X, Y2, 'ro',label="liste triée par ordre décroissant") plt.legend() plt.show() # **Explication :** On a déjà vu que dans le cas le pire (liste triée par ordre décroissant); la complexité est en $O(n^2)$. Dans le meilleur des cas, celui d'une liste déjà triée par ordre croissant, le nombre de comparaisons effectuées est en $O(n)$ : il n'y a qu'une itération dans chaque boucle ```while```. #
# Exercice 3 : exponentiation rapide #
# $\q1$ On peut proposer l'algorithme naïf suivant : # In[9]: def naivepow(a, n): r = 1 for k in range(n): r *= a return r # In[10]: naivepow(3, 4) # $\q2$ Il y a $n$ itérations dans la boucle ```for```, et chaque itération donne lieu à $1$ multiplication. # # Le nombre de multiplications effectuées est donc $n$. # $\q3$ On implémente en Python l'algorithme proposé : # In[11]: def fastpow(a, n): r = 1 while n > 0: if n % 2 == 0: a *= a n = n // 2 else: r *= a n -= 1 return r # In[12]: fastpow(3,4) # $\q4$ Soit $k = \left\lfloor \log_{2}(n)\right\rfloor$. Alors $k$ vérifie : # $$ 2^k \leqslant n < 2^{k+1}$$ # # Au cours de 2 itérations successives de la boucle ```while```, l'entier $n$ est au moins divisé par $2$. # # Ainsi, si $p$ est le nombre d'itérations dans la boucle ```while```, alors $\frac{p}{2}$ est inférieur au nombre de fois où l'on peut diviser $n$ par $2$ avant d'obtenir un résultat nul, *i.e.* : # $$ p \leqslant 2(k+1)$$ # Finalement : # $$ p = O(k)$$ # Chaque itération donnant lieu à une multiplication, le nombre total de multiplications effectuées est en $O(k) = \myfbox{$O\big(\log_2(n)\big)$.}$ # $\q5$ On l'adapte en une fonction récursive. Le principe est le suivant : # $$\texttt{recfastpow(a, n)}= \begin{cases} # \texttt{recfastpow(a*a, n//2)}\quad\text{si $n$ est pair}\\ # a\times\texttt{recfastpow(a, n-1)}\quad\text{sinon} # \end{cases}$$ # avec le cas de base : # $$ # \texttt{recfastpow(a,0)} = 1 # $$ # In[13]: def recfastpow(a, n): if n == 0: return 1 else: if n % 2 == 0: return recfastpow(a*a, n // 2) else: return a * recfastpow(a, n - 1) # In[14]: recfastpow(3,4) # $\q6$ On compare les temps d'exécution des 3 versions pour le calcul de $3^n$ (j'ai même rajouté la comparaison avec la fonction ```pow()``` de Python) : # In[15]: N = 500 Yn = [] # implémentation naive Yf = [] # exponentiation rapide itérative Yr = [] # exponentiation rapide récursive Yp = [] # fonction native en Python X = list(range(0,N,50)) for n in X: t1 = toc() naivepow(3,n) t2 = toc() Yn.append((t2-t1)) t1 = toc() fastpow(3,n) t2 = toc() Yf.append((t2-t1)) t1 = toc() recfastpow(3,n) t2 = toc() Yr.append((t2-t1)) t1 = toc() pow(3,n) t2 = toc() Yp.append((t2-t1)) plt.plot(X, Yn, 'o', label="méthode naïve") plt.plot(X, Yf, 'o',label="exponentiation rapide") plt.plot(X, Yr, 'o',label="exponentiation rapide récursive") plt.plot(X, Yp, 'o',label="fonction pow() de Python") plt.legend() plt.show() #
# Exercice 4 : méthode de Hörner #
# $\q1$ On peut calculer la somme plus ou moins naïvement : # In[16]: def eval(A, x): s = 0 for k in range(len(A)): s += A[k] * x**k return s # In[17]: eval([1, -2, 1], 4) # $\q2$ On recalcule précédemment toutes les puissances de $x$ à partir de zéro. Pour éviter cela, on peut les stocker progressivement : # In[18]: def eval(A, x): s = A[0] p = x for k in range(1,len(A)): s += A[k] * p p *= x return s # In[19]: eval([1, -2, 1], 4) # $\q3$ Il y a $d$ itérations de la boucle ```for```, chacune donnant lieu à deux multiplications. # # Le nombre total de multiplications est donc $2d$. # $\q4$ On implémente la méthode de Hörner de manière itérative : # In[20]: def horner(A, x): d = len(A)-1 s = A[d] for k in range(1, d + 1): s = s * x + A[d - k] return s # In[21]: horner([1, -2, 1], 4) # $\q5$ Il y a $d$ itérations dans la boucle ```for```. Chacune donne lieu à une seule multiplication. # # Le nombre total de multiplications est donc exactement $d$. #
#
Bonus
# In[22]: N = 1000 Yn = [] # implémentation naive Yh = [] # methode de Horner X = list(range(N, 10*N, 200)) x = 3 for n in X: A = gen(n, n) t1 = toc() eval(A, x) t2 = toc() Yn.append((t2 - t1)) t1 = toc() horner(A, x) t2 = toc() Yh.append((t2 - t1)) X = X[20:] Yn = Yn[20:] Yh = Yh[20:] plt.plot(X, Yn, 'o', label="méthode naïve") plt.plot(X, Yh, 'o', label="méthode de Hörner") plt.legend() plt.show()