Algorithmes et programmation 4 janvier 2015 1 Introduction 1.1 Algorithme et programme Un algorithme est une suite d’opérations élémentaires constituant un schéma de calcul et de résolution en un temps fini (et acceptable) d’un problème donné. — Les opérations élémentaires correspondent à une suite d’instructions visant à tranformer les données pour obtenir le résultat. — Un algorithme est indépendant de tout langage de programmation. Il est écrit en pseudo-langage. Un programme est l’implémentation (ou implantation) d’un algorithme dans un langage de programmation donné. Un algorithme n’est pas un programme. Par exemple, dans un algorithme, on ne précise pas comment sont formatées les variables utilisées (polynôme, tableau...). 1.2 Portée des variables — Une variable locale est une variable utilisée comme paramètre d’une procédure, ou qui est affectée à l’intérieur d’une procédure. Une fois la procédure exécutée, la variable locale a perdu la valeur qu’elle avait au sein de la procédure. — On dit que la portée d’une variable locale est limitée au corps de la procédure où elle est utilisée. — Une variable globale est une variable utilisée à l’intérieur d’une fonction sans y être affectée ou qui est déclarée comme globale à l’intérieur d’une procédure. — On dit que la portée d’une variable globale s’étend à l’ensemble du programme. 1 def f ( ) : global a a=a+1 c=2*a return a+b+c Dans ce programme, a et b sont globales mais c est locale. x=1 def plusun ( ) : x+=1 return x ERREUR car, x étant affecté à l’intérieur de la procédure, x est considérée comme variable locale qui n’est donc pas initialisée donc ’x=x+1’ donne une erreur. Par contre x=1 def plusun ( ) : return 1+x renvoie 2. x est considérée comme variable globale. def f ( ) : global x x=2 def g ( ) : x=3 Ici, on a deux variables x indépendantes : une globale dans f et une locale dans g. Dans g, on ne peut plus accéder à la variable globale x car masquée par la variable locale x. x=1 f() g() print x affiche 2. On utilisera les variables globales pour les constantes du problème. On évitera d’avoir à recourir à l’instruction global . On évitera de donner des noms de variables identiques aux variables locales et aux variables globales. On donnera des noms de variables longs et explicites pour les variables globales, et des noms courts pour les variables locales. Pour écrire un algorithme, on doit procéder de la manière suivante : 1. définir la spécification (ie le contrat) formée de : — La précondition, — La postcondition, 2 2. écrire l’algorithme 3. Justifier sa correction composée de : — la correction partielle (sous réserve de terminaison) — La terminaison . — Le couple précondition/postcondition représente un contrat : on garantie que sous réserve que la précondition est vérifiée au début, la postcondition le sera à la fin. — Justifier la correction partielle du programme consiste à démontrer que l’algorithme respecte le contrat pour toutes les valeurs pour lesquelles l’algorithme termine. Dans le cas d’une boucle ’for’ ou ’while’ : — définir un invariant de boucle pour une boucle for ou while qui est une propriété qui est vraie à l’entrée de la boucle et qui, si elle est vraie en début de tour de boucle, est vraie à la fin du tour. Dans ce cas, à la sortie du dernier tour de boucle, on obtient le résultat cherché. — justifier cet invariant de boucle. — Prouver la correction partielle du programme ie montrer que (Precond + condition d’arrêt de la boucle ⇒ Postcond ). Pour une boucle ’while cond’, la condition d’arrêt est ’non cond’ Pour une boucle ’for i in range(n)’, la condition d’arrêt est ’i=n-1’. La question de la terminaison ne se pose que pour une boucle «tant que condition ». On la justifie en : — définissant un variant de boucle qui est une valeur entière, positive et décroissant strictement à chaque tour de boucle ; — justifiant ce variant de boucle pour en déduire la terminaison (ie condition fausse) 2 Exemples classiques 2.1 Division euclidienne Calculer quotient et reste d’une division euclidienne en utilisant pour seules opérations arithmétiques l’addition et la soustraction. 2.1.1 Spécifier le problème Étant donné deux entiers n et d, avec d > 0 et n ≥ 0, on veut retourner l’unique couple d’entiers (q, r) tel que n = dq + r et 0 ≤ r < d. 3 2.1.2 Écrire un algorithme 1 2 3 4 5 6 Précondition: n ∈ N et d ∈ Z∗ Postcondition: q ∈ N, r ∈ [[0, d[[ et n = dq + r q ← 0; r ← a; tant que r ≥ d faire r ← r − d; q ←q+1 fin 2.1.3 Correction partielle d’un algorithme Ajout d’un invariant : Précondition: n ∈ N et d ∈ Z \ { 0 } Postcondition: q ∈ N, r ∈ [[0, d[[ et n = dq + r 1 2 3 4 5 6 7 q ← 0; r ← n; tant que r ≥ d faire invariant n = dq + r; r ← r − d; q ←q+1 fin Justification de l’invariant L’invariant de boucle «tant que» ligne 4 est vérifié car 1. Quand on arrive à la boucle, q = 0 et r = n, donc n = dq + r ; 2. Si l’invariant est vérifié au début d’un tour de boucle alors il est encore vérifié à la fin de ce même tour. En effet, si on note qk et rk les valeurs de q et r au début d’un tour de boucle, on a rk+1 = rk − d et qk+1 = qk + 1 donc dqk+1 + rk+1 = dqk + d + rk − d = dqk + rk = n ce qui justifie l’invariant. Correction partielle du programme À la sortie de la boucle on a donc n = dq + r et r < d. (Il nous manque r ≥ 0 : il aurait suffit de l’ajouter dans l’invariant de boucle). Conclusion Si la précondition est vérifiée au début de l’exécution de l’algorithme alors la postcondition est vérifiée à la fin. . . sous réserve que cette exécution se termine ! L’algorithme est donc partiellement correct. Remarquez que notre algorithme ne termine pas toujours : prendre d = −1 par exemple ! 4 2.1.4 Justifier la terminaison d’un algorithme (impératif) Risque de non-terminaison : les boucles «tant que». Renforçons la précondition : on prendra d ∈ N∗ . Précondition: n ∈ N et d ∈ N∗ Postcondition: q ∈ N, r ∈ [[0, d[[ et n = dq + r 1 2 3 4 5 6 7 8 q ← 0; r ← a; tant que r ≥ d faire invariant n = dq+r; variant r - d; r ← r − d; q ←q+1 fin Sous la précondition n ∈ N, r et d ont des valeurs entières. De plus, la condition de boucle est r ≥ d donc dans le corps de la boucle, r − d est toujours positif. Enfin, r diminue de d à chaque tour de boucle et d > 0 (précondition d ∈ N∗ ). Donc r − d est un entier positif diminuant strictement à chaque tour de boucle. 2.1.5 Programme def diveuclide ( n , d ) : " " " Retourne un c o u p l e ( q , r ) avec q n a t u r e l , 0 <= r < d e t n == d * q + r Pr é c o n d i t i o n : n e n t i e r n a t u r e l , d n a t u r e l non nul . " " " q = 0 r = a while r >= d : #n == d * q + r # v a r i a n t : r−d r = r − d q = q + 1 return q , r 2.2 Recherche du minimum d’un tableau 2.2.1 Spécification On veut rechercher l’indice du minimum d’un tableau. Étant donné un tableau de nombres t non nécessairement trié contenant au moins un élément, retourner un indice i ∈ [[0, len(t)[[ tel que t[i] = min j∈[[0,len(t)[[ 5 (t[j]) 2.2.2 Algorithme Précondition: t tableau avec len(t) > 0 Postcondition: t[i] = minj∈[[0,len(t)[[ (t[j]) 1 2 3 4 5 6 7 8 9 i ← 0; m ← t[0]; pour k de 1 (inclus) à len(t) (exclu) faire invariant m = t[i] = minj∈[[0,k[[ (t[j]); si t[k] < m alors i ← k; m ← t[k]; fin fin 2.2.3 Justification de l’invariant Ici, notons, pour k entier non nul, P (k) la proposition « m = t[i] = minj∈[[0,k[[ (t[j]) ». 1. On a clairement P (1) est vrai quand on arrive à la boucle (ligne 3) puisqu’alors on a i = 0 et m = t[0], donc m = t[i] = t[0] = minj∈[[0,1[[ (t[j]). 2. Si au début d’un tour de boucle, on a P (k), alors ou bien t[k] < m = minj∈[[0,k[[ (t[j]) alors à la fin du tour, i = k, donc m = t[i] = t[k] = minj∈[[0,k+1[[ (t[j]) ou alors m ≥ t[k] et alors m = t[i] = minj∈[[0,k+1[[ (t[j]). Dans les deux cas, on a P (k + 1) à la fin du tour. Donc on a P (len(t)) à la fin de l’exécution de la boucle, qui est exactement la postcondition que l’on voulait montrer. L’algorithme est donc (totalement) correct. 2.2.4 Programme def iminimum ( t ) : " " " Retourne un i n d i c e i t e l que t [ i ] s o i t l e minimum de t . Pr é c o n d i t i o n : t e s t un t a b l e a u non−v i d e de nombres " " " i=0 m = t [0] f o r k in range ( 1 , len ( t ) ) : # t [ i ] == min ( t [ j ] f o r j i n r a n g e ( k ) ) i f t [ k ] <m : m = t[k] i = k return i 6 2.3 Pgcd 2.3.1 Algorithme Étant donné un entier relatif α, on note D(α) l’ensemble de ses diviseurs. En particulier que D(0) = Z. Étant donnés deux entiers α et β, on note D(α, β) leurs diviseurs communs : D(α, β) = D(α) ∩ D(β). Précondition: (a, b) ∈ Z2 avec (a, b) 6= (0, 0) Postcondition: R0 = a ∧ b 1 2 3 4 5 6 7 8 R0 ← |a|; R1 ← |b|; tant que R1 > 0 faire invariant D(R0 , R1 ) = D(a, b) et R0 ∈ N et R1 ∈ N et (R0 , R1 ) 6= (0, 0); variant R1 ; (q, R2 ) ← diveuclide(R0 , R1 ); (R0 , R1 ) ← (R1 , R2 ); fin 2.3.2 Invariant L’invariant de boucle est vérifié car : 1. D’après la précondition, (a, b) ∈ Z2 . Donc D(R0 , R1 ) = D(|a|) ∩ D(|b|) = D(a, b) et R0 = |a| ∈ N et R1 = |b| ∈ N et (R1 , R0 ) = (|a| , |b|) 6= (0, 0). 2. Supposons qu’au début d’un tour de boucle l’invariant est vérifié et notons (α, β) la valeur de (R0 , R1 ) au début du tour, (α0 , β 0 ) sa valeur à la fin du tour et γ et δ le quotient et le reste dans la division euclidienne de α par β. On a α = γβ + δ et (α0 , β 0 ) = (β, δ) , donc on a α0 ∈ N et β 0 ∈ N et de plus D(α0 , β 0 ) = D(β, α − γβ) = D(α, β) = D(a, b). Donc à la fin du tour, on a D(R0 , R1 ) = D(a, b). Enfin, comme on a effectué ce tour de boucle c’est que la condition du "while" est vérifiée, donc β > 0, donc α0 > 0, donc (R0 , R1 ) 6= (0, 0). 2.3.3 Correction partielle En fin d’exécution de la boucle «tant que», la condition de boucle n’est plus vérifiée, donc on a R1 ≤ 0. De plus on a D(R0 , R1 ) = D(a, b) et R1 ∈ N et R0 ∈ N. Donc on a R1 = 0 et D(a, b) = D(R0 , 0) = D(R0 ). Donc R0 = a ∧ b. On a donc la correction partielle. 2.3.4 Terminaison Comme on l’a vu, R1 est positif au début de chaque tour de boucle. De plus lors d’un tour de boucle, R1 voit sa valeur β remplacée par un reste dans la division euclidienne par β, donc décroît strictement. 7 D’où la correction totale de l’algorithme. 2.3.5 Programme def pgcd ( a , b ) : " " " Retourne l e pgcd de a e t b . Pr é c o n d i t i o n : a e t b e n t i e r s r e l a t i f s non t o u s l e s deux n u l s " " " R0=abs ( a ) R1=abs ( b ) while R1 >0: # D( R0 , R1)=D( a , b ) e t R0 , R1 e n t i e r s n a t u r e l s q , R2=diveuclide ( R0 , R1 ) R0 , R1=R1 , R2 return R0 2.4 Produit matriciel 2.4.1 Algorithme Étant données deux matrices carrées A et B d’ordre n, il s’agit de définir la matrice produit A × B définie par X ∀i, j ∈ [[0, n[[, (AB)i,j = (A)i,k (B)k,j k∈[[0,n[[ NB : on prendra les indices de ligne et colonne dans [[0, n[[ (contrairement à l’usage habituel en mathématiques). 1 2 3 4 5 6 7 8 Précondition: A et B sont deux tableaux bidimensionnels de n2 réels. Postcondition: C = A × B. Initialiser un tableau C de dimensions n × n; pour i de 0 (inclus) à n (exclu) faire invariant C[i0 , .] vaut la ligne i0 de AB pour tout i0 ∈ [[0, i[[; pour j de 0 (inclus) à n (exclu) faire invariant C[i, j 0 ] vaut l’élément (i, j 0 ) de AB pour tout j 0 ∈ [[0, j[[; C[i, j] ← 0; pour k de 0 à n (exclu) faire P invariant C[i, j] = A[i, p]B[p, j]; p∈[[0,k[[ C[i, j] ← C[i, j] + A[i, k] × B[k, j] 9 fin 10 fin 11 12 fin 2.4.2 Explications — L’invariant annoncé pour la boucle la plus interne (celle sur k) est C[i, j] = X p∈[[0,k[[ 8 A[i, p]B[p, j] Il est vrai au début du tour de boucle pour k = 0 car on a initialisé C[i, j] à 0 et la somme est vide donc nulle. De plus s’il est vrai au début d’un tour de boucle quelconque (avec k ∈ [[0, n[[), alors on exécute C[i, j] ← C[i, j] + A[i, k] × B[k, j] donc à la fin du tour, C[i, j] vaut C[i, j] = X A[i, p] × B[p, j] + A[i, k] × B[k, j] p∈[[0,k[[ = X A[i, p] × B[p, j] p∈[[0,k+1[[ L’invariant est donc vérifié, et à la sortie de cette boucle la plus interne, on a C[i, j] = X A[i, p]B[p, j] = (AB)i,j p∈[[0,n[[ — L’invariant de la boucle «pour j» est vrai au début du tour où j = 0 car alors [[0, j[[ est vide. De plus si au début d’un tour de boucle, C[i, j 0 ] = (AB)i,j 0 pour tout j 0 ∈ [[0, j[[, alors à la fin du tour du boucle, on a de plus C[i, j] = (AB)i,j , donc on a bien C[i, j 0 ] = (AB)i,j 0 pour tout j 0 ∈ [[0, j + 1[[. En particulier à la fin de chaque exécution de la boucle, C[i, j 0 ] = (AB)i,j 0 pour tout j 0 ∈ [[0, n[[, donc C[i, .] vaut la ligne i de AB. — De la même façon, l’invariant de la boucle «pour i» est vrai au début du tour où i = 0 car [[0, i[[ est alors vide. De plus si au début d’un tour de boucle, C[i0 , .] vaut la ligne i0 de AB pour tout i0 ∈ [[0, i[[, alors à la fin de ce tour, on a de plus C[i, .] égal à la ligne i de AB, donc C[i0 , ] vaut la ligne i0 de AB pour tout i0 ∈ [[0, i + 1[[. En particulier, à la fin de l’exécution de la boucle la plus externe, on a C[i0 , .] égal à la ligne i0 de AB pour tout i0 ∈ [[0, n[[. C est donc égal à AB. 2.4.3 Programme def prodmat ( A , B ) : " " " Retourne l e p r o d u i t m a t r i c i e l de A e t B . Pr é c o n d i t i o n : A e t B s o n t deux m a t r i c e s c a r r é e s de même t a i l l e n . " " " n = len ( A [ 0 ] ) C = [ None ] * n f o r i in range ( n ) : C [ i ] = [ None ] * n f o r j in range ( n ) : C[i , j] = 0 f o r k in range ( n ) : C [ i , j ] += A [ i , k ] * B [ k , j ] return C 9 2.5 Exercices Prouver la correction de l’algorithme suivant de calcul de a.b : 1 2 3 4 5 6 7 8 9 10 Précondition: a et b deux entiers naturels Postcondition: p = ab x ← a; y ← b; p ← 0; tant que y > 0 faire si y impair alors p←p+x fin x ← 2 ∗ x; y ← E(y/2) fin Prouver la correction de l’algorithme suivant de calcul de ap : 1 2 3 4 5 6 7 8 9 10 Précondition: a un nombre et p un entier naturel Postcondition: r = a ∗ ∗p r ← 1; n ← p; x ← a; tant que n 6= 0 faire si n impair alors r ←r∗x fin x ← x ∗ x; n ← E(n/2) fin 10