Vous êtes sur la page 1sur 198

Complexité des algorithmes

Question :

Comment choisir parmi les différentes approches


pour résoudre un problème?

Exemples: Liste chaînée ou tableau?


algorithme de tri par insertion de tri
rapide?
…, etc
Pour comparer des solutions,
plusieurs points peuvent être pris en
considération
• L’algorithme est-il correct par rapport à sa
spécification
• Simplicité des algorithmes
• Efficacité des algorithmes (il est souhaitable que
nos solutions ne soient pas lentes, ne prennent pas
de l’espace mémoire considérable)

• Le point que nous allons développer dans
ce chapitre est celui de l’efficacité des
algorithmes.
• Définition: Un algorithme est un ensemble
d’instructions permettant de transformer un
ensemble de données en un ensemble de
résultats et ce, en un nombre fini étapes.

• Pour atteindre cet objectif, un algorithme


utilise deux ressources d’une machine: le
temps et l’espace mémoire.
• Définition 1: la complexité temporelle d’un
algorithme est le temps mis par ce dernier
pour transformer les données du problème
considéré en un ensemble de résultats.

• Définition 2: la complexité spatiale d’un


algorithme est l’espace utilisé par ce dernier
pour transformer les données du problème
considéré en un ensemble de résultats.
Comparaison de solutions
Pour comparer des solutions entre-elles, deux méthodes
peuvent être utilisées:

• Méthode empirique
• Méthode mathématique

Cette comparaison se fera, en ce qui nous concerne,


relativement à deux ressources critiques : temps,
espace mémoire,...

Dans ce qui suit, nous allons nous concentrer beaucoup


plus sur le temps d’exécution
Facteurs affectant le temps d’exécution:
1. machine,
2. langage,
3. programmeur,
4. compilateur,
5. algorithme et structures de données.

Le temps d’exécution dépend de la longueur de


l’entrée.
Ce temps est une fonction T(n) où n est la longueur
des données d’entrée.
Exemple 1: x=3;
la longueur des données dans ce cas est
limitée à une seule variable.

Exemple 2:
sum = 0;
for (i=1; i<=n; i++)
for (j=1; j<=n; j++)
sum++;

Dans ce cas, elle est fonction du paramètre n


Pire cas, meilleur cas et cas moyen
Toutes les entrées d’une taille donnée ne nécessitent pas
nécessairement le même temps d’exécution.
Exemple:
soit à rechercher un élément C dans un tableau de n
élément triés dans un ordre croissant.

Deux solutions s’offrent à nous:

1. Recherche séquentielle dans un tableau de taille n.


Commencer au début du tableau et considérer chaque
élément jusqu’à ce que l’élément cherché soit trouvé ou
bien soit déclaré inexistant.
2. Recherche dichotomique : tient compte du
fait que les éléments du tableau soient déjà
triés. Information ignorée par l’algorithme
de la recherche séquentielle.

• Ces deux algorithmes sont présentés comme


suit:
int recherche(int *tab, int C){
int i;
i = 0;
while (tab[i] != C && i<n )
i = i+1;
if (i == n)
return(0);
else return(1);
} /* fin de la fonction */
int recherche(int *tab, int C){
int sup, inf, milieu;
int trouve;
inf = 0; sup = n-1; trouve = 0;
while (sup >=inf && !trouve)
{
milieu = (inf + sup) / 2;
if (C == tab[milieu])
trouve = 1;
else if (C < tab[milieu])
sup = milieu -1;
else inf = milieu + 1;
}
if (!trouve)
return(0);
return(milieu)
} /* fin de la fonction */
La méthode empirique

• Elle consiste à coder et exécuter deux (ou


plus) algorithmes sur une batterie de
données générées d’une manière aléatoire
• À chaque exécution, le temps d’exécution
de chacun des algorithmes est mesuré.
• Ensuite, une étude statistique est entreprise
pour choisir le meilleur d’entre-eux à la
lumière des résultats obtenus.
Problème!
● Ces résultats dépendent :
- De la machine utilisée;
- Du jeu d’instructions utilisées
- De l’habileté du programmeur
- Du jeu de données générées
- Du compilateur choisi
- De l’environnement dans lequel sont
exécutés les deux algorithmes (partagés ou
non)
.... etc.
Méthode mathématique
• Pour pallier à ces problèmes, une notion de
complexité plus simple mais efficace a été
proposée par les informaticiens.
• Ainsi, pour mesurer cette complexité, la
méthode mathématique consiste non pas à la
mesurer en secondes, mais à faire le
décompte des instructions de base exécutées
par ces deux algorithmes.
• Cette manière de procéder est justifiée
par le fait que la complexité d’un
algorithme est en grande partie induite
par l’exécution des instructions qui le
composent.
Cependant, pour avoir une idée plus
précise de la performance d’un
algorithme, il convient de signaler que
la méthode expérimentale et
mathématique sont en fait
complémentaires.
Comment choisir entre plusieurs
solutions?
1. Décompte des instructions
• Reconsidérons la solution 1 (recherche
séquentielle) et faisons le décompte des
instructions. Limitons-nous aux instructions
suivantes:

• Affectation notée par e


• Test noté par t
• Addition notée par a
• Il est clair que ce décompte dépend non seulement
de la valeur C mais de celles des éléments du
tableau.

• Par conséquent, il y a lieu de distinguer trois


mesures de complexité:

• 1. dans le meilleur cas


• 2. dans le pire cas
• 3. dans la cas moyen
• Meilleur cas: notée par tmin(n) représentant la complexité de
l’algorithme dans le meilleur des cas en fonction du paramètre n
(ici le nombre d’éléments dans le tableau).

• Pire cas: notée par tmax(n) représentant la complexité de


l’algorithme dans le cas le plus défavorable en fonction du
paramètre n (ici le nombre d’éléments dans le tableau).

• Cas Moyen: notée par tmoy(n) représentant la complexité de


l’algorithme dans le cas moyen en fonction du paramètre n (ici le
nombre d’éléments dans le tableau). C’est-à-dire la moyenne de
toutes les complexités, t(i), pouvant apparaître pour tout
ensemble de données de taille n (t(i) représente donc la
complexité de l’algorithme dans le cas où C se trouve en
position i du tableau). Dans le cas où l’on connaît la probabilité
Pi de réalisation de la valeur t(i), alors par définition, on a:
Meilleur cas pour la recherche séquentielle:
Le cas favorable se présente quand la valeur
C se trouve au début du tableau

tmin(n) = e + 3t (une seule affectation et 3


test: deux dans la boucle et un autre à
l’extérieur de la boucle)
int recherche(int *tab, int C){
int i;
i = 0;
while (tab[i] != C && i<n )
i = i+1;
if (i == n)
return(0);
else return(1);}
tmin(n) = e + 2t
Pire cas: Le cas défavorable se présente
quand la valeur C ne se trouve pas du tout
dans le tableau. Dans ce cas, l’algorithme
aura à examiner, en vain, tous les éléments.

tmax(n) = 1e + n(2t+1e+ 1a)+ 2t+1t


= (n+1)e + na + (2n+3)t
int recherche(int *tab, int C){
int i;
i = 0;
while (tab[i] != C && i<n )
i = i+1;
if (i == n)
return(0);
else return(1);}
tmax(n) = 1e + n(2t+1e+ 1a)+ 2t+1t
= (n+1)e + na + (2n+3)t
Cas moyen: Comme les complexités favorable et
défavorable sont respectivement (e + 3t) et (n+1)e + na +
(2n+3)t, la complexité dans le cas moyen va se situer entre
ces deux valeurs. Son calcul se fait comme suit:

On suppose que la probabilité de présence de C dans le


tableau A est de ½. De plus, dans le cas où cet élément C
existe dans le tableau, on suppose que sa probabilité de
présence dans l’une des positions de ce tableau est de 1/n.

Si C est dans la position i du tableau, la complexité t(i) de


l’algorithme est :

t(i) = (i+1)e + ia + (2i+3)t


Par conséquent, la complexité moyenne de notre
algorithme est :
𝑛−1
1
𝑌= ෍ ( 𝑖 + 1 𝑒 + 𝑖𝑎 + 2𝑖 + 3 𝑡)
𝑛
𝑖=0

𝑋𝑚𝑜𝑦 𝑛 = ½ 𝑡𝑚𝑎𝑥 (n) + ½ y


Complexité asymptotique
• Le décompte d’instructions peut s’avérer
fastidieux à effectuer si on tient compte d’autres
instructions telles que: accès à un tableau, E/S,
opérations logiques, appels de fonctions,.. etc.
• De plus, même en se limitant à une seule
opération, dans certains cas, ce décompte peut
engendrer des expressions que seule une
approximation peut conduire à une solution.
• Par ailleurs, même si les opérations élémentaires
ont des temps d’exécution constants sur une
machine donnée, ils sont différents d’une machine
à une autre.
Par conséquent:
• Pour ne retenir que les caractéristiques essentielles
d’une complexité, et rendre ainsi son calcul simple
(mais indicatif!), il est légitime d’ignorer toute
constante pouvant apparaître lors du décompte du
nombre de fois qu’une instruction est exécutée.

• Le résultat obtenu à l’aide de ces simplifications


représente la complexité asymptotique de
l’algorithme considéré.
Ainsi, si
tmax(n) = (n+1)e + na + (2n+3)t,
alors on dira que la complexité de cet algorithme est
tout simplement en n. On a éliminé toute
constante, et on a supposé aussi que les opérations
d’affectation, de test et d’addition ont des temps
constants.

Définition: La complexité asymptotique d’un


algorithme décrit le comportement de celui-ci
quand la taille n des données du problème traité
devient de plus en plus grande, plutôt qu’une
mesure exacte du temps d’exécution.
• Une notation mathématique, permettant de
représenter cette façon de procéder, est
décrite dans ce qui suit:
Notation grand-O
Définition: Soit T(n) une fonction non négative. T(n) est en
O(f(n)) s’il existe deux constantes positives c et n0 telles
que:
La notation grand-O indique une borne
T(n) <= cf(n) pour
supérieure surtout
le ntemps
>= n0. d’exécution.

Exemple:
Utilité: Si d’exécution
Le temps T(n) = 3nest2 +2
borné
2
Signification:alors T(n)les
Pour toutes Î O(n ). entrées (i.e., n >= n0),
grandes
on est assuré que l’algorithme ne prend pas plus de
On cf(n) étapes.
désire le plus de précision possible:
➔Borne supérieure. 2 3
Bien que T(n) = 3n +2 Î O(n ),
on préfère O(n2).
Grand-O: Exemples
Exemple 1: Initialiser un tableau d’entiers
for (int i=0; i<n; i++) Tab[i]=0;
Il y a n itérations
Chaque itération nécessite un temps <= c,
où c est une constante (accès au tableau + une
affectation).
Le temps est donc T(n) <= cn
Donc T(n) = O(n)
Grand-O: Exemples
Exemple 2: T(n) = c1n2 + c2n .
c1n2 + c2n <= c1n2 + c2n2 = (c1 + c2)n2
pour tout n >= 0.
T(n) <= cn2 où c = c1 + c2 et n0 = 0.
Donc, T(n) est en O(n2).

Exemple 3: T(n) = c. On écrit T(n) = O(1).


Grand-Omega
Définition: Soit T(N), une fonction non négative. On a T(n)
= Ω (g(n)) s’il existe deux constantes positives c et n0
telles que T(n) >= cg(n) pour n > n0.

Signification: Pour de grandes entrées, l’exécution de


l’algorithme nécessite au moins cg(n) étapes.

➔ Borne inférieure.
Grand-Omega: Exemple
T(n) = c1n2 + c2n.

c1n2 + c2n >= c1n2 pour tout n > 1.


T(n) >= cn2 pour c = c1 et n0 = 1.

Ainsi, T(n) est en Ω (n2) par définition.

Noter qu’on veut la plus grande borne inférieure.


La notation Theta
Lorsque le grand-O et le grand-omega
d’une fonction coïncident, on utilise alors
la notation grand-theta.

Définition: Le temps d’exécution d’un


algorithme est dans Θ(h(n)) s’il est à la
fois en O(h(n)) et Ω(h(n)).
Exemple

Θ(n)

Θ(n2)

Θ(n3)

Θ(2n)

Θ(lg n)
O(lg n) <O(n) < O(n2) <
O(n3)<O(2n)
Remarques
“Le meilleur cas pour mon algorithme est
n = 1 car c’est le plus rapide.” FAUX!

On utilise la notation grand-O parce qu’on s’intéresse au


comportement de l’algorithme lorsque n augmente.

Meilleur cas: on considère toutes les entrées de longueur


n.
Notes
Ne pas confondre le pire cas avec la notation
asymptotique.

La borne supérieure réfère au taux de croissance.

Le pire cas réfère à l’entrée produisant le plus long


temps d’exécution parmi toutes les entrées d’une
longueur donnée.
Règles de simplification 1

Si
f(n) = O(g(n))
et
g(n) = O(h(n)),

alors
f(n) = O(h(n)).
Règles de simplification 2
Si
f(n) = O(kg(n))
où k > 0 est une constante,
alors
f(n) = O(g(n)).
Règles de simplification 3
Si
f1(n) = O(g1(n))
et
f2(n) = O(g2(n)),
alors
(f1 + f2)(n) = O(max(g1(n), g2(n)))
Règles de simplification 4
Si
f1(n) = O(g1(n))
et
f2(n) = O(g2(n))
alors
f1(n)f2(n) = O(g1(n) g2(n))
Quelques règles pour calculer la
complexité d’un algorithme
• Règle 1: la complexité d’un ensemble
d’instructions est la somme des complexités
de chacune d’elles.
• Règle 2: Les opérations élémentaires telle que
l’affectation, le test, l’accès à un tableau,
opérations logiques et arithmétiques, lecture
ou écriture d’une variable simple ... etc, sont
en O(1) (ou en Θ(1))
• Règle 3: Instruction if: maximum entre
le then et le else

switch: maximum parmi les différents


cas
Règle 4: Instructions de répétition

1. la complexité de la boucle for est calculée par la


complexité du corps de cette boucle multipliée par
le nombre de fois qu’elle est répétée.

2. En règle générale, pour déterminer la complexité


d’une boucle while, il faudra avant tout déterminer
le nombre de fois que cette boucle est répétée,
ensuite le multiplier par la complexité du corps de
cette boucle.
Règle 5: Procédure et fonction: leur complexité est
déterminée par celui de leur corps. L’appel à une
fonction est supposé prendre un temps constant en
O(1) (ou en Θ(1))

Notons qu’on fait la distinction entre les fonctions


récursive et celles qui ne le sont pas:

• Dans le cas de la récursivité, le temps de calcul est


exprimé comme une relation de récurrence.
Exemples
Exemple 1 : a = b;
Temps constant: Θ(1).

Exemple 2 :
somme = 0;
for (i=1; i<=n; i++)
somme += n;
Temps: Θ(n)
Exemple 3:
somme = 0;
for (j=1; j<=n; j++)
for (i=1; i<=n; i++)
somme++;
for (k=0; k<n; k++)
A[k] = k;

Temps: Θ(1) + Θ(n2) + Θ(n) = Θ(n2)


Exemple 4:
somme = 0;
for (i=1; i<=n; i++)
for (j=1; j<=i; j++)
somme++;

Temps: Θ(1) + O(n2) = O(n2)

On peut montrer : Θ(n2)


Exemple 5:
somme = 0;
for (k=1; k<=n; k*=2)
for (j=1; j<=n; j++)
somme++;

Temps: Θ(nlog n) pourquoi?


Efficacité des algorithmes
• Définition: Un algorithme est dit efficace si sa
complexité (temporelle) asymptotique est dans
O(P(n)) où P(n) est un polynôme et n la taille des
données du problème considéré.
• Définition: On dit qu’un algorithme A est meilleur
qu’un algorithme B si et seulement si:

Où et sont les complexités des


algorithmes A et B, respectivement.
Meilleur algorithme ou ordinateur?

On suppose que l’ordinateur utilisé peut exécuter une instruction en une


seconde
Robustesse de la notation O, Θ
et Ω
Algorithmes Complexité Taille max. Taille max.
Résolue par les Résolue par les
machines machines 100
actuelles fois plus rapides
Z1= (T1)100
A1 log n T1
Z2 =100T2
A2 n T2
Z3 =100T3
A3 n log n T3
A4 n2 T4 Z4 = 10T4

A5 2n T5 Z5 = T5+log 100

Z6 = T6 +
A6 n! T6
log 100/ logT6 –1
Remarque
• Les relations entre les complexités Ti et Zi,
données dans le tableau précédent, peuvent
être obtenues en résolvant l’équation
suivante:

• 100 f(Ti) = f(Zi)


• Où f(.) représente la complexité de
l’algorithme considéré.
Exemple.
• Pour l’algorithme A6 (n!), nous avons à
résoudre l’équation suivante:
100 (T6)! = (Z6)!
Pour les grandes valeurs de n, nous avons la
formule suivante (de Stirling)
Par conséquent, on obtient ce qui suit:

En introduisant la fonction log, on obtient:

En posant Z6 = T6 + ε, en approximant log (T6+ ε)


par log T6, pour de très petites valeurs de ε, on
obtient:
Exemples d’analyse d’algorithmes
non récursifs
Exemple1. Produit de deux matrices

void multiplier(int *A[n][p], int *B[p][m], int *C[n][m],


int n, int m, int p){
for (i = 0; i<n; i++)
for (j=0; j<m; j++){
S = 0;
for(k =0; k<p; k++)
S = S + A[i][k]*B[k][j];
C[i][j] = S;
} /* fin de la boucle sur j */
} /* fin de la fonction */
Analyse: le corps de la boucle sur k est en
O(1) car ne contenant qu’un nombre
constant d’opérations élémentaires. Comme
cette boucle est itérée p fois, sa complexité
est alors en O(p). La boucle sur j est itérée
m fois. Sa complexité est donc en m .O(p) =
O(mp). La boucle sur i est répétée n fois.
Par conséquent, la complexité de tout
l’algorithme est en O(nmp).

Note: Il est clair qu’il n’y pas lieu de


distinguer les différentes complexités: dans
tous les cas, nous aurons à effectuer ce
nombre d’opérations.
2. Impression des chiffres
composant un nombre
Le problème consiste à déterminer les chiffres
composant un nombre donné. Par exemple, le
nombre 123 est composé des chiffres 1, 2 et 3.
Pour les trouver, on procède par des divisions
successives par 10. A chaque fois, le reste de la
division génère un chiffre. Ce processus est répété
tant que le quotient de la division courante est
différent de zéro.
• Par exemple, pour 123, on le divise par 10,
on obtient le quotient de 12 et un reste de 3
(premier chiffre trouvé); ensuite, on divise
12 par 10, et on obtient un reste de 2
(deuxième chiffre trouvé) et un quotient de
1. Ensuite, on divise 1 par 10; on obtient un
reste de 1 (troisième chiffre trouvé) et un
quotient de zéro. Et on arrête là ce
processus.
L’algorithme pourrait être comme suit:
void divisionchiffre(int n){
int quotient, reste;
quotient = n / 10;
while (quotient >= 10){
reste = n % 10;
printf(‘%d ’, reste);
n = quotient;
quotient = n / 10;
}
reste = n % 10;
printf(‘%d ’, reste);
}/* fin de la fonction
Analyse: Comme le corps de la boucle ne contient qu’un
nombre constant d’instructions élémentaires, sa complexité
est en O(1). Le problème consiste à trouver combien de
fois la boucle while est répétée. Une fois cette information
connue, la complexité de tout l’algorithme est facile à
dériver. Déterminons donc ce nombre. Soit l’itération k.
Nous avons ce qui suit:

itération k 1 2 3 .... k

valeur de n n/10 n/100 n/1000 … n/10k

Donc, à l’itération k, la valeur courante de n est de n/10k


Or, d’après l’algorithme, ce processus va s’arrêter
dès que
n/10k < 10
Autrement dit, dès que
n < 10k+1
En passant par le log,
k + 1 > log n
Autrement dit, le nombre d’itérations effectuées est
k = O(log n)
Par conséquent, la complexité de l’algorithme ci-
dessus est en O(log n).
3. PGCD de deux nombres
int PGCD(int A, int B){
int reste;
reste = A % B;
while (reste !== 0)
{
A = B;
B = reste;
reste = A % B;
}
return(B);

} /* fin de la fonction */
Analyse: Encore une fois, le gros problème consiste
à déterminer le nombre de fois que la boucle while
est répétée. Il est clair que dans ce cas, il y a lieu
normalement de distinguer les trois complexités.
En ce qui nous concerne, nous allons nous limiter
à celle du pire cas. Pour ce qui est de celle du
meilleur cas, elle est facile à déterminer; mais, en
revanche, celle du cas moyen, elle est plus
compliquée et nécessite beaucoup d’outils
mathématiques qui sont en dehors de ce cours.
Pour ce faire, procédons comme suit pour la
complexité dans le pire cas:
Analyse PGCD suite
Avant de procéder, nous avons besoin du résultat suivant:

Proposition : Si reste = n % m alors reste <= n/2


Preuve: Par définition, nous avons:
reste = n –q.m; q >=1
reste <= n –m (1)
On sait aussi que reste <= m -1 (2)
En additionnant (1) avec (2), on obtient:
2 reste <= n – 1
donc: reste <= n / 2 CQFD
PGCD Suite
Durant les itérations de la boucle while, l’algorithme
génère, à travers la variable reste, la suite de
nombre de nombre {r0, r1, r2, r3 , ... },
représentant les valeurs que prennent les variable
n et m, où

De la proposition précédente, on peut déduire

Par induction sur j, on obtient l’une des deux


relations suivantes, selon la parité de l’indice j
PGCD suite
r <= r / 2j/2
j 0 si j est pair
r <= r / 2(j-1)/2
j 1 si j est impair

Dans les deux cas, la relation suivante est vérifiée:

rj <= max(n,m) / 2j/2


Dès que rj < 1, la boucle while se termine, c’est-à-
dire dès que:
2j/2 = max(n,m)
Par conséquent, le nombre de fois que la boucle
while est répétée est égal à

2log(max(n,m)) = O(log max(n,m)).

Comme le corps de cette boucle est en O(1), alors la


complexité de tout l’algorithme est aussi en

O(log max(n,m))
4. Recherche d’un élément
dans un tableau trié
int recherche(int *tab, int C){
int sup, inf, milieu;
int trouve;
inf = 0; sup = n; trouve = 0;
while (sup >=inf && !trouve) {
milieu = (inf + sup) / 2;
if (C == tab[milieu])
trouve = 1;
else if (C < tab[milieu])
sup = milieu -1;
else inf = milieu + 1;
if (!trouve)
return(0);
return(milieu)
} /* fin de la fonction */
Analyse: comme nous l’avons déjà
mentionné précédemment, il y a lieu de
distinguer entre les trois différentes
complexités.
Meilleur cas: Il n’est pas difficile de voir que
le cas favorable se présente quand la
valeur recherchée C est au milieu du
tableau. Autrement dit, la boucle while ne
sera itérée qu’une seule fois. Dans ce cas,
l’algorithme aura effectué un nombre
constant d’opérations; c’est-à-dire en
O(1).
• Pire cas: Ce cas se présente quand
l’élément C n’existe pas. Dans ce cas, la
boucle while sera itérée jusqu’à ce que la
variable sup < inf. Le problème est de savoir
combien d’itérations sont nécessaires pour
que cette condition soit vérifiée. Pour le
savoir, il suffit de constater, qu’après
chaque itération, l’ensemble de recherche
est divisé par deux. Au départ, cet intervalle
est égal à sup (= n-1) – inf (= 0) + 1 = n.
Itération intervalle de recherche
0 n
1 n/2
2 n/4
3 n/8
................................................
k n/ 2k
On arrêtera les itérations de la boucle while dès que
la condition suivante est vérifiée

n/ 2k = 1 donc k = O(log n)

Autrement dit, la complexité de cet algorithme dans


le pire cas est en O(log n).

Cas moyen: Exercice


2. Exemples d’analyse
d’algorithmes récursifs
• Définition: une fonction est récursive si elle
fait appel à elle-même d’une manière directe
ou indirecte
• La récursivité est une technique de
programmation très utile qui permet de
trouver des solutions d’une grande élégance à
un certain nombre de problèmes.
• Attention!
lorsqu’elle mal utilisée, cette subtilité
informatique peut créer un code totalement
inefficace.
Types de récursivité

• Récursivité simple : une fonction qui


s’appelle elle-même

• Récursivité croisée :
F1 qui appelle F2 et F2 qui appelle F1
• Récursivité terminale
Pas d’instruction après l’appel récursif
Types de récursivité (suite)

• Récursivité non terminale :


comporte des instructions après l’appel
récursif
Type de récursivité (suite)
• Un critère définissant le type de
récursivité est le nombre d’appels
récursifs dans une fonction
• Nombre = 1 récursivité unaire
• Nombre = 2 récursivité binaire
………
• Nombre = n récursivité n-aire
Propriétés d’une récursivité
1. La récursivité (appels de la fonction à elle-même)
doit s’arrêter à un moment donné (test d’arrêt).
Autrement, l’exécution va continuer indéfiniment

void exemple()
{
printf("La recursion\n");
exemple();
}
2. Un processus de réduction: à chaque appel, on
doit se rapprocher de la condition d’arrêt.

Exemple

int mystere (int n, int y){


if (n == 0) return y;
else return (mystere (n +1,y));
}
• Pour n > 0, la condition d’arrêt ne pourra pas
être atteinte.
Tours de hanoi
void hanoi(int n, int i, int j, int k){
/*Affiche les messages pour déplacer n disques
de la tige i vers la tige k en utilisant la tige j */
if (n > 0)
{
hanoi(n-1, i, k, j)
printf (‘Déplacer %d vers %d’, i,k);
hanoi(n-1, j, i, k)
}
} /* fin de la fonction */
Analyse de Hanoi
Pour déterminer la complexité de cette fonction,
nous allons déterminer combien de fois elle fait
appel à elle-même. Une fois ce nombre connu, il
est alors facile de déterminer sa complexité. En
effet, dans le corps de cette fonction, il y a:
• Un test
• Deux appels à elle même
• Deux soustractions
• Une opération de sortie
En tout, pour chaque exécution de cette fonction, il y
a 6 opérations élémentaires qui sont exécutées.
Hanoi suite

Soit t(n) la complexité de la fonction hanoi(n,i,j,k).


Il n’est pas difficile de voir, quelque que soit les
trois derniers paramètres, t(n-1) va représenter la
complexité de hanoi(n-1, -,-,-).
Par ailleurs, la relation entre t(n) et t(n-1) est comme
suit:
t(n) = t(n-1)+ t(n-1) + 6, si n > 0
t(0) = 1 (un seul test)
Autrement écrit, nous avons:
t(n) = 2 t(n-1) + 6, si n > 0
t(0) = 1 (un seul test)
Hanoi suite
Pour résoudre cette équation (de récurrence), on
procède comme suit:

t(n) = 2 t(n-1) + 6
2 t(n-1) = 4 t(n-2) + 2.6
4t(n-2) = 8 t(n-3) + 4.6
...................................
2(n-1) t(1) = 2n t(0) + 2(n-1) .6
En additionnant membre à membre, on obtient:

t(n) = 2n t(0) +6(1+2+4+ ... + 2(n-1) )


= 2n + 6. (2n - 1)
= O(2n).
• Suite de Fibonnacci

• U0=0
• U1=1
• Un+2= Un+1 + Un
4. Nombres de Fibonacci

int Fibonacci(int n){


int temp;
if (n==0)
temp = 0;
else if (n==1)
temp = 1;
else temp = Fibonacci(n-1) + Fibonacci(n-2);
return (temp);
}
Soit t(n) la complexité de la fonction Fibonacci(n). Il
n’est pas difficile de voir que t(n-1) va représenter
la complexité de Fibonacci(n-1) et t(n-2) celle de
Fibonacci(n-2).
Par ailleurs, la relation entre t(n), t(n-1) et t(n-2) est
comme suit:

t(n) = t(n-1)+ t(n-2) + 9, si n > 1


t(0) = 3 (un seul test)
t(1) = 4 (2 tests)

Pour résoudre cette équation (aux différences), on va


procéder comme suit:
Borne inf Ω :
t(n) = 9 +t(n-1)+t(n-2)
t(n)≥ 9+ 2 t(n-2) car t croissante
t(n)≥ 9 + 2 (9+ 2 t(n-4))
t(n)≥ 9 + 2 * 9+ 4 (9+ 2 t(n-6))
t(n)≥9+ 2*9 + 4*9 + 8*9 + ….+ 2n/2*9

t(n) ≥

tn= Ω ( )
Borne sup O :
t(n) = 9 +t(n-1)+t(n-2)
t(n) ≤ 9 + 2*t(n-1) car t est croissante
t(n) ≤ 9 + 2*(9+ 2*t(n-2))
t(n) ≤ 9 + 2*9+ 4*(9+ 2*t(n-3))
……
t(n) ≤ 9 + 2*9+ 4*9 + 8*9 +…+ 2n*9
t(n) ≤ 9(2n+1-1)
t(n) = O (2n)
Paradigme : Diviser pour régner
(Divide and Conquer)
• Principe
Nombres d’algorithmes ont une structure récursive :
pour résoudre un problème donné, ils s’appellent
eux-mêmes récursivement une ou plusieurs fois sur
des problèmes très similaires, mais de tailles
moindres, résolvent les sous problèmes de manière
récursive puis combinent les résultats pour trouver
une solution au problème initial.
Le paradigme « diviser pour régner » donne lieu à
trois étapes à chaque niveau de récursivité :

– Diviser : le problème en un certain nombre de


sous-problèmes ;
– Régner : sur les sous-problèmes en les
résolvant récursivement ou, si la taille d’un
sous-problème est assez réduite, le résoudre
directement ;
– Combiner : les solutions des sous-problèmes
en une solution complète du problème initial.
Vérifier qu’une liste de taille n est triée

P(n)

P(n/m) P(n/m) … P(n/m)

Vérifier que la liste L est triée revient à :


• Vérifier que chaque liste Lk est triée (k=1..m)
• Vérifier que le dernier élément de Lk est inférieur au
premier élément de Lk+1 (k=1..m-1)
Complexité

• t(n) complexité de résolution de P(n)


• t(n)= m*t(n/m) + m-1
– m-1 pour les m-1 tests des bornes
– Le coût de la résolution est la somme des coût
des résolutions des problèmes élémentaires
plus la somme des coût des combinaisons
Évaluation des complexités et exemples

I- Récursivité de type :
t(n)=a* t(n/b) + f(n) (a étant le nombre
d’appel avec une taille n/b et f(n) étant le
coût des combinaisons)
t(n)=a*t(n/b)+f(n)
a1*t(n/b)=(a*t(n/b2)+f(n/b))*a1
a2*t(n/b2)=(a*t(n/b3)+f(n/b2))*a2
a3*t(n/b3)=(a*t(n/b4)+f(n/b3))*a3
……………

aq-1*t(n/bq-1)=(a*t(n/bq)+f(n/bq-1))*aq-1
aq*t(n/bq)=t(r)*aq
avec n=r*bq+1
t(n)= aq *t(r) +
Si f(n)= Θ (nα) avec α ≥0
alors

si a<bα alors t(n)= Θ (nα)

si a=bα alors t(n)= Θ (nαlog n)

si a>bα alors t(n) = Θ ( )


Exemple : recherche dichotomique

a=1 b=2 et α=0


t(n)= t(n/2) + Θ (1)

Cas 2 :
➔ t(n)= Θ (log n)
Tri fusion

a=2 b=2 et α=1


t(n)=2t(n/2) + Θ (n)
Cas 2 :
➔ t(n)= Θ (n log n)
Vérification de tri pour un algorithme
récursif
t(n)= m*t(n/m) + Θ (m-1)
a=m b=m et α=0

Cas 3 : a>bα
➔ t(n)=
• Multiplication naïve de matrices
– Nous nous intéressons ici à la multiplication de
matrices carrés de taille n
MULTIPLIER-MATRICES(A, B)
Soit n la taille des matrices carrés A et B
Soit C une matrice carré de taille n
Pour i ← 1 à n faire
Pour j ← 1 à n faire
cij ← 0
Pour k ← 1 à n faire
cij ← cij +aik * bkj
renvoyer C
– Cet algorithme effectue Θ(n3) multiplications et
autant d’additions.
Nous supposons que n est une puissance exacte de 2.
Décomposons les matrices A, B et C en sous-matrices
de taille n/2 *n/2. L’équation C = A *B peut alors se
réécrire :

.
nous obtenons :
r= ae+bf , s = ag + bh, t = ce + df et u=cg +dh

Chacune de ces quatre opérations correspond à


deux multiplications de matrices carrés de taille
n/2 et une addition de telles matrices. À partir de
ces équations on peut aisément dériver un
algorithme « diviser pour régner » dont la
complexité est donnée par la récurrence
l’addition des matrices carrés de taille n/2 étant en Θ(n2).
• x1 = (b − d)(f + h) x5 = a(g − h)
• x2 = (a + d)(e + h) x6 = d(f − e)
• x3 = (a − c)(e + g) x7 = (c + d)e
• x4 = (a + b)h

x1 + x2 − x4 + x6 x4 + x5
x6 + x7 x2 − x3 + x5 − x7

T(n) = 7T(n/2) + Θ(n2)


a= 7 b=2 et α =2

t(n)=Θ ( )
• Analyse des algorithmes « diviser pour régner »

– Lorsqu’un algorithme contient un appel


récursif à lui-même, son temps d’exécution peut
souvent être décrit par une équation de
récurrence qui décrit le temps d’exécution
global pour un problème de taille n en fonction
du temps d’exécution pour des entrées de taille
moindre.
–La récurrence définissant le temps
d’exécution d’un algorithme « diviser
pour régner » se décompose suivant les
trois étapes du paradigme de base :
1. Si la taille du problème est
suffisamment réduite, n ≤ c pour une
certaine constante c, la résolution est
directe et consomme un temps constant
Θ(1).
2. Sinon, on divise le problème en a sous-
problèmes chacun de taille 1/b de la taille du
problème initial. Le temps d’exécution total
se décompose alors en trois parties :
(a) D(n) : le temps nécessaire à la division
du problème en sous-problèmes.
(b) aT(n/b) : le temps de résolution des a
sous-problèmes.
(c) C(n) : le temps nécessaire pour
construire la solution finale à partir des
solutions aux sous-problèmes.
• La relation de récurrence prend alors la forme :

• où l’on interprète n/b soit comme , soit


comme
3. Normalisation des boucles
1. boucles for de type
for (i = k; i <= n; i = i+c)
avec : k>=0, n>=0, c>0

Ces boucles sont appelées


boucles arithmétiques.
L’objectif est de nous ramener d'une boucle de type
for (i = k; i <= n; i += c) avec k>=0; n>=0 c>0
à une boucle de type
for (j = 0; j <= nombreIT-1; j++) dite boucle
normalisée.
Soit une boucle du type :
for (i = k; i <= n; i = i+c),
l'écriture for (i = k; i <= n/c ; i++) n'est pas
équivalente en terme de nombre d'itérations.
Par exemple, les boucles for (i=1; i<=9 ; i+=5) et
for (i=1; i<=9/5 ; i+=1) n'ont pas le
même nombre d'itérations. Les valeurs de i pour
lesquelles on passe dans la boucle pour la
première sont 1 et 6, on a donc 2 itérations alors
que pour la seconde boucle, on a une seule
itération, la valeur de i étant 1.
Avant de modifier le pas de boucle, il faut donc
procéder à tous les changements de repères
nécessaires de manière à commencer le nombre
d'itérations à 0 .
Ici, nous proposons une méthode de
normalisation pour les boucles for du type
for (i=k; i<=n; i+=c) puis for (i=k; i<n; i+=c).
Démarrage de la valeur initiale à 0
Étudions le nombre d'itérations des boucles for (i=k; i<=n ; i+=c)
et for ( i = 0; i<=n-k; i = i+c)
Le nombre d'itérations de la boucle for ( i = k; i<=n; i +=c) est :
- si k > n : 0
- si k = n : 1
- si k < n:
Soit it entier positif (it est un entier car it +1 est le nombre d'itérations) tel que
k + c×it <= n et k + c ×(it +1)> n
=> (n-k)/c -1 < it <= (n-k)/c (c>0)
=> (n-k)/c < it +1 <= (n-k)/c +1
- si (n-k)/c est entier alors le nombre d'itérations est : (n-k)/c +1
sinon le nombre d'itérations est : E((n-k)/c +1).
- On remarquera que cette formule est également valable pour k=n.

En conclusion, le nombre d'itérations de la boucle for ( i = k; i<=n; i +=c)


est :
si k>n : 0 sinon : E((n-k)/c +1) .
Montrons que dans les deux cas le nombre d'itérations est identique
On applique la formule ainsi obtenue à la
seconde boucle en remplaçant n par n-k et k
par 0,
on obtient ainsi quand k<=n : E((n-k-0)/c +1)
et 0 sinon.
Le nombre d'itérations nombreIT
de ces boucles est donc identique et est :
nombreIT = si k>n : 0 sinon : E((n-k)/c +1)
Transformation de l'incrément
Transformation de for ( i=0; i<=n-k; i +=c) en
for ( i=0; i<=(n-k)/c; i++)
Montrons que dans les deux cas le nombre
d'itérations est identique :
E((n-k)/c +1) itérations pour la première et
E(((n-k)/c)/1 +1) pour la seconde donc les
deux boucles ont le même nombre d'itérations.
Le nombre d'itérations nombreIT de ces
boucles est
nombreIT = si si k>n : 0 sinon : E((n-k)/c +1)
Remplacement du < par <=.
La transformation de for ( i = k; i<n; i +=c) en
for (i=k; i<=n-ε ; i+=c) avec c>= ε >0 ,
n >= ε (pour conserver une borne positive)
conserve-t-elle le nombre d'itérations ?
Le nombre d'itérations de la boucle
for ( i = k; i<=n-ε; i +=c) est :
si k>n-ε : 0 sinon : E((n-ε-k)/c +1)
Le nombre d'itérations de la boucle
for (i=k; i<n ; i+=c) :
- si k >n -ε : 0 (k>n car ε>0 )
- si k = n -ε : 1 ( k = n -ε => on a donc k-ε <n (car
ε>0 : au moins une itération) et
k+c =c+n-ε >=n( c>=ε>0) on a donc une et une seule
itération).
- si k<n-ε :
1. i<n-ε => i<n (car ε>0) (on aura au moins le bon
nombre d'itérations).
2. i>=n-ε => i+c>=n-ε+c>n (car c >=ε > 0)) (on aura
au plus le bon nombre d'itérations).
La transformation de la boucle
for (i=k; i<n; i+=c) en for ( i=k; i<=n-ε; i+=c)
conserve le nombre d'itérations.
Le nombre d'itérations nombreIT de ces
boucles est :
nombreIT = si k>n-ε : 0
sinon : E((n-ε-k)/c +1)
Transformation d'incrément – valeur en
incrément + valeur

for (i=sup ; i>= inf; i-=dec)


a le même nombre d'itérations que la boucle
for(i=inf; i<=sup; i+=dec)

La preuve en exercice
Boucles while similaires aux boucles for
précédentes
Par définition, une boucle while de type :
i=inf;
while (i <= sup)
{
instructions ne modifiant ni i, ni inc, ni sup;
i+=inc;
}
avec sup, inf et inc constantes pour la boucle est
équivalente en nombre d'itérations à la boucle
for (i=inf; i<=sup;i+=inc).
Une boucle while de type :
i=sup;
while (i >= inf)
{
instructions ne modifiant ni i, ni dec, ni inf;
i-=dec;
}
avec sup, inf et dec constants pour la boucle est
équivalente en nombre d'itérations à la boucle
for (i=sup; i >=inf; i-=dec) laquelle est équivalente à
for(i=inf; i<=sup; i+=dec)
Les boucles do while
Une boucle do ...while de type :
i=inf;
do
{
instructions ne modifiant ni i, ni inc, ni sup;
i+=inc;
} while(i <= sup);
Cette boucle est équivalente à l'écriture suivante :
i=inf;
instructions ne modifiant ni i, ni inf, ni inc, ni sup;
i+=inc;
while(i<=sup)
{
instructions ne modifiant ni i, ni inf, ni inc, ni sup;
i+=inc;
}
Soit à :
i=inf;
instructions ne modifiant ni i, ni inf, ni inc, ni sup;
for (i+=inc;i<=sup; i+=inc)
{ instructions ne modifiant ni i, ni inf, ni inc, ni sup; }

Calculer en exercice le nombre d’itérations


Une boucle do ...while de type :
i=sup;
do
{ instructions ne modifiant ni i, ni inf, ni inc, ni sup;
i-=inc;
}while(i >= inf);
est équivalente à l'écriture suivante :
i=sup;
instructions ne modifiant ni i, ni inf, ni inc, ni sup;
i-=inc;
while(i>=inf) { instructions ne modifiant ni i, ni inf, ni inc,
ni sup;
i-=inc;}
Soit à :
i=sup;
instructions ne modifiant ni i ni inf ni inc ni sup;
for (i-=inc;i>=inf; i-=inc)
{ instructions ne modifiant ni i ni inf ni inc ni sup }
Etudions maintenant la transformation
d'une boucle géométrique de type
for (i=inf; i<=sup-ε; i*=cte) avec inf>0,
sup>ε >0 et cte un entier strictement positif.
Soit la boucle for (i=1; i<=sup-ε; i*=cte)
avec sup strictement positif et cte un entier
strictement positif.
A l'itération k, i=cte^(k-1)
for (i=1; i<=sup-ε; i*=cte) est équivalente en
nombre d'itérations à
For (k=0;k<= logcte(sup-ε) ;k++) (avec
sup>ε>0 et cte entier >1 sinon boucle
infinie ).
Pour une boucle géométrique commençant
à inf >=1
for (i=inf; i<= sup-ε; i*=cte) avec
inf >0 , sup >0 et cte un entier positif en

for (i=1; i<= (sup-ε)/inf; i*=cte).


Transformation d'une boucle décroissante
en boucle croissante
for(i=sup; i>=inf; i/=cte) est équivalente en
nombre
d'itérations à la boucle
for(i=E(inf); i <=sup; i*=cte)
avec inf>=0, sup >0 et cte entier >0.
Boucles while similaires aux boucles for
précédentes
boucle while de type :
i=inf;
while (i <= sup)
{...
i*=inc;
}
avec sup et inf constantes pour la boucle est
équivalente en nombre d'itérations à la boucle
for(i=inf;i<=sup;i*=inc).
boucle while de type :
i=sup;
while (i >= inf)
{...
i/=inc;
}
avec sup et inf constants pour la boucle est
équivalente en nombre d'itérations à la boucle
for(i=sup; i>=inf; i/=inc).
Boucles do while assimilables une des boucles
Précédentes :
boucle do ...while de type :
i=inf;
do
{ instructions ne modifiant ni i ni inf ni inc ni sup;
i*=inc;
} while(i <= sup);
Cette boucle est équivalente l'écriture suivante :
i=inf;
instructions ne modifiant ni i ni inf ni inc ni sup;
i*=inc;
while(i<=sup)
{ instructions ne modifiant ni i ni inf ni inc ni sup;
i*=inc;}
Soit à :
i=inf; instructions ne modifiant ni i ni inf ni inc ni
sup;
for (i*=inc;i<=sup; i*=inc)
{ instructions ne modifiant ni i ni inf ni inc ni sup; }
boucle do ...while de type :
i=sup;
do
{ instructions ne modifiant ni i ni inf ni inc ni
sup
i/=inc;
}while(i >= inf);
Cette boucle est équivalente l'écriture suivante :
i=sup;
instructions ne modifiant ni i ni inf ni inc ni sup;
i/=inc;
while(i>=inf)
{ instructions ne modifiant ni i ni inf ni inc ni sup
i/=inc;}
Soit a :
i=sup ;
instructions ne modifiant ni i ni inf ni inc ni sup;
for (i/=inc;i>=inf; i/=inc)
{ instructions ne modifiant ni i ni inf ni inc ni sup }
Algorithmes exactes et
d’approximation
Introduction
pbs Algo Algo

faciles durs exact d’approximation déterministes non déterministes

Complexité polynomiale

Problème dur : il n’existe pas d’algorithme déterministe de


complexité polynomiale qui permet de le résoudre
• Définitions
- Algorithmes déterministes : Un algorithme déterministe
est un algorithme qui à chaque étape passera toujours à
l’étape suivante de la même façon : celle prévue par le
concepteur de l’algorithme. S’il y a un choix à faire, il fera
toujours le même choix si on l’exécute avec les mêmes
données en entrée.
- Algorithmes non déterministes : Un algorithme non
déterministe est un algorithme qui, lorsqu’il se trouve face
à un choix, peut indifféremment choisir l’un ou l’autre des
chemins d’exécution sans que l’on ne puisse prédire à
l’avance celui qu’il va choisir. De manière imagée, on
s’imagine qu’il se dédouble pour exécuter les différents
choix simultanément jusqu’à aboutir à tous les résultats
possibles.
• Discussion : Les algorithmes non déterministes
permettent donc de résoudre les problèmes bien plus vite
qu’un algorithme déterministe. Sauf que ces algorithmes ne
peuvent s’exécuter que sur des ordinateurs non
déterministe et ces machines ne sont pas constructibles.
Il est possible de transformer un algorithme non
déterministe en algorithme déterministe (il suffit de
calculer tous les chemins possibles l’un après l’autre plutôt
que simultanément), mais au détriment de la durée de
calcul qui peut exploser.
Pour illustrer la différence entre un algorithme
déterministe et un algorithme non déterministe, prenons le
problème suivant : ” 1241 n’est pas un nombre premier “.
L’algorithme doit répondre vrai ou faux. Un algorithme
déterministe va faire :
– Est-il divisible par 2 ? Non.
– Est-il divisible par 3 ? Non.
– Est-il divisible par 4 ? Non.
– Etc.
Jusqu’à aboutir soit à trouver un diviseur, et conclure que
la proposition est vraie, soit ne pas en trouver un et
constater que c’est faux. Au pire, il aura fait 1239 divisions
pour tester.
L’algorithme non déterministe va simplement répondre :
vrai, 1241=17*73.

Est-ce qu’il devine ? C’est un peu vrai.

En fait, l’algorithme non déterministe évalue tous les cas


simultanément et trouve donc immédiatement s’il y en a un
qui répond à la question.
Un problème de décision est un problème
dont la solution est OUI ou NON
Définitions (classes) :
• Classe P : Cette classe couvre les problèmes
de décision qu’on peut résoudre par un
algorithme polynomial déterministe
• Classe NP : Cette classe couvre les
problèmes de décision qu’on peut résoudre
par un algorithme polynomial non
déterministe
Exemple de problème dur (NP-complet)
Partition minimale
S1= Somme (L1(i))
S2=Somme (L2(i))
Minimiser |S1-S2|

L= [7,1,9,3,2,6]
L= [7,1,9,3,2,6]
L1=[7,1,9] L2=[3,2,6]
L1=[7,1,3] L2=[9,2,6]
L1=[7,1,2] L2=[9,3,6]
L1=[7,1,6] L2=[9,3,2]
L1=[7,9,3] L2=[1,2,6]
L1=[7,9,2] L2=[1,3,6]
|R|=C63 = 6! / (3! * 3!) = 20
Choisir 3 parmi 6
L de taille n 🡺 |R| = Cnn/2

Un algorithme exact résolvant ce problème


consiste à étudier toutes les solutions et de
calculer |S1-S2| pour chacune d’elles, puis
chercher le minimum.

🡺 complexité exponentielle
Classes de complexité
Premier objectif: regrouper ensemble des problèmes
ayant une même complexité
– P: problèmes de décision solvables en temps
polynomial.
– NP: solvables en un temps polynomial avec un
algorithme non-déterministe.
– EXP: solvables en temps exponentiel.
Rôle central de ces deux premières classes.
Pour placer un problème dans une classe de
complexité donnée, il suffit de fournir un
algorithme opérant dans les contraintes de
ressources correspondante.

Idéalement, on voudrait un moyen de placer


chaque problème dans la plus petite classe
de complexité possible. Cela correspondrait
à trouver un algorithme optimal.
Réductions
• Il est très difficile de démontrer
formellement qu’un problème nécessite une
quantité minimale de ressources.
• Il existe par contre de bons outils pour
comparer la complexité de deux problèmes.
Le problème de calcul A se réduit au problème B s’il
existe un algorithme « efficace » qui permet de
résoudre A en utilisant des appels procéduraux à
B.

Ceci est dénoté A ≤T B. On peut avoir à la fois A ≤T


B et B ≤T A. Dans ce cas, on considère que A et B
sont des problèmes de complexité équivalente.

Avantage: Si un problème qui nous intéresse est de


complexité équivalente à des problèmes pour
lesquels aucun algorithme efficace n’est connu, on
a une forte indication que ce problème est très
complexe.
Complétude
• Un problème A est complet pour la classe
de complexité C si
1. A ∈ C
2. Pour tout B ∈ C on a B ≤T A.
• Si A est complet pour la classe C alors soit
A n’admet pas d’algorithme efficace, soit il
en admet un et alors tous les problèmes de C
en admettent un également.
• Par contraposée, s’il existe un seul problème
de C qui n’admet pas d’algorithme efficace,
alors A n’admet pas d’algorithme efficace.
Un exemple
Problème du voyageur de commerce (TSP):
Étant donné un réseau de villes et un coût de
déplacement dij entre les villes i et j. On
cherche à trouver un circuit fermé qui visite
chaque ville une seule fois en empruntant le
parcours de plus bas coût possible.
Plusieurs types de problèmes de calcul.
• Problèmes de décision: réponse est oui/non.
(ou 0/1)
– TSP, existe-t-il un circuit de coût ≤ t?
• Problèmes de recherche ou d’optimisation:
recherche d’une solution à un ensemble de
contraintes
– TSP, quel est le circuit optimal?
• Problèmes d’évaluation
– TSP, quel est le coût du circuit optimal?
Remarques sur TSP
• On peut facilement évaluer le coût d’un circuit
donné.
• Même pour un petit nombre de villes, il y a un
nombre astronomique de circuits possibles, donc la
fouille systématique n’est pas envisageable.
• Deux caractéristiques typiques des problèmes
d’optimisation combinatoire rencontrés très
fréquemment en algorithmique.
Triste réalité: TSP est NP-complet. Cela
semble indiquer qu’il n’existe pas
d’algorithme efficace pour le résoudre.

Alors que conseiller au voyageur de


commerce?

le même problème se pose pour un très grand


nombre de problèmes d’optimisation.
NP-complétude
La NP-complétude est un des outils les plus utilisés
pour donner une indication forte qu’un problème
n’admet pas d’algorithme efficace.

• Exemples choisis: TSP, satisfiabilité d’une


formule Booléenne, couvertures de graphes,
coloriage de graphe, problème du sac à dos, etc.
Contourner la NP-complétude
• Considérer des variantes plus simples du
problèmes.
– TSP est-il plus simple si on considère que dij ∈
{0,1}?
– TSP est-il plus simple si on considère que dij +
djk ≥ dik?
Contourner la NP-complétude
• Algorithmes d’approximation
– Existe-t-il un algorithme efficace qui obtient
une solution de TSP qui est au pire 50% plus
chère que la solution optimale?
– Peut-on formellement démontrer qu’un tel
algorithme n’existe pas à moins que TSP
admette lui-même un algorithme efficace?
Contourner la NP-complétude
• Algorithmes probabilistes
– Peut-on trouver un algorithme efficace qui aura
99% de chances de trouver une solution
optimale à chaque instance?
• Heuristiques
– Peut-on trouver un algorithme efficace qui
trouve une solution optimale pour 99% des
instances qui nous intéressent?
Contourner la NP-complétude
• Complexité paramétrée
– Raffiner l’analyse du temps d’exécution d’un
algorithme pour confiner l’explosion du temps
de calcul à un paramètre qui reste petit dans les
applications pratiques.
– Limites de ce paradigme.
P = NP?
• Beaucoup de problèmes sont NP-complets.
Donc si P ≠ NP, aucun de ceux-ci n’admet
d’algorithme qui s’exécute en temps
polynomial.
Les réductions polynomiales
La notion de réduction permet de traduire qu’un problème
n’est pas ”plus dur” qu’un autre.
Si un problème A se réduit en un problème B, le problème A est (au
moins) aussi facile que B si la réduction est facile.

On peut a priori utiliser la réduction de deux façons :


pour montrer qu’un problème est dur: si A est réputé dur,
B l’est aussi;
pour montrer qu’un problème est facile: si B est facile, A
l’est aussi.
C’est en fait surtout le premier raisonnement que l’on va
utiliser.
On considère les deux problèmes de décisions suivants :

Problème 1
Données : N, un nombre de participants et une liste de paires de
participants: les paires d’ennemis
Peut-on faire p équipes de telle sorte qu’aucune équipe ne
contienne une paire d’ennemis.

Problème 2
Données : Un graphe G, un entier k : un nombre de couleurs.
G est -il k-coloriable ?
Les deux problèmes sont NP, pour aucun des deux, on ne
connaît d’algorithme polynomial.
Comment transformer un problème en un autre?
Transformation

Supposons qu’on dispose d’un algorithme pour le problème


de coloriage de graphes.
Comment s’en servir pour le problème des équipes?

On transforme une instance du Pb1 en une instance du Pb2 :

- Les sommets du graphe sont les personnes.

- Il y a un arc entre deux sommets si les personnes sont


ennemies.

-k=p
La transformation est correcte

G est k-coloriable Ssi on peut faire p équipes :


Supposons qu’on puisse faire p équipes: on donne à chaque
sommet la couleur de l’équipe de la personne correspondant
au sommet. Deux sommets de même couleur correspondent à
deux personnes de la même équipe : donc il n’y a pas d’arc
entre eux. Le coloriage est bien correct.
Supposons qu’on puisse faire un k = p-coloriage correct de G.
Affectons chaque personne à l’équipe couleur du sommet
correspondant. Deux personnes ennemies sont reliées par un
arc donc ne sont pas dans la même équipe.
La construction de G se fait polynomialement (En exercice).

Si on a un algorithme polynomial pour le p−coloriage de


graphe, on en a aussi un pour le problème d’équipes.

Par contraposée

Si il n’y a pas d’algorithme polynomial pour le problème


d’ équipes, il n’y en a pas non plus pour le p−coloriage.
Éléments de complexité amortie
Complexité amortie
• Définition
Dans l’analyse amortie, le temps mis pour effectuer
une suite d’opérations est pris comme la moyenne
arithmétique sur l’ensemble de ces opérations (ne
pas confondre avec l’espérance mathématique) .
Cela garantit une borne supérieure sur le temps
moyen de chaque opération dans le pire cas.
Complexité amortie
• La motivation de cette démarche est que la
complexité dans le pire cas donne
généralement une borne pessimiste car elle
ne considère que l’opération la plus coûteuse
sur l’ensemble des opérations restantes.
• La méthode amortie consiste à répartir les
coûts sur toutes opérations de telle manière
que chacune d’elle aura un coût unique.
• En d’autres termes,
L’analyse par rapport à la complexité amortie
garantie la performance moyenne de chaque
opération dans le plus mauvais cas.
L'analyse amortie est différente de l’analyse en
moyenne. En effet,
• dans l'analyse en moyenne, on cherche à exploiter
le fait que le pire cas est peu probable en faisant
des hypothèses sur la distribution des entrées ou
sur les choix aléatoires effectués dans
l'algorithme ;
• dans l'analyse amortie, on cherche à exploiter le
fait que le pire cas de l'algorithme ne peut pas se
produire plusieurs fois consécutivement, ou de
manière trop rapprochée, quelle que soit l'entrée.
Complexité amortie 3
• Pensez comme si, lors de vos achats, vous aviez
dépensé 10 pour l’article 1, 20 pour l’article 2 et 3
pour l’article 3. Vous pourriez dire que le coût de
chaque article est de (20+10+3)/3 = 11.
• On voit bien que le coût pour certains articles est
sous-estimé, alors qu’il est surestimé pour
d’autres. Ne pas confondre la complexité amortie
avec la complexité en moyenne (qui, elle, fait des
suppositions probabilistes). Dans le cas amorti,
aucune distribution probabiliste n’est requise.
• Cette mesure représente en quelque sorte la
complexité en moyenne du cas défavorable.
Complexité amortie 4
• L’approche consiste à affecter un coût artificiel à chaque
opération dans la séquence d’opérations. Ce coût artificiel
est appelé coût amorti d’une opération.

• Le coût amorti d’une opération est un artifice de calcul qui


souvent n’a aucune relation avec le coût réel: souvent, ce
coût peut être n’importe quoi. La seule propriété requise
par un coût amorti est que le coût réel total de toute la
séquence d’opérations doit être borné par le coût total
amorti de cette séquence d’opérations.

• Autrement dit, pour les besoins de l’analyse, il sera


suffisant d’utiliser le coût amorti au lieu de l’actuel coût de
l’opération.
Complexité amortie 5

• L’analyse amortie consiste à estimer une borne


supérieure sur le coût total T(n) requis par une
séquence de n opérations.
• Quelques opérations, parmi cette séquence,
peuvent avoir une coût énorme et d’autres, un coût
moindre. L’algorithme génère un coût T(n) pour
les n opérations.
• Le coût amorti pour chaque opération est T(n)/n.
Complexité amortie 6
Il existe 3 méthodes pour déterminer la complexité
amortie d’un algorithme. La différence réside
dans la manière dont le coût est assigné aux
différentes opérations.
1. Méthode par aggrégation
2. Méthode comptable
3. Méthode du potentiel
Dans ce qui suit, nous allons illustrer chacune de ces
3 méthodes sur l’exemple du compteur binaire.
Le problème du compteur binaire
Étant donné un tableau de n bits, le problème
consiste, à partir des bits 00000…0, à
compter le nombre de fois que les bits
changent de valeur (de 0 à 1 et de 1 à 0), à
chaque fois que la valeur 1 est ajoutée. On
suppose que cette opération est répétée n fois.
L’algorithme est alors comme suit:
Compteur binaire d’ordre 4
Analyse amortie
• Une analyse naïve montre que’une séquence
de n opérations génère un coût de O(n^2).
En fait, l’algorithme INCREMENT, dans le
pire cas, a une complexité de O(n). S’il est
répété n fois, alors la complexité est en
O(n^2). En effet, ce ne sont pas tous les bits
qui sont changés à chaque itération.
1. Analyse par aggrégation
• Combien de fois A[0] a t-il changé
• Réponse: à chaque fois
• Combien de fois A[1] a t-il changé
• Réponse: chaque 2 fois.
• Combien de fois A[2] a t-il changé
• Réponse: chaque 4 fois.
• …etc.
• Le coût total pour changer A[0] est n,celui de
A[1] est floor(n/2), celui de A[2] est floor(n/4),
etc.
• Par conséquent, le coût total amorti est
• T(n) = n + n2 +n/4+…+1
<= n\sum{i>=0} 1/2^I
<= 2n
Le coût amorti d’une opération est
t(n) = T(n)/n = 2 = O(1)
2. Analyse comptable
• Dans cette méthode, on assigne des coûts
différents aux opérations: quelqu’unes
seront surchargées et d’autres sous-
chargées. La quantité dont nous chargeons
une opération est appelée le coût amorti.
• Quand une opération dépasse son coût réel,
la différence est affectée à un autre objet, de
la structure de données comme un crédit.
Suite 1
• Ce crédit est ensuite utilisé plus tard pour payer les
opérations de complexité amortie moindre que leur
coût réel.
• Le coût amorti d’une opération peut donc être vu
comme étant réparti entre le coût réel et le crédit
qui est soit déposé soit utilisé. Cela est différent de
la méthode précédente dans la mesure où les
opérations ont toutes le même coût.
• Pour satisfaire la propriété fondamentale d’un coût
amorti, il y a lieu d’avoir le coût total amorti
comme une borne supérieure du coût réel. Par
conséquent, on doit faire attention à ce que le
crédit total doit toujours être positif.
Suite 2
• Dans le cas du problème du compteur binaire,
assignons un côut amorti de 2unités pour initiliaser un
bit à 1 (le 2 vient du fait qu’un bit est soit mis à 1, soit
mis à 0). Quand un bit est intiliasé, on utilise 1unité
(sur les 2unités) pour payer l’initiliasation proprement
dite, et nous plaçons l’autre unité sur ce bit comme
crédit.
• En tout temps, tout bit 1 possède 1unité de crédit sur
lui. Donc, pas besoin de charger les bits quand ils
passent à 0.
Suite 3
• Le coût amorti de l’algorithme peut maintenant
être déterminé comme suit:
• Le coût de la réinitialisation de bits dans la boucle
while est payé par les unités sur les bits initialisés.
Au plus un seul bit est initialisé à chaque itération
de l’algorithme, voir ligne 6, et par conséquent le
coût amorti par opération est au plus de 2 unités.
Le nombre de 1 dans le compteur n’est jamais <0,
et donc la quantité de crédit est toujours >=0. Par
conséquent, pour n opérations de l’algorithme, le
coût total amorti est en O(n), majorant le coût total
réel.
3. Méthode du potentiel
Au lieu de représenter le travail prépayé
comme un crédit stocké avec des objets
spécifiques dans la structure de données, la
méthode du potentiel de l’analyse amortie
représente ce travail comme un potentiel qui
peut être libéré pour payer des opérations
futures. Ce potentiel est associé à toute la
structure de données, au lieu d’une
opération comme c’est le cas de la méthode
comptable.
Principe de la méthode
• On commence avec une structure de
données D0 sur laquelle n opérations sont
effectuées. Pour chaque i =1,2,..,n, on pose
ci le cout réel de la ième opération et Di la
structure de données qui résulte de
l’application de la ième opération sur la
structure de données D_{i-1}. La fonction
potentielle Ψ qui associe chaque Di à un
nombre Ψ(Di)
Suite 1
• Le coût amorti d_i de la ième opération, par
rapport à Ψ, est défini comme suit:
d_i = c_i +\psy(D_i) - \psy(D_i-1)
Le coût amorti est le coût réel plus
l’augmentation du potentiel dû à l’opération
Le coût total amorti est donc:
\sum_{i=1}^n d_i = \sum_{i=1}^n
+\psy(D_n)-\psy(D_0)
Suite 2
• Si nous pouvions définir une fonction\psy
telle que \psy(D_n)>= \psy(D_0), alors le
coût total amorti est un majorant sur le coût
total réel.
• En pratique, on supose toujours
\psy)D_0) = 0.
• Définissons la fonction potentielle comme suit:
• = bi = nombre de 1 dans la structure de
données.

Déterminons le coût amorti de l’algorithme


INCREMENT
Supposons que la ième exécution de INCREMENT a
changé t_i bits à 0. Alors le coût total de cette
opération est au plus t_i +1, car en plus de cette
initialisation, elle peut aussi mettre un 1(le plus à
gauche
• Le nombre de 1 dans le tableau, après la ième
opération de INCREMENT est
bi <= b_{i-1}-t_i +1
La différence des potentiels est alors
\psy(D_i) - \psy(D_{i-1} <= b_{i-1}-t_i +1
- b_{i-1}
<= 1-t_i
Le coût amorti est donc

d_i = c_i + \psy(D_i) - \psy(D_{i-1}


<= (t_i + 1)+ (1-t_i)
= 2 = O(1)
Comme le compteur commence à 0, alors
nous avons bien

Le coût total amorti est bien un majorant sur


le coût total réel. Le coût total amorti est
bien n*O(1) = O(n).
Quelques Références
1. D. Rebaïne (2000): une introduction à l’analyse
des algorithmes, ENAG Édition.

2. Cormen et al. (1990): Algorithms, MacGraw


Hill.

3. J.M. Lina (2004): Analyse amortie des


algorithmes, ETS, Montréal.

4. Data strctures, Algorithmes and applications,


Sartaj Sahni, Silicon Press, 1999.

Vous aimerez peut-être aussi