Cliquez sur cette invitation pour récupérer le repository du TP.
Considérons le code suivant, qui implémente un ancien algorithme égyptien.
a
et b
sont supposés être des entiers positifs.
def multegy(a, b):
p = 0
while a > 0:
if a%2 == 1:
p += b
b *= 2
a //= 2
return p
Qui est le variant de boucle permettant de prouver que
multegy
termine toujours ?
- a : a
- b : b
- c : p
- d : autre réponse
a
est une suite d'entiers positifs strictement décroissante, c'est donc un variant de boucle.
Détaillez l’éxécution de
multegy(23,5)
en affectant àa_i
,b_i
,p_i
, les valeurs rencontrées en début de chaque itération.
Ce code affiche :def multegy(a, b): p = 0 i = 0 while a > 0: print(f"itération {i} : {a = }, {b = }, {p = }") if a%2 == 1: p += b b *= 2 a //= 2 i += 1 print(f"itération {i} : {a = }, {b = }, {p = }") return p multegy(23,5)
itération 0 : a = 23, b = 5, p = 0
itération 1 : a = 11, b = 10, p = 5
itération 2 : a = 5, b = 20, p = 15
itération 3 : a = 2, b = 40, p = 35
itération 4 : a = 1, b = 80, p = 35
itération 5 : a = 0, b = 160, p = 115
Construisons la preuve de l’algorithme :
on va montrer que $a\times b + p$ est un invariant de boucle et l’utiliser pour prouver que l’algorithme retourne bien le produit entre $a$ et $b$.
Notons $a_k$, $b_k$, $p_k$, les valeurs de $a$, $b$ et $p$ après la ke itération et supposons $a_k\times b_k + p_k = cste$.
Que vallent X,Y et Z ?
- a : $a_k$, $b_k$, $p_k+b_k$
- b : $\frac{a_k-1}{2}$, $b_k\times 2$, $p_k+b_k$
- c : $\frac{a_k+1}{2}$, $b_k\times 2$, $p_k$
$\displaystyle X = \frac{a_k+1}{2}$, $Y = b_k\times 2$, $Z = p_k+b_k$
Quelle est la complexité de
multegy
(en supposant chacun des calculs comme élémentaire) ?
- a : $O(a)$
- b : $O(a\times b)$
- c : $O(a\log b)$
- d : $O(\log a)$
- e : $O(b)$
Il y aura autant de tours dans la bouclewhile
que le nombre de fois qu'on peut divisera
par 2 (division euclidienne) avant d'arriver sur 0. Ce nombre est en $O(\log a)$.
Et à chaque tour, il y a soit $c$ étapes, soit $c+1$ où $c$ est une constante.
La complexité globale reste donc en $O(\log a)$.
def cherche(s, m):
for k in range(len(s) - len(m) + 1):
b = True
for i in range(len(m)):
if s[k + i] != m[i]:
b = False
if b:
return True
return False
def cherche2(s, m):
for k in range(len(s) - len(m) + 1):
if s[k:k + len(m)] == m:
return True
return False
Quelle est la complexité de la fonction cherche ? Et celle de cherche2 ?
Appelonslen(s)
n etlen(m)
p.
- a : les deux en $O(n\times p)$
- b :
cherche
en $O(n\times p)$ etcherche2
en en $O(n)$- c : une autre réponse
Les deux sont en $O(n\times p)$.
Il ne faut pas simplement compter les imbrications de boucles (même si c'est souvent suffisant) ; dans le cas decherche2
, $m$ étapes sont cachées dans la comparaisons[k:k + len(m)] == m
puisque l'interprète python ne peut faire autrement que de comparer les caractères un par un pour s'assurer que les deux chaînes sont bien les mêmes.
Exécuter le code suivant peut vous aider à confirmer votre réponse.
import time
from random import randint
abc = 'abcdefghijklmnopqrstuvwxyz'
def motdenlettres(n):
mot = ''
for i in range(n):
mot += abc[randint(0,25)]
return mot
print('-'*30)
print('| n | p | cherche2 |')
print('-'*30)
for j in range(2,6):
n = 10**5*2**j
s = motdenlettres(n)
for k in range(3):
p = 10**4*2**k
m = motdenlettres(p)
d = time.time()
cherche2(s,m)
f = time.time()
t = f - d
print(f'|{n:^9d}|{p:^8d}|{t:^10.2E}|')
print('-'*30)
On constate bien sur le tableau que pour un $n$ fixé, le temps de calcul de cherche2
semble proportionnel à $p$, et que pour un $p$ fixé, il semble proportionnel à $n$...
Quelle est la complexité au meilleur de la fonction cherche ?
- a : $O(n)$
- b : $O(p)$
- c : $O(1)$
Le meilleur des cas possible correspond à un mot à cherché placé au tout début du texte. Il faudra alors $p$ comparaisons (la longueur du mot) avant de sortir de la fonction.
D'où $O(p)$ au meilleur.
On peut déterminer ce que fait la fonction cherche
en mettant en lumière deux invariants, un pour chaque boucle.
La boucle interne a pour invariant “b
équivaut à s[k:k+i]==m[:i]
”.
Et la boucle externe a pour invariant “s[j:j+len(m)] != m pour tout j<k
” car s’il y avait égalité, on serait sorti de la boucle avec le return True
.
On en conclut que si la boucle n’est jamais interrompue par le return True
alors m
n’est pas un sous-mot de s
.
Si la boucle est interrompue, d’après l’invariant de la boucle intérieure, on a trouvé m
dans s
.
Vous trouverez des exemples de calcul de complexité et une démonstration de correction dans la section recensant les défis algorithmiques ( le défi est souvent de réussir à diminuer la classe de complexité en passant d'un algo naïf à un algo plus malin).