Algorithmique - IC Lycée Paul Constans

publicité
PTSI – IC
A LGORITHMIQUE
I. Terminaison et preuve d’algorithmes
1) Terminaison : variant de boucle
On considère l’algorithme suivant :
Entrées : n ∈ N
Sorties : r = n!
r ←1
k ←n
tant que k > 0 faire
r ← r ×k
k ← k −1
On est sûr que ce programme va se terminer car :
• k est un entier positif.
• La valeur de k diminue strictement à chaque itération.
Or, il n’existe pas de suite d’entiers naturels strictement décroissante infinie : il ne peut donc y avoir qu’un nombre fini
d’itérations.
Plus généralement, pour prouver qu’une boucle va se terminer, on introduit une quantité qui vérifie les deux propriétés :
être un entier naturel et décroître strictement à chaque itération. Cette quantité s’appelle un variant de boucle.
2) Preuve : invariant de boucle
Une preuve d’algorithme par invariant de boucle utilise la démarche suivante :
• On prouve tout d’abord que l’algorithme s’arrête, par exemple en utilisant un variant de boucle ;
• On exhibe un invariant de boucle, c’est-à-dire une propriété P qui, si elle est vraie avant l’exécution d’un tour de
boucle, est aussi valide après l’exécution du tour de boucle ;
• On vérifie que les conditions initiales rendent la propriété P vraie en entrée du premier tour de boucle ;
• On en conclue que cette propriété est vraie en sortie du dernier tour de boucle.
Un bon choix de la propriété P prouvera qu’on a bien produit l’objet recherché. La difficulté de cette méthode réside dans
la détermination de la propriété P. Quand on l’a trouvé il est en général simple de montrer que c’est bien un invariant de
boucle.
3) Exemples
a) Calcul de n!
On a déjà prouvé que l’algorithme du 1) se termine.
Notons r i et k i les valeurs de r et k après la i -ème itération, et P(i ) la propriété r i × k i ! = n!.
Initialisation : r 0 = 1 et k 0 = n d’où r 0 × k 0 ! = n! donc P(0) est vraie.
Hérédité : Supposons que P(i ) est vraie pour un certain entier i et que l’on effectue une itération de plus.
Alors r i +1 = r i × k i et k i +1 = k i − 1 d’où r i +1 × k i +1 ! = r i × k i × (k i − 1)! = r i × k i = n! par P(i ). P(i + 1) est donc vraie.
Conclusion : Après le dernier tour de boucle, k i = 0 et comme r i × k i ! = n!, on en déduit que r i = n!.
b) Calcul de x n
On considère l’algorithme « naïf » de calcul de puissance suivant :
Entrées : x ∈ R∗ et n ∈ N
Sorties : r = x n
r ←1
tant que n > 0 faire
r ←r ×x
n ← n −1
Démontrer que cet algorithme se termine et est correct.
1/5
DELAY – Paul Constans – 2016 - 2017
PTSI – IC
A LGORITHMIQUE
c) Exponentiation rapide
(
Pour accélérer le calcul de x n (où x ∈ R et n ∈ N), on se propose d’exploiter les identités
x 2n = (x 2 )n
qui montrent
x 2n+1 = x(x 2 )n
comment calculer une puissance en remplaçant x par son carré et l’exposant par sa moitié. L’algorithme est le suivant :
Entrées : x ∈ R∗ et n ∈ N
Sorties : r = x n
r ←1
tant que n > 0 faire
si n est impair alors
r ←r ×x
x ← x ×x
¥ ¦
n ← n2
1. Établir la terminaison de cet algorithme.
2. Démontrer que cet algorithme est correct.
3. Cet algorithme est dit d’exponentiation rapide. Pour comprendre pourquoi, évaluer combien de multiplications il
effectue et comparer avec la version naïve du b).
d) Division Euclidienne
Démontrer que l’algorithme suivant se termine, et est correct :
Entrées : a et b entiers naturels, b 6= 0
Sorties : le quotient q et le reste r de la division euclidienne de a par b
q ←0
r ←a
tant que r Ê b faire
r ← r −b
q ← q +1
e) Algorithme d’Euclide et théorème de Bézout
Démontrer que l’algorithme suivant se termine, et est correct :
Entrées : x, y deux entiers naturels non nuls.
Sorties : d = PGCD(x, y) et (m, n) ∈ Z2 tels que mx + n y = d .
a ←x;b ← y
m ← 1; n ← 0
k ← 0; l ← 1
tant que a 6= b faire
si a > b alors
a ← a −b
m ← m −k
n ← n −l
sinon
b ←b−a
k ← k −m
l ← l −n
d ←a
II. Complexité d’un algorithme
1) Introduction
Il existe souvent plusieurs algorithmes différents pour résoudre un même problème, qui peuvent être plus ou moins
rapide, et demander plus ou moins de place en mémoire. Il est alors intéressant de savoir choisir l’algorithme le plus
2/5
DELAY – Paul Constans – 2016 - 2017
PTSI – IC
A LGORITHMIQUE
rapide, ou le moins gourmand en mémoire, selon les contraintes que l’on a.
Par exemple, pour déterminer l’ensemble des diviseurs d’un nombre, on peut commencer par l’algorithme naïf :
Entrées : Un entier n
Sorties : Affichage de tous les diviseurs de n
pour p variant de 1 à n faire
si p divise n alors Afficher p
Cet algorithme fait n tests de divisibilité et au maximum n affichages. Mais on peut nettement améliorer cet algorithme
en remarquant qu’à chaque fois qu’on trouve un diviseur p de n, alors q = n/p est aussi un diviseur de n. On obtient
l’algorithme suivant :
p
pour p variant de 1 à n faire
si p divise n alors
Afficher p
q ← n/p
si q 6= p alors Afficher q
p
Cet algorithme demande un calcul de racine carrée, puis réalise n itérations avec dans chacune 1 test de divisibilité,
entre 0 et 2 affichages, 0 ou 1 division, 0 ou 1 affectation et 0 ou 1 test.
En général, on cherche à savoir comment le temps d’exécution, ou la place occupée en mémoire, d’un algorithme évolue
en fonction d’un paramètre que l’on appelle la taille du problème. Dans l’exemple précédent, il parait naturel de prendre
n comme taille du problème. On voit alors que le temps d’exécution est proportionnel à n dans le premier algorithme,
p
alors qu’il est proportionnel à n dans le second.
Le point important est que, si la taille du problème est multipliée par 100, le temps de calcul sera multiplié par 100 dans
le premier cas, mais seulement par 10 dans le deuxième.
2) Évaluation de la complexité
Pour déterminer le coût d’un algorithme, nous nous fonderons en général sur le modèle de complexité suivant :
• Une affectation, une comparaison ou l’évaluation d’une opération arithmétique ayant en général un faible temps
d’exécution, nous le considérerons comme l’unité de base dans laquelle on mesure le coût d’un algorithme.
• Le coût de deux instructions p et q l’une après l’autre est la somme des coûts des instruction p et q.
• Le coût d’un test « Si b alors p sinon q » est inférieur ou égal au maximum des coûts des instructions p et q, plus une
unité qui correspond au temps d’évaluation de l’expression b.
• Le coût d’une boucle « Pour i variant de 1 à n faire p » est égale à n fois le coût de l’instruction p si ce coût ne dépend
pas de la valeur de i . Quand le coût du corps de la boucle dépend de la valeur du compteur i , le coût total de la boucle
est la somme des coûts du corps de la boucle pour chaque valeur de i .
• Le cas des boucles conditionnelles est plus complexe à traiter puisque le nombre de répétitions n’est en général pas
connu a priori. On peut majorer le nombre de répétitions de la boucle de la même façon qu’on démontre sa terminaison et ainsi majorer le coût de l’exécution de la boucle.
Exercice 1 : Déterminer la complexité des algorithmes du paragraphe I.
3) Notation O(n)
Dans la pratique, il est souvent difficile, et inutile, de compter précisément le nombre d’opérations effectuées par un
p
algorithme. On se contente en général de dire que le nombre d’instruction est proportionnel à n, où à n. En effet, le
temps d’exécution dépend de la vitesse de l’ordinateur sur lequel on implante l’algorithme, du langage utilisé, . . .
Un autre point important est la notion de terme dominant. Supposons que l’on trouve que le temps d’exécution est
proportionnel à n 2 +3n. Alors dès que n Ê 3, n 2 Ê 3n et n 2 É 2n 2 . Le temps d’exécution est donc finalement proportionnel
à n 2 . On dit que la quantité n 2 + 3n est un grand O de n 2 , ce que l’on écrit n 2 + 3n = O(n 2 ). La notion de grand O sera
détaillée en cours de maths.
De manière générale, on dit qu’un algorithme a une complexité en O( f (n)) si son temps d’exécution est, à partir d’un
certain rang, inférieur au produit de f (n) par une constante.
3/5
DELAY – Paul Constans – 2016 - 2017
PTSI – IC
A LGORITHMIQUE
Exemples de temps d’exécution :
Nom courant
Temps
n = 106
pour
O(1)
temps constant
1 ns
O(log n)
logarithmique
10 ns
O(n)
linéaire
1 ms
O(n 2 )
quadratique
1/4 h
O(n k )
polynomiale
30 ans si k = 3
exponentielle
plus de 10300 000
milliards d’années
n
O(2 )
Remarques
Le temps d’exécution ne dépend pas des données traitées, ce qui est
assez rare ! En particulier, la plupart des données ne sont même pas
lues.
En pratique, cela correspond à une exécution quasi instantanée. Bien
souvent, à cause du codage binaire de l’information, c’est en fait la
fonction log2 n qu’on voit apparaître ; mais comme la complexité est
définie à un facteur près, la base du logarithme n’a pas d’importance.
Le temps d’exécution d’un tel algorithme ne devient supérieur à une
minute que pour des données de taille comparable à celle des mémoires vives disponibles actuellement. Le problème de la gestion de
la mémoire se posera donc avant celui de l’efficacité en temps.
Cette complexité reste acceptable pour des données de taille
moyenne (n < 106 ) mais pas au delà.
Ici n k est le terme de plus haut degré d’un polynôme en n ; il n’est pas
rare de voir des complexités en O(n 3 ) ou O(n 4 ).
Un algorithme d’une telle complexité est impraticable sauf pour de
très petites données (n < 50). Comme pour la complexité logarithmique, la base de l’exponentielle ne change fondamentalement rien
à l’inefficacité de l’algorithme.
Exercice 2 : Déterminer le rôle des algorithmes suivants, et donner la complexité de chacun :
1. Algo 1 :
Entrées : n ∈ N
pour i variant de 1 à 10 faire
Afficher i × n
2. Algo 2 :
Entrées : n ∈ N
pour i variant de 1 à n faire
Afficher i × i
3. Algo 3 :
Entrées : n ∈ N
pour i variant de 1 à n faire
pour j variant de 1 à n faire
Afficher i × j
Aller à la ligne
4) Différentes nuances de complexité
a) Complexité dans le pire des cas
Pour deux données de même taille, un algorithme n’effectue pas nécessairement le même nombre d’opérations élémentaires. Par exemple, considérons l’algorithme de test de primalité :
Entrées : n ∈ N∗
Sorties : premier : un booléen égal à vrai si n est premier, faux sinon
premier ← vrai
¥p ¦
pour i variant de 2 à
n faire
si i divise n alors
premier ← faux
quitter la boucle
¥p ¦
Si n est un nombre premier, alors il faudra
n − 1 itérations ; mais pour n − 1 et n + 1 qui sont pairs, le programme
s’arrête dès la première itération.
4/5
DELAY – Paul Constans – 2016 - 2017
PTSI – IC
A LGORITHMIQUE
Si la fonction f caractérise l’efficacité d’un algorithme, on veut avoir l’assurance que l’exécution du programme sera
terminée en un temps proportionnel à f (n), éventuellement moins mais pas plus. On cherche donc un majorant du
temps d’exécution, autrement dit on retiendra le pire des cas pour exprimer la complexité.
b) Complexité dans le meilleur des cas
La complexité au pire est la plus significative, mais dans certains cas, il peut être utile de connaître aussi la complexité
dans le meilleur des cas, pour avoir une borne inférieure du temps d’exécution d’un algorithme.
En particulier, si la complexité dans le meilleur et dans le pire des cas sont du même ordre, cela signifie que le temps
d’exécution de l’algorithme est relativement indépendant des données et ne dépend que de la taille du problème.
c) Complexité en moyenne et complexité amortie
Certains algorithmes ont des temps d’exécution très différents suivant les données d’entrées. Par exemple, certains algorithmes pour trier un tableau de taille n prennent un temps proportionnel à n si le tableau est trié et proportionnel à
n 2 dans le cas le pire. On s’intéresse donc parfois à la complexité en moyenne d’un algorithme.
Parler de moyenne des temps d’exécution n’a de sens que si l’on a une idée de la fréquence des différentes données
possibles pour un même problème de taille n. Les calculs de complexité moyenne sont très difficiles dans le cas général,
mais peuvent parfois être intéressants si on connaît à l’avance la fréquence d’apparition des différents types de données.
Par ailleurs, il existe des problèmes où le cas le pire peut se produire mais où, sur des exécutions répétées, on a la certitude que celui-ci ne se produira que peu fréquemment. Prenons l’exemple d’une personne désirant envoyer un SMS
depuis un téléphone mobile. Quelle est la complexité en temps de cet envoi ? Si tout se passe bien, la rédaction et l’envoi se font en 2 minutes. En revanche, dans le cas le pire, la batterie du téléphone est déchargée, il convient donc de la
recharger pendant 4 heures. La complexité dans le cas le pire pour un envoi est donc de 4 heures et 2 minutes.
Mais la complexité pour n envois, où n est grand, est bien inférieure à 4n heures et 2n minutes. En effet, une fois le
téléphone chargé, son utilisateur va pouvoir envoyer un millier de SMS avant de devoir recharger la batterie. On peut
n
donc dire que dans le cas le pire, l’envoi de n SMS successifs va demander ( 1000
+ 1) × 4 heures plus 2n minutes. On a
n
donc la garantie que pour n grand, le temps mis pour envoyer n SMS est au plus de l’ordre de 1000
× 4 × 3600 = n × 14, 4
secondes plus 2n minutes. Autrement dit, le temps mis pour envoyer un SMS est de l’ordre de 2 minutes et 14 secondes.
On dit que cette durée est la complexité amortie représentant le coût de l’envoi. La notion d’amortissement vient de
la comptabilité : le coût d’un kilomètre en voiture est nul si la voiture fonctionne et que le plein est fait alors qu’il est
extrêmement élevé s’il faut commencer par acheter la voiture.
La notion pertinente pour mesurer ce coût est en général de calculer l’amortissement des dépenses initiales (l’achat de
la voiture) sur la totalité du kilométrage. C’est une situation qu’on retrouve en informatique : il arrive ainsi que, dans
certains problèmes, une opération ait un coût O(n) dans le cas le pire, où n est la taille du problème, et un coût constant
en complexité amortie. En Python, l’opération d’ajout d’un nouvel élément à un tableau de taille n à la fin de ce tableau
rentre dans ce cadre. Cependant, la théorie de la complexité amortie dépasse le cadre du programme.
d) Complexité en espace
Jusqu’ici nous avons uniquement discuté du temps d’exécution des algorithmes. Une autre ressource importante en
informatique est la mémoire. On appelle complexité en espace d’un algorithme la place nécessaire en mémoire pour
¡
¢
faire fonctionner cet algorithme. Elle s’exprime également sous la forme d’un O f (n) où n est la taille du problème.
Évaluer la complexité en espace d’un algorithme ne pose la plupart du temps pas de difficulté, il suffit de faire le total des
tailles en mémoire des différentes variables utilisées. La seule vraie exception à la règle est le cas des fonctions récursives,
qui cachent souvent une complexité en espace élevée, et sur lequel on reviendra en deuxième année.
5/5
DELAY – Paul Constans – 2016 - 2017
Téléchargement