Partie 2
Construction d’algorithmes
24 septembre 2019
Plan
1.Introduction
2.Construction d’algorithmes it´eratifs 3.Construction d’algorithmes r´ecursifs
Construction d’algorithmes 49
Construction de programme
Pour r´esoudre un probl`eme de programmation complexe, on le d´ecoupe g´en´eralement ensous-probl`emesplus simples `a appr´ehender
Exemples de sous-probl`emes pour afficher l’ensemble de Mandelbrot : I SP1 : calculer le module d’un nombre complexe,
I SP2 : v´erifier l’appartenance d’un point `a l’ensemble, I SP3 : produire l’image en parcourant le plan complexe.
Ces probl`emes d´ependent g´en´eralement les uns des autres.
Dans l’exemple, SP3 d´epend de SP2 qui d´epend de SP1.
Avantages d’un d´ecoupage :
Facilite l’impl´ementation : on peut r´esoudre chaque sous-probl`eme ind´ependamment,
G´en´eralit´e : on peut partager du code entre di↵´erents programmes,
Construction de programmes
R´esoudre un sous-probl`eme ´el´ementaire :
Certains sont triviaux et requi`erent d’´etablir une s´equence simple d’instructions (p.ex., calculer le module d’un nombre complexe) D’autres sont plus complexes et n´ecessitent der´ep´eter une s´equence d’instructions selon un sch´ema d´ependant des donn´ees(p.ex., d´eterminer l’appartenance `a l’ensemble de Mandelbrot)
Deux grands types de solutions algorithmiques pour ces derniers cas : Solutions it´eratives, bas´ees sur des boucles
Solutions r´ecursives, bas´ees sur des fonctions qui s’invoquent elles-mˆemes
On va (re)voir dans cette partie quelques grands principes pour la conception de ces deux types de solutions.
Construction d’algorithmes 51
Plan
1.Introduction
2.Construction d’algorithmes it´eratifs Technique de l’invariant
Illustrations
Correction d’algorithmes it´eratifs 3.Construction d’algorithmes r´ecursifs
Construction d’algorithmes it´eratifs
Concevoir un algorithme it´eratif (correct) peut ˆetre un exercice tr`es compliqu´e, surtout si on cherche une solution efficace.
Deux difficult´es principales :
Imaginer le sch´ema it´eratifpermettant de r´esoudre le probl`eme.
G´en´erer lecode impl´ementant ce sch´ema en ´evitant les bugs.
Le premier probl`eme est de loin le plus compliqu´e et cette comp´etence s’acqui`ere quasi uniquement via la pratique (cf. INFO0902 pour des techniques g´en´eriques n´eanmoins).
Latechnique de l’invariantpermet d’aborder formellement le second probl`eme une fois le sch´ema de la boucle imagin´e.
Construction d’algorithmes 53
Invariant de boucle
Uninvariant de boucle est une propri´et´e d´efinie sur les variables du programme qui d´efinit pr´ecis´ement ce qui doit ˆetre calcul´e `a chaque it´eration pour arriver au r´esultat escompt´e. Il r´esume l’´etat courant des calculs.
Identifier l’invariant revient `a imaginer le sch´ema it´eratif de r´esolution du probl`eme et est parfois non trivial.
Une fois l’invariant ´etabli, impl´ementer la boucle peut par contre se faire de mani`ere relativement automatique.
L’utilisation de l’invariant permet donc d’´eviter les erreurs d’impl´ementation.
Invariant de boucle
Une assertionest une relation entre les variables et les donn´ees utilis´ees par le programme qui est vraie `a un moment donn´e lors de l’ex´ecution du programme.
Deux assertions particuli`eres :
Pr´e-condition P :condition que doivent remplir les entr´ees valides du programme
Post-conditionQ :condition qui exprime que le r´esultat du programme est celui attendu.
On cherche donc `a ´ecrire un programme, not´eS, dont l’ex´ecution dans tous les cas o`u P est vraie m`ene `a ce queQ soit toujours vraie.
Lorsque c’est le cas, on dira que le triplet{P}S{Q} est correct.
Exemple : SiP ={x 0} etQ ={y2 =x}, le code S = ”y = sqrt(x); ” rend le triplet {P}S{Q}correct.
Construction d’algorithmes 55
Invariant de boucle : plus formellement
{P} INIT while(B)
CORPS FIN
{Q}
{P} INIT {I} while(B)
{I etB}CORPS{I} {I et nonB}
FIN {Q}
Dans le cas o`u le programme n´ecessite une boucle :
On met en ´evidence une assertion particuli`ere I,l’invariant de boucle, qui d´ecrit l’´etat du programme pendant la boucle.
On d´etermine ensuite le gardien B et les codes INIT,CORPS et FIN tels que les trois triplets suivants soient corrects :
I {P}INIT{I} I {I etB}CORPS{I} I {I et nonB} FIN{Q}
Ecriture du code sur base de l’invariant
Trois parties de code `a ´ecrire en se basant sur l’invariant: 1. Initialisation :{P} INIT{I}
I INIT doit rendre l’invariant vrai avant de rentrer dans la boucle et en partant de la pr´e-condition.
I L’invariant identifie les variables n´ecessaires et comment les initialiser.
2. Maintenance :{I etB}CORPS{I}
I CORPS doit faireavancerle probl`eme en maintenant l’invariant en supposant que le gardien soit vrai.
I L’invariant d´efinit le sch´ema de la boucle.
3. Terminaison :{I et non B} FIN{Q}
I FIN doit rendre la post-condition vraie en supposant que l’invariant est v´erifi´e et le gardien est faux.
I L’invariant d´efinit comment finalement r´esoudre le probl`eme.
Construction d’algorithmes 57
D´etermination de l’invariant
Pas de recette miracle pour d´eterminer l’invariant (ou de mani`ere
´equivalente le sch´ema d’une boucle).
Quelques trucs n´eanmoins :
Enlever une partie de la post-condition
Remplacer dans la post-condition une constante par une variable Combiner les pr´e-conditions et post-conditions
Raisonner par induction (voir plus loin)
Dans tous les cas, l’invariant doit faire apparaˆıtre toutes les variables du programme.
Pour un mˆeme probl`eme, plusieurs invariants (et/ou gardiens de boucles) sont possibles qui m`eneront `a di↵´erentes impl´ementations de la boucle.
Illustration 1 : appartenance `a l’ensemble de Mandelbrot
Pr´e et post-conditions : P ={cr2R,ci2R}
Q ={r= 1 si 8n,0nN:|zn|2,0 sinon}
o`u cr et ci sont les entr´ees du programme, r le r´esultat, N une constante, etzn est lan-`eme valeur de la suite d´efinie pr´ec´edemment avec c 2C tel quec =cr+ici.
Sch´ema de la boucle :
On calculezn pour des valeurs croissantes de n allant de 0 `aN.
A chaque it´eration, on calculezn `a partir de la valeurzn 1 calcul´ee
`a l’it´eration pr´ec´edente.
On s’arrˆete d`es que|zn|>2.
Construction d’algorithmes 59
Illustration 1 : appartenance `a l’ensemble de Mandelbrot
Invariant :
I ={(8n0: 0n0<n:|zn0|2) et (zr+izi=zn) et (0nN)}, o`unest le compteur de boucle etzretzisont deux variables qui contiendront les r´esultats interm´ediaires.
|···|2
z }| {
z0,z1, . . . ,zn 1,
|···|?
z zn }| {
|{z}
zr+izi=zn
,zn+1, . . . ,zN
Gardien :
B={n<N,zr2+zi24}.
Illustration 1 : appartenance `a l’ensemble de Mandelbrot
|···|2
z }| {
z0,z1, . . . ,zn 1,
|···|?
z }| {
zn
|{z}
zr+izi=zn
,zn+1, . . . ,zN
{P} {cr2R,ci2R}
INIT {I}
while((n < N) && (zr*zr + zi*zi <= 4.0)) { {IetB}
CORPS {I} }
{Iet nonB} FIN
{Q} {r= 1 si9n: 0nN:|zn|>2,0 sinon}
Construction d’algorithmes 61
Illustration 1 : appartenance `a l’ensemble de Mandelbrot
|···|2
z }| {
z0,z1, . . . ,zn 1,
|···|?
z }| {
zn
|{z}
zr+izi=zn
,zn+1, . . . ,zN
{P} {cr2R,ci2R}
double zr = 0;
double zi = 0;
int n = 0;
{I}
while((n < N) && (zr*zr + zi*zi <= 4.0)) { {IetB}
CORPS {I} }
{Iet nonB} FIN
{Q} {r= 1 si9n: 0nN:|zn|>2,0 sinon}
Illustration 1 : appartenance `a l’ensemble de Mandelbrot
|···|2
z }| {
z0,z1, . . . ,zn 1,
|···|?
z }| {
zn
|{z}
zr+izi=zn
,zn+1, . . . ,zN
{P} {cr2R,ci2R}
double zr = 0;
double zi = 0;
int n = 0;
{I}
while((n < N) && (zr*zr + zi*zi <= 4.0)) { {IetB}
double temp;
temp = zr*zr - zi*zi + cr;
zi = 2*zr*zi + ci;
zr = temp;
n++;
{I} }
{Iet nonB} FIN
{Q} {r= 1 si9n: 0nN:|zn|>2,0 sinon}
Construction d’algorithmes 61
Illustration 1 : appartenance `a l’ensemble de Mandelbrot
|···|2
z }| {
z0,z1, . . . ,zn 1,
|···|?
z }| {
zn
|{z}
zr+izi=zn
,zn+1, . . . ,zN
{P} {cr2R,ci2R}
double zr = 0;
double zi = 0;
int n = 0;
{I}
while((n < N) && (zr*zr + zi*zi <= 4.0)) { {IetB}
double temp;
temp = zr*zr - zi*zi + cr;
zi = 2*zr*zi + ci;
zr = temp;
n++;
{I}
}
{Iet nonB}
Illustration 1 : appartenance `a l’ensemble de Mandelbrot
double zr = 0;
double zi = 0;
int n = 0;
while((n < N) && (zr*zr + zi*zi <= 4.0)) { double temp;
temp = zr*zr - zi*zi + cr;
zi = 2*zr*zi + ci;
zr = temp;
n++;
}
int r = (zr*zr+zi*zi <= 4.0);
Construction d’algorithmes 62
Illustration 2 : tri par insertion
On souhaite ´ecrire une fonction pour trier un tableau Ade valeurs enti`eres. Le tri doit ˆetre e↵ectu´e dans le tableau lui-mˆeme, via ´echanges d’´el´ements.
void insertion_sort(int A[], int N);
Principe de l’algorithme :
On parcourt le tableau de gauche `a droite en triant successivement les pr´efixes du tableau de tailles 2, 3, . . .,N.
A chaque it´eration, on augmente la taille du pr´efixe tri´e en ins´erant le nouvel ´el´ementA[i] `a sa position dans le sous-tableau A[0. .i 1]
pr´ec´edemment ordonn´e.
Tri par insertion : graphiquement
Intro to Data Structures and Algorithms © Introduction, Slide 11
Insertion Sort
5 2 4 6 1 3
2 5 4 6 1 3
2 4 5 6 1 3
2 4 5 6 1 3
1 2 4 5 6 3
1 2 3 4 5 6
Construction d’algorithmes 64
Tri par insertion : boucle externe
Pr´e-condition Pe :A est un tableau d’entiers de tailleN.
Post-conditionQe :Acontient les ´el´ements du tableau de d´epart tri´es par ordre croissant.
InvariantIe (de la boucle externe) : Le sous-tableauA[0. .i 1], avec 1i N, contient les i premiers ´el´ements du tableau initial tri´es par ordre croissant.
trié pas trié
Gardien Be :i <N.
Tri par insertion : boucle externe
{Pe} {Aest un tableau d’entiers de tailleN}
INITe
{Ie} {A[0. .i 1] tri´e}
while (i<N) {
{Ie etBe} {A[0. .i 1] tri´e eti <N}
CORPSe
{Ie} {A[0. .i 1] tri´e}
}
{Ie et nonB} {A[0. .i 1] tri´e eti =N}
FINe
{Qe} {Acontient les ´el´ements du tableau de d´epart tri´es}
Construction d’algorithmes 66
Tri par insertion : boucle externe
{Pe} {Aest un tableau d’entiers de tailleN}
int i = 1;
{Ie} {A[0. .i 1] tri´e}
while (i<N) {
{Ie etBe} {A[0. .i 1] tri´e eti <N}
CORPSe
{Ie} {A[0. .i 1] tri´e}
}
{Ie et nonB} {A[0. .i 1] tri´e eti =N}
FINe
{Qe} {Acontient les ´el´ements du tableau de d´epart tri´es}
Tri par insertion : boucle externe
{Pe} {Aest un tableau d’entiers de tailleN}
int i = 1;
{Ie} {A[0. .i 1] tri´e}
while (i<N) {
{Ie etBe} {A[0. .i 1] tri´e eti <N}
CORPSe
{Ie} {A[0. .i 1] tri´e}
}
{Ie et nonB} {A[0. .i 1] tri´e eti =N}
-
{Qe} {Acontient les ´el´ements du tableau de d´epart tri´es}
Construction d’algorithmes 66
Tri par insertion : boucle externe
{Pe} {Aest un tableau d’entiers de tailleN}
int i = 1;
{Ie} {A[0. .i 1] tri´e}
while (i<N) {
{Ie etBe} {A[0. .i 1] tri´e eti <N}
CORPSe’
{Qi} {A[0. .i] tri´e}
i++;
{Ie} {A[0. .i 1] tri´e}
}
{Ie et nonB} {A[0. .i 1] tri´e eti =N}
-
{Qe} {Acontient les ´el´ements du tableau de d´epart tri´es}
Tri par insertion : boucle interne (CORPSe’)
Pr´e-conditionPi :{Ie etBe}={A[0. .i 1] tri´e eti <N} Post-conditionQi :Ie ={A[0. .i] tri´e}
Id´ee de la boucle :Soitkey =A[i], la valeur `a d´eplacer :
On parcourt le sous-tableauA[0. .i 1] de droite `a gauche.
Tant que les ´el´ements parcourus sont sup´erieurs `akey, on les d´ecale d’une position vers la droite.
On ins´erekey `a la position finalement atteinte.
Construction d’algorithmes 67
Tri par insertion : boucle interne
InvariantIi :Soit j un nouvel indice etkey =A[i] la valeur `a ins´erer : A[0..j 1] etA[j + 1. .i] sont tri´es par ordre croissant et ensemble contiennent tous les ´el´ements du sous-tableauA[0. .i] initial, except´ekey.
key <A[j+ 1]
Gardien Bi :{j >0 etA[j 1]>key}
Tri par insertion : boucle interne
{Pi} {A[0. .i 1] tri´e eti <N}
INITi {Ii}
while (j > 0 && A[j-1] > key) { {Ii etBi}
CORPSi {Ii} }
{Ii et nonBi} FINi
{Qi} {A[0. .i 1] tri´e}
Construction d’algorithmes 69
Tri par insertion : boucle interne
{Pi} {A[0. .i 1] tri´e eti <N}
int key = A[i];
int j = i;
{Ii}
while (j > 0 && A[j-1] > key) { {Ii etBi}
CORPSi {Ii} }
{Ii et nonBi} FINi
{Qi} {A[0. .i 1] tri´e}
Tri par insertion : boucle interne
{Pi} {A[0. .i 1] tri´e eti <N}
int key = A[i];
int j = i;
{Ii}
while (j > 0 && A[j-1] > key) { {Ii etBi}
A[j] = A[j-1];
j--;
{Ii} }
{Ii et nonBi} FINi
{Qi} {A[0. .i 1] tri´e}
Construction d’algorithmes 69
Tri par insertion : boucle interne
{Pi} {A[0. .i 1] tri´e eti <N}
int key = A[i];
int j = i;
{Ii}
while (j > 0 && A[j-1] > key) { {Ii etBi}
A[j] = A[j-1];
j--;
{Ii} }
{Ii et nonBi} A[j] = key;
i++;
Tri par insertion : code complet
void insertion_sort(int A[], int N) { int i = 1;
while (i < N) { int key = A[i];
int j = i;
while (j > 0 && A[j-1]>key) { A[j] = A[j-1];
j--;
}
A[j] = key;
i++;
} }
Construction d’algorithmes 70
Synth`ese
L’expression d’un invariant permet de limiter les erreurs lors de l’impl´ementation d’une boucle.
Vous devez prendre l’habitude d’exprimer l’invariant de toutes vos boucles avantleur impl´ementation, au minimum de mani`ere informelle ou graphique.
Dans la suite du cours, on fournira ponctuellement les invariants des boucles les plus compliqu´ees.
Plan
1.Introduction
2.Construction d’algorithmes it´eratifs Technique de l’invariant
Illustrations
Correction d’algorithmes it´eratifs 3.Construction d’algorithmes r´ecursifs
Construction d’algorithmes 72
Preuve de correction d’algorithmes it´eratifs
Dans certains contextes, il est crucial de prouver formellement qu’un programme est correct (p.ex. dans le domaine m´edical ou en
a´eronautique).
On peut toujours tester son programmeempiriquementmais en g´en´eral, il est impossible de consid´erer tous les cas possibles d’utilisation d’un code.
Testing can only show the presence of bugs, not their absence E.W. Dijkstra L’analyse de correction de triplets et la technique de l’invariant de boucle peuvent aussi ˆetre utilis´ees pour prouver formellement qu’un algorithme it´eratif est correct (voir INFO2009).
On peut automatiser une grosse partie de ces analyses, mais pas la d´erivation des invariants de boucle.
Terminaison de boucle
Prouver qu’un triplet{P}S{Q} est correct n’est pas suffisant dans le cas d’une boucle.
Il faut encore prouver que la boucle se termine.
Exemple : On peut prouver que le triplet ci-dessous est correct mais la boucle ne se termine pas toujours. On dira que le code estpartiellement correct.
{cr2R,ci2R}
double zr = 0;
double zi = 0;
while(zr*zr + zi*zi <= 4.0)) { double temp;
temp = zr*zr - zi*zi + cr;
zi = 2*zr*zi + ci;
zr = temp;
}
int r = (zr*zr+zi*zi <= 4.0);
{r= 1 si8n 0 :|zn|2,0 sinon}
Construction d’algorithmes 74
Terminaison de boucle
Pour prouver qu’une boucle se termine, on cherche une fonction de terminaison f :
d´efinie sur base des variables de l’algorithme et `a valeur enti`ere naturelle( 0)
telle quef d´ecroˆıt strictement suite `a l’ex´ecution du corps de la boucle
telle queB implique f >0
Puisque f d´ecroit strictement, elle finira par atteindre 0 et donc `a infirmerB.
Exemple :
Mandelbrot :f =N n
Tri par insertion (boucle externe) :f =N i
Terminaison de boucle
Il n’est pas toujours trivial de prouver la terminaison d’une boucle.
Personne n’a pu encore prouv´e que la boucle suivante se terminait pour tout n>1, bien qu’on l’ait prouv´e empiriquement pour toutes les valeurs deN <1,25.262.
void Algo(int n) { while(n != 1) {
if (n % 2) // n est impair n = 3*n+1;
else // n est pair n = n/2;
} }
https://fr.wikipedia.org/wiki/Conjecture_de_Syracuse
Construction d’algorithmes 76
Plan
1.Introduction
2.Construction d’algorithmes it´eratifs 3.Construction d’algorithmes r´ecursifs
Principe Illustrations
Impl´ementation de la r´ecursivit´e
Algorithme r´ecursif
Unalgorithmede r´esolution d’un probl`eme P sur une donn´eeaest dit r´ecursif si parmi les op´erations utilis´ees pour le r´esoudre, on trouve une r´esolution du mˆeme probl`emeP sur une donn´eeb.
Dans un algorithme r´ecursif, on nommera appel r´ecursiftoute ´etape r´esolvant le mˆeme probl`eme sur une autre donn´ee.
Un algorithme r´ecursif s’impl´emente g´en´eralement via desfonctions r´ecursives.
Forme g´en´erale d’une fonction r´ecursive (directe) : type f(P) {
...
x = f(Pr);
...
return r;
}
Construction d’algorithmes 78
Exemple 1 : fonction factorielle
La d´efinition math´ematique de la factorielle est r´ecursive : n! =
(1 si n1,
n⇤(n 1)! sinon
et se prˆete donc naturellement `a une impl´ementation via une fonction r´ecursive :
int fact(int n) { if (n <= 1)
return 1;
return n * fact(n-1);
}
Exemple 1 : fonction factorielle
Trace des appels de fonctions pour fact(5) : fact(5)
|fact(4)
| |fact(3)
| | |fact(2)
| | | |fact(1)
| | | | |return 1
| | | |return 2*1 = 2
| | |return 3*2 = 6
| |return 4*6 = 24
|return 5*24 = 120
Construction d’algorithmes 80
Exemple 2 : algorithme d’Euclide
L’algorithme du calcul du pgcd d’Euclide est bas´e sur la propri´et´e r´ecursive suivante :
Soient deux entiers positifs a et b. Sia > b, le pgcd de a et b est ´egal au pgcd deb et de (a modb).
Sugg`ere l’impl´ementation r´ecursive suivante :
int pgcd(int a, int b) { if (b > a)
return pgcd(b,a);
if (b == 0) return a;
return pgcd(b, a % b);
}
Exemple :
pgcd(1440, 408)
|pgcd(408, 216)
| |pgcd(216, 192)
| | |pgcd(192, 24)
| | | |pgcd(24,0)
| | | | |return 24
| | | |return 24
| | |return 24
Conception de fonctions r´ecursives
Deux conditions pour qu’une fonction r´ecursive soit bien d´efinie : Pr´esence d’un cas de base(condition d’arrˆet)
La “taille” du probl`eme doit ˆetre r´eduite`a chaque ´etape Forme g´en´erale d’une fonction r´ecursive :
type f(P) {
if (cas_de_base) { // cas de base ... // pas d'appel r´ecursif return [...];
}
// ´etape de r´eduction ...
[x =] f(Pr); // avec Pr plus "simple" que P ...
[return r;]
}
Construction d’algorithmes 82
Conception de fonctions r´ecursives
D’autres formes peuvent n´eanmoins ˆetre valides (mais sont plutˆot `a ´eviter).
R´ecursivit´e non d´ecroissante
(Suite de Syracuse)
intSyracuse(int n) { if(n==1)
return1;
if(n%2)// n est impair returnSyracuse(3*n+1);
else // n est pair returnSyracuse(N/2);
}
R´ecursivit´e imbriqu´ee
(fonction d’Ackermann)
intack(intm,intn) { if(m==0)
returnn+1;
else{ if(n==0)
returnack(m-1,1);
else
returnack(m-1,ack(m,n-1));
} }
R´ecursivit´e crois´ee
intP(intn) { if(n==0)
return1;
else
returnI(n-1);
}
intI(intn) { if(n==0)
return0;
Que calcule la fonctionP?
Principe de construction d’un algorithme r´ecursif
Trouver un ou plusieurs param`etres de taillesur lesquels construire la r´ecursion
Factorielle :n, le nombre dont on veut calculer la factorielle.
Trouver une solution pour le(s) cas de base, c’est-`a-dire des probl`emes de petites tailles (la plupart du temps trivial).
Factorielle :n! = 1 sin1.
Trouver commentr´eduirele probl`eme `a un ou plusieurs sous-probl`eme de tailles strictement plus petites.
Factorielle :n! =n⇤(n 1)! si n>1
La derni`ere ´etape est la plus d´elicate. Equivalent `a trouver l’invariant dans le cas d’algorithmes it´eratifs.
Construction d’algorithmes 84
Plan
1.Introduction
2.Construction d’algorithmes it´eratifs 3.Construction d’algorithmes r´ecursifs
Principe Illustrations
Impl´ementation de la r´ecursivit´e
Fonction puissance : solution it´erative
On souhaite calculer ax, aveca2Ret x entier 1.
Version it´erative
float pow_iter(float a, int x) { float res = a;
for (int i = 1; i < x; i++) res = res * a;
return res;
}
(Invariant ?) Nombre de multiplications n´ecessaires :x 1
Construction d’algorithmes 86
Fonction puissance : r´ecursivement
(1/2)Une premi`ere formulation r´ecursive : ax =
(a si x = 1
a·ax 1 si x >1
float pow_rec1(float a, int x) { if (x == 1)
return a;
return a*pow_rec1(a, x-1);
}
Nombre de multiplications n´ecessaires :x 1
Cette version est une simple r´e´ecriture de la version it´erative.
Fonction puissance : r´ecursivement
(2/2)Une deuxi`eme formulation r´ecursive :
ax = 8>
<
>:
a si x= 1
(a·a)x/2 si x>1 et pair a·(a·a)(x 1)/2 si x>1 et impair
float pow_rec2(float a, int x) { if (x == 1)
return a;
if (x % 2 == 0) // x pair return pow_rec2(a * a, x/2);
else // x impair
return a * pow_rec2(a * a, (x-1)/2);
}
Nombre de multiplications n´ecessaires : entre log2(x) et 2 log2(x) x = 128) 7 multiplications au lieu de 127.
Construction d’algorithmes 88
Fonction puissance : remarque
La solution pr´ec´edente peut aussi s’impl´ementer de mani`ere it´erative.
Mais l’invariant de la boucle est obtenu sur base de la mˆeme formulation r´ecursive et l’impl´ementation serait (un peu) moins imm´ediate.
Il existe des probl`emes pour lesquels une solution it´erative serait nettement plus complexe `a impl´ementer qu’une solution r´ecursive.
Le probl`eme des tours de Hano¨ı
cf. INFO2009Source : wikipedia
Objectif : transf´erer lesndisques de la tour 1 `a la tour 2, sans jamais mettre un disque sur un plus petit.
Solution r´ecursive :Pour d´eplacerndisques:
On d´eplace lesn 1 disques du dessus vers la tour 3.
On d´eplace len-`eme disque de la tour 1 vers la tour 2.
On d´eplace lesn 1 disques de la tour 3 vers la tour 2.
Solution it´erative?
Construction d’algorithmes 90
Le probl`eme des tours de Hano¨ı
cf. INFO2009Source : wikipedia
En fait, une solution it´erative simple existe :
Tant que la pile n’est pas dans sa position finale :
Bouger le plus petit disque `a droite (1!2,2!3 ou3!1) Bouger le seul autre disque qui peut ˆetre boug´e
Cette solution bien que strictement identique `a la solution r´ecursive est beaucoup plus compliqu´ee `a concevoir `a partir de rien.
Son impl´ementation est aussi plus complexe car elle demande de repr´esenter
Percolation
(Sedgewick et Wayne, 2016)Probl`eme : d´eterminer si de l’eau peut s’´ecouler dans un mat´eriau poreux.
Entr´ee : un tableaugrid de taillen⇥n, tel quegrid[i][j] = 0 si le mat´eriau est vide `a la position (i,j), 1 si le mat´eriau est plein.
Sortie : vrai s’il existe un chemin partant de la premi`ere ligne, arrivant `a la derni`ere ligne de la matrice et passant uniquement par des cases vides, faux sinon.
Construction d’algorithmes 92
Percolation : d´ecoupage en sous-probl`eme
Id´ee de la solution :
Marquer chaque case de la grille pour laquelle il existe un chemin depuis une case vide de la premi`ere ligne.
V´erifier qu’une case de la derni`ere ligne au moins a ´et´e marqu´ee.
// Valeur 2 utilis´ee comme marquage int percolate(int **grid, int n) {
flow(grid,n); // fonction qui effectue le marquage des cases for (int j = 0; j < n; j++)
if (grid[n-1][j] == 2) return 1;
return 0;
}
Fonction flow : d´ecoupage en sous-probl`emes
On d´efinit une fonction flowrec:
void flowrec(int **M, int **marked, int N, int i, int j);
qui va marquer toutes les cases accessibles `a partir de la position (i,j).
La fonction flowconsiste alors simplement `a appliquer flowrec`a toutes les cases de la premi`ere ligne du tableau.
void flow(int **grid, int n) { for (int j = 0; j < n; j++)
flowrec(grid, n, 0, j);
}
Construction d’algorithmes 94
Une solution r´ecursive pour flowrec
void flowrec(int **grid, int n, int i, int j);
Cas de base : Ne rien faire si i<0, i n,j<0, ouj n, (la case est hors de la grille) grid[i][j] == 1,
(la case est pleine) grid[i][j] == 2
(la case a d´ej`a ´et´e visit´ee lors d’un pr´ec´edent appel r´ecursif).
Cas inductif : Si on est pas dans un cas de base : On marque la case (i,j) qui est accessible.
On applique r´ecursivement la fonctionflowrec `a toutes les cases adjacentes `a la case (i,j).
Une solution r´ecursive pour flowrec
void flow(int **grid, int n) { for (int j = 0; j < n; j++)
flowrec(grid, n, 0, j);
}
void flowrec(int **grid, int n, int i, int j) { // Cas de base
if (i < 0 || i >= n || j < 0 || j >= n) return;
if (grid[i][j] == 1 || grid[i][j]== 2) return;
// Cas inductif grid[i][j] = 2;
flowrec(grid, n, i+1, j); // Bas flowrec(grid, n, i, j+1); // Droite flowrec(grid, n, i, j-1); // Gauche flowrec(grid, n, i-1, j); // Haut }
Correction ? Complexit´e ?
Note :flowrecimpl´emente ce qu’on appelle une recherche en profondeur d’abord (depth-first search).
Construction d’algorithmes 96
Illustration
Application : simulations num´eriques
Cette fonction permet par exemple d’´etudier l’impact de la porosit´e du mat´eriau sur la probabilit´e de percolation.
Pour des grilles de taille 100x100 (10000 grilles al´eatoires g´en´er´ees par point) :
(Sedgewick et Wayne, 2016)
Construction d’algorithmes 98
Efficacit´e de codes r´ecursifs
Dans le cas de la fonction puissance, des tours de Hano¨ı et de la percolation, la solution r´ecursive donne un code optimal en terme de complexit´e.
Dans certains cas, l’utilisation na¨ıve de la r´ecursivit´e peut cependant mener `a une solution tr`es sous-optimale.
Exemple : calcul des nombres de Fibonacci F0 = 0 F1 = 1
8n 2 :Fn = Fn 2+Fn 1
Nombres de Fibonacci : impl´ementation r´ecursive
Impl´ementation r´ecursive directe : int Fib(int n) {
if (n <= 1) return n;
return Fib(n-1)+Fib(n-2);
}
Peut-on calculerFib(60) ?
n temps de calcul
10 0s
20 0s
40 1s
45 8s
50 87s
60 ? ?
Construction d’algorithmes 100
Nombres de Fibonacci : arbres des appels r´ecursifs
0
0 0
0
0 1 1
1 1 1
1 1
1
1
1
1 1
2 1 2
2
3
5
8
3
Beaucoup de valeurs sont recalcul´ees inutilement.
Nombres de Fibonacci : temps de calcul
Soit T(n) le nombre de noeuds de l’arbre des appels r´ecursifs. On a : T(0) = 1
T(1) = 1
8n 2 :T(n) = T(n 1) +T(n 2) + 1
Le nombre d’appels de fonctions est donc plus ´elev´e que len`eme nombre de Fibonacci qui vaut approximativement pn
5, avec ⇡1,618.
Chaque appel de fonction demandant un temps constant, les temps de calcul vont augmenterexponentiellement avecn. Ils sont multipli´es par 1,618 au moins `a chaque incr´ement den.
S’il faut 87s pour n= 50, il faudra au moins 3 heures pourn = 60 et
>77000 ann´ees pourn= 100.
Construction d’algorithmes 102
Nombres de Fibonacci : version it´erative
Une version it´erative beaucoup plus efficace
int Fibiter(int n) { if (n <= 1)
return n;
else { int f;
int pprev = 0;
int prev = 1;
for (int i = 2; i <= n; i++) { f = prev + pprev;
pprev = prev;
prev = f;
}
return f;
} }
n R´ecursive It´erative
10 0s 0s
20 0s 0s
40 1s 0s
45 8s 0s
50 87s 0s
60 3h 0,001s
100 77k ans 0,001s
Correction formelle d’algorithmes r´ecursifs
int fact(int n) { if (n <= 1)
return 1;
return n * fact(n-1);
}
La correction d’un algorithme r´ecursif se prouve pas induction: On montre que le code est correct dans lecas de base.
I Sin1,fact(n) renvoie 1, ce qui est correct.
On montre que l’´etape de r´eduction est correcte en supposant que les appels r´ecursifs sont corrects (hypoth`ese inductive)
I Sin>1, la fonction renvoien⇤fact(n 1), qui vaut bienn! si fact(n-1)renvoie (n 1)!.
On en conclut, par leprincipe d’induction, que l’algorithme est correct pour toutes les entr´ees.
Construction d’algorithmes 104
Plan
1.Introduction
2.Construction d’algorithmes it´eratifs Technique de l’invariant
Illustrations
Correction d’algorithmes it´eratifs 3.Construction d’algorithmes r´ecursifs
Principe Illustrations
Impl´ementation de la r´ecursivit´e
Une exp´erience
Y-a-t’il une di↵´erence lors de l’ex´ecution de ces deux codes ?
int fact_rec(int n) { return n*fact_rec(n-1);
}
int main() { fact_rec(100);
return 0;
}
int fact_iter(int n) { int res = 1;
for (;; n--) res = res*n;
return res;
}
int main() { fact_iter(100);
return 0;
}
Construction d’algorithmes 106
Contextes d’appels de fonctions
A chaque appel de fonction, on doit retenir en m´emoire (dans une zone appel´ee leStack) le contexte de l’appel, c’est-`a-dire :
L’endroit o`u le code appelant doit continuer son ex´ecution `a la fin de l’appel
La valeur des argumentsfournis `a la fonction La valeur des variables locales`a la fonction
Cette information ne peut ˆetre supprim´eequ’une fois l’appel termin´e.
Dans le cas d’une fonction r´ecursive, le nombre d’appels de fonction actifs `a un moment donn´e peut ˆetre tr`es important.
La r´ecursivit´e a donc uncoˆut m´emoiredont il faut tenir compte.
Ce coˆut m´emoire est directement proportionnel `a laprofondeur de l’arbre
Contextes d’appels de fonctions : illustration
int fact(int n) { if (n == 1)
return 1;
return n*fact(n-1);
}
int main() { fact(5);
return 0;
}
Construction d’algorithmes 108
Une exp´erience : conclusion
Y-a-t’il une di↵´erence lors de l’ex´ecution de ces deux codes ? int fact_rec(int n) {
return n*fact_rec(n-1);
}
int main() { fact_rec(100);
return 0;
}
)Le programme s’arrˆete lorsque la m´emoire qui lui est allou´ee est remplie.
int fact_iter(int n) { int res = 1;
for (;; n--) res = res*n;
}
int main() { fact_iter(100);
return 0;
}
) Le programme ne s’arrˆete ja- mais.
R´ecursivit´e terminale
Une fonction estr´ecursive terminale s’il n’y a plus de calcul `a e↵ectuer une fois l’appel r´ecursif termin´e.
Exemple : le calcul du PGCD :
int pgcd(int a, int b) { if (b > a)
return pgcd(b,a);
if (b == 0) return a;
return pgcd(b, a % b);
}
Il est dans ce cas inutile de stocker le contexte des appels.
Les compilateurs modernes sont capables de d´etecter ce type de r´ecursion (et d’autres plus compliqu´ees) et de ne pas utiliser de m´emoire inutile.
Construction d’algorithmes 110
Synth`ese
Principal int´erˆet de la r´ecursivit´e : ´elegance, simplicit´e, et lisibilit´e du code.
Moins sujet `a des bugs et analyse de correction facilit´ee par rapport aux boucles.
Beaucoup d’algorithmes efficaces sont bas´es sur la r´ecursivit´e (cf.
Partie 5 et INFO0902).
Mais l’utilisation na¨ıve de la r´ecursivit´e peut parfois mener `a des solutions moins efficaces, voire tr`es inefficaces.
Il existe n´eanmoins des techniques syst´ematiques pour am´eliorer l’efficacit´e d’une solution r´ecursive (cf. INFO0902 et INFO0540).
L’impl´ementation a souvent un coˆut non n´egligeable en terme d’espace m´emoire dont il faut tenir compte.