Académique Documents
Professionnel Documents
Culture Documents
Problèmes et instances
1.2 Exemple
1.2.1 Planarité d’un graphe
Une instance du problème : Imaginons que dans le cadre de l’aménagement du territoire,
on doive choisir le lieu d’implantation de trois usines et le lieu des sources de distribution du
gaz, d’électricité et d’eau. Le problème consiste à établir s’il est possible que le câblage et la
tuyauterie allant des sources aux entreprises soit fait au même niveau du sous-sol et dans
l’affirmative de produire un plan comprenant les usines, les sources et les liens. La figure 1.1
décrit un plan qui conduit à un croisement et ne répond pas aux exigences énoncées.
Le problème. On se donne n lieux à placer sur le plan et m couples de lieux qui doivent être
liés. Le problème à résoudre consiste à déterminer, s’il est possible de placer les lieux et les
liens sur le plan de telle sorte qu’il n’y ait aucun croisement et dans l’affirmative de produire
un tel plan.
1.2.2 Programmation linéaire
Une instance du problème. Supposons qu’une personne doive suivre un régime nutritif en fruits
qui lui garantit un apport quotidien suffisant en vitamines A, B et C.
Cette personne ne mange que trois fruits : des oranges, des bananes et des pommes. Pour chaque
fruit, elle connaît son prix et la quantité de vitamines par kilo comme indiqué dans le tableau 1
Le problème. Etant donné le texte d’un programme écrit par exemple en JAVA, le problème
consiste à déterminer si ce programme se termine. Plusieurs variantes sont possibles. Ainsi si
le programme comprend une entrée, le problème pourrait consister à déterminer si le
programme se termine pour toutes les entrées possibles.
1.3 Taille d’une instance
L’algorithmique consiste à résoudre des problèmes de manière efficace. Il est donc nécessaire
de définir une mesure de cette efficacité. Du point de vue de l’utilisateur, un algorithme est
efficace si :
1. il met peu de temps à s’exécuter ;
2. il occupe peu de place en mémoire principale.
Cependant ces mesures dépendent de la taille de l’instance du problème à traiter. Il convient
donc de définir la taille d’une instance. Plusieurs définitions sont possibles dans la mesure où
une même instance peut s’énoncer de différentes manières. En toute rigueur, l’efficacité d’un
algorithme devrait prendre en compte non pas l’instance mais sa représentation fournie en
entrée de l’algorithme. Cependant la plupart des représentations raisonnables d’une instance
conduisent à des tailles similaires. Plutôt que de formaliser cette notion, nous la préciserons
pour chaque problème traité.
Si on prend comme unité de mesure le bit, nous commençons par faire quelques hypothèses.
- Le nombre de bits nécessaires pour représenter un caractère est constant (en réalité, il
dépend de la taille de l’alphabet mais celle-ci ne change pas de manière significative).
On le notera Bc.
- Le nombre de bits nécessaires pour représenter un entier est constant. Cette hypothèse
n’est valable que si, d’une part on connaît a priori une borne supérieure de la taille d’un
entier intervenant dans une instance et si, d’autre part les opérations effectuées sur les
entiers par l’algorithme ne conduisent pas à un dépassement de cette borne1 On le notera
Be.
- Dans la plupart des systèmes d’information, les informations stockées dans des fichiers
sont accessibles via des identifiants qui peuvent être des valeurs numériques (e.g. le n˚
de sécurité sociale) ou des chaînes de caractères (e.g. la concaténation du nom et du
prénom). On supposera aussi que le nombre de bits nécessaires pour représenter un
identifiant est constant et on le notera Bi.
Illustrons maintenant la taille d’une instance à l’aide des exemples précédents.
Planarité d’un graphe. Il suffit de représenter les liaisons, i.e. des paires d’identifiants. On
obtient donc 2m · Bi. Cette représentation appelle deux remarques.
D’un point de vue technique, le programme implémentant l’algorithme doit savoir où se termine
la représentation : soit par la valeur m précédant la liste des paires soit par un identifiant spécial
(différent des identifiants possibles) qui suit la liste. Dans les deux cas, on a ajouté un nombre
constant de bits.
D’un point de vue conceptuel, un lieu qui n’apparaît dans aucune liaison, est absent de la
représentation. Vis à vis du problème traité, cela n’est pas significatif car si le plan a pu être
établi, il suffit d’ajouter ces lieux hors de l’espace occupé par le plan.
Programmation linéaire. On précise d’abord m et n, puis les éléments ai,j (ligne par ligne ou
colonne par colonne), les éléments bj et finalement les éléments ci. Ces nombres ne sont pas
nécessairement des entiers. En faisant l’hypothèse que ce sont des rationnels on peut les
représenter sous forme de fractions d’entier, i.e. par deux entiers. On obtient comme taille du
problème (2 + 2m + 2n + 2m · n) · Be.
Recherche de seuils. On précise d’abord n la taille du tableau et k le seuil puis les cellules du
tableau, ce qui nous donne (en supposant que les salaires soient des entiers) (2 + n) · Be.
Déduction automatique. On indique le nombre d’hypothèses puis les hypothèses suivi de la
conclusion. A priori, les formules peuvent être de taille quelconque. En notant |ϕ| le nombre de
caractères d’une formule φ, la taille du problème est alors Be + (Pni=1 |ϕi| + |ϕ|) · Bc.
Terminaison d’un programme. La taille de l’instance est n · Bc où n est le nombre de
caractères du texte du programme.
Remarque importante. Nous nous intéressons au comportement des algorithmes sur des
instances de grande taille. De manière encore plus précise, nous souhaitons estimer la nature de
la variation du temps d’exécution de l’algorithme lorsque la taille de l’instance augmente. Aussi
il est raisonnable de :
- conserver le terme prédominant de la taille d’une instance ;
- dans ce terme, remplacer les constantes multiplicatives par 1.
Ainsi pour le cas de la programmation linéaire, le terme prédominant est 2m · n · Be. Par
conséquent, en oubliant les constantes, on prendra pour la taille d’une instance m · n.
1.4 Algorithmes
Sauf mention du contraire, on considèrera que l’algorithme est défini par une fonction dont les
entrées sont indiquées par le mot-clef Input et les sorties (il peut y en avoir plusieurs) sont
indiqués par le mot-clef Output.
Les déclarations des variables locales de la fonction sont indiquées par le mot-clef Data dans
le corps de la fonction après les entrées et les sorties et avant le bloc d’instructions de la
fonction. L’opérateur d’affectation est noté ← à ne pas confondre avec le test d’égalité = utilisé
pour construire des expressions booléennes. On n’utilisera le « ; » que pour concaténer des
instructions placées sur la même ligne. Les opérateurs de comparaison sont notés =, ≠, <, >, ≤,
≥, les opérateurs arithmétiques sont notés +, ∗ et les opérateurs logiques sont notés and, or, not.
Lorsque nous introduirons un opérateur spécifique à un nouveau type de donnée, nous en
préciserons la signification. Pour l’instant, nous travaillerons avec des tableaux dont la
dimension sera précisée lors de leur définition. Pour accéder à un élément du tableau, on utilise
la notation Tableau[indice].
Les constructeurs de programmes (avec leur interprétation usuelle) sont :
if condition then
instructions
else if condition then (optionnel)
...
else (optionnel)
instructions
end
– while condition do
instructions
end
– repeat
instructions
until condition
– for indice ← valeur initiale to valeur finale do
instructions
end
- Chaque instruction basique consomme une unité de temps (affectation d’une variable,
comparaison, …)
- Chaque itération d’une boucle rajoute le nombre d’unités de temps consommées dans
le corps de cette boucle.
- Chaque appel de fonction rajoute le nombre d’unités de temps consommées dans cette
fonction
- Pour avoir le nombre d’unité d’opérations effectuées par l’algorithme, on additionne le
tout.
Définition la notation O
Soit T(n) une fonction qui désigne le temps de calcul d’un algorithme A.
On dit que T(n) est en grand O de f(n) : T(n) =O(f(n)) ssi il existe (no , c) telle que
T(n)< = c * f(n) pour tout n> =no
Exemple
Prouvons que f(n) = 5n + 37 est en O(n)
le but est de trouver une constante c de l’ensemble R et no de l’ensemble N à partir duquel
|f(no)| <= c|no|
On déduit que c=6 fonctionne à partir du no = 37
Dans cet exemple, il est très facile de borner le temps d’exécution afin de trouver la complexité
O. Mais en générale il est difficile de trouver le seuil afin de borner le temps d’exécution. Pour
cela on applique les règles suivante pour simplifier l’expression du temps d’exécution :
- On remplace les constantes multiplicatives par 1
- On annule les constantes additives
- On ne retient que les termes dominants
Exemple
g(n)=4n3 – 5n2 + 2n + 3 = O(n3)
1.5.4 Différente cas de complexité d’un algorithme
Nous notons Dn l’ensemble des données de taille n et T(d) le coût de l’algorithme sur la donnée
d.
Complexité au meilleur : Tmin(n) = mind∈Dn C(d). C’est le plus petit nombre d’opérations
qu’aura à exécuter l’algorithme sur un jeu de données de taille fixée, ici à n. C’est une borne
inférieure de la complexité de l’algorithme sur un jeu de données de taille n.
Complexité au pire : Tmax(n) = maxd∈Dn C(d). C’est le plus grand nombre d’opérations qu’aura
à exécuter l’algorithme sur un jeu de données de taille fixée, ici à n. Par exemple lorsqu’on
parcours toute une liste de taille n pour chercher un élément qui n’y est pas ou qui est le
dernier.
Avantage : il s’agit d’un maximum, et l’algorithme finira donc toujours avant d’avoir effectué
Tmax(n) opérations.
Inconvénient : cette complexité peut ne pas refléter le comportement « usuel » de l’algorithme,
le pire cas pouvant ne se produire que très rarement, mais il n’est pas rare que le cas moyen soit
aussi mauvais que le pire cas
Complexité en moyenne :
C’est la moyenne des complexités de l’algorithme sur des jeux de données de taille n (en toute
rigueur, il faut bien évidemment tenir compte de la probabilité d’apparition de chacun des jeux
de données).
Avantage : reflète le comportement « général » de l’algorithme si les cas extrêmes sont rares
ou si la complexité varie peu en fonction des données.
Inconvénient : la complexité en pratique sur un jeu de données particulier peut être nettement
plus importante que la complexité en moyenne, dans ce cas la complexité en moyenne ne
donnera pas une bonne indication du comportement de l’algorithme.
1.5.5 Classe de complexités classiques
On compare la complexité de n+1 à n. C-à-d on incrémente le paramètre n et regarde le nombre
d’instruction élémentaire qu’il faut rajouter à la complexité de n. soit C le coût d’un algorithme.
- Si C(n+1) = C(n) alors O(1) => complexité constante.
- Si C(n+1) = C(n) + 1 alors O(n) => complexité linéaire
- Si C(n+1) = C(n) + e difficile à conclure mais en continuant l’incrémentation jusqu’à
avoir 2n. C(2n) = C(n) + 1 alors O(log2(n)) => complexité logaritmique
- Si C(n+1) = C(n) + n alors O(n2) => complexité quadratique
- Si C(n+1) = 2 * C(n) alors O(2n) => complexité exponentielle
O(1) correspond à la situation où on doit retourner les k premiers éléments d’une liste.
O(n) => lorsqu’on doit chercher un élément dans une liste non ordonnée (On doit parcourir
toute la liste)
O(log2(n)) => lorsqu’on doit chercher un élément dans une liste ordonnée d’élément
O(n2) traitement sur toutes les paires dans une liste. Pour chaque élément dans la liste, il faut
reparcourir toute la liste et refaire le traitement avec toutes les paires.
O(2n) => nombre de sous ensemble dans un ensemble. C’est ce qui se passe lorsqu’on parcours
les arbres binaires.
1.5.6 Illustration cas du trie par insertion
a- Problématique du trie
Entrée : une séquence de n nombres, a1, ..., an.
Sortie : une permutation, a’1, … , a’n , de la séquence d’entrée, telle que a’1 ≤ a’2 ≤ … ≤ a’n
c- Algorithme
d- Exemple
Soit le tableau suivant : [5 ; 2 ; 4 ; 6 ; 1 ; 3]
e- Complexité
Nous passons en revue les différentes étapes de notre algorithme afin d’évaluer son temps
d’exécution. Pour ce faire, nous attribuons un coût en temps à chaque instruction, et nous
comptons le nombre d’exécutions de chacune des instructions. Pour chaque valeur de j ∈ [2,n],
nous notons tj le nombre d’exécutions de la boucle tant que pour cette valeur de j. Il est à noter
que la valeur de tj dépend des données
Complexité au meilleur : le cas le plus favorable pour l’algorithme TRI-INSERTION est
quand le tableau est déjà trié, comme le montre le cas j = 4 du tableau ci-dessus. Dans ce cas
tj = 1 pour tout j.
T(n) peut ici être écrit sous la forme T(n) = an + b, a et b étant des constantes indépendantes
des entrées, et T(n) est donc une fonction linéaire de n.
Complexité au pire : le cas le plus défavorable pour l’algorithme TRI-INSERTION est quand
le tableau est déjà trié dans l’ordre inverse, comme le montre le cas j = 5 tableau ci-dessus.
Dans ce cas tj = j pour tout j.
T(n) peut ici être écrit sous la forme T(n) = an2 + bn + c, a, b et c étant des constantes, et T(n)
est donc une fonction quadratique de n.
f- Ordre de grandeur
Ce qui nous intéresse vraiment, c’est l’ordre de grandeur du temps d’exécution. Seul le terme
dominant de la formule exprimant la complexité nous importe, les termes d’ordres inférieurs
n’étant pas significatifs quand n devient grand. On ignore également le coefficient
multiplicateur constant du terme dominant. On écrira donc, à propos de la complexité du tri par
insertion :
meilleur cas : Θ(n).
pire cas : Θ(n2).
en moyenne : Θ(n2).
En général, on considère qu’un algorithme est plus efficace qu’un autre si sa complexité
dans le pire cas a un ordre de grandeur inférieu
2. Algorithmes récursifs
Un algorithme est dit récursif lorsqu’il est défini en fonction de lui-même.
2.2 Principe
- Une fonction est dite récursive si elle comporte, dans son corps, au moins un appel à
elle-même. Exemple pour calculer la somme des entiers de 1 à n, on calcul la somme
jusqu’à n-1 et on ajoute n ; (n + somme(n-1)).
- Une fonction peut-être d’une récursivité terminale ou non terminale. Exemple
Récursivité terminale
Fonction Bonjour(n)
Début
Si (n==0)
Alors Ecrire (‘’Bonjour’’)
Sinon Bonjour(n-1)
Fin
2.3 Implémentation
Pour implémenter une fonction récursive, il faut non seulement veiller à ce que la fonction
s’appelle elle-même, mais surtout identifier une condition d’arrêt. Exemple
Fonction Bonjour(n)
Début
Si (n==0)
Alors Ecrire (‘’Bonjour’’)
Sinon Bonjour(n-1) Fin
Dans l’algorithme ci-dessus lorsque n=0, on s’arrête. Arrive-t-on à n=0 ? la condition
terminale ne sert à rien si elle ne devient jamais vraie. Exemple Bonjour(-2) avec l’algo ci-
dessus, il y aura une infinité d’exécution. Pour éviter cela, il faut ajouter une condition dans la
boucle :
Fonction Bonjour(n)
Début
Si (n < 0)
Alors Ecrire(‘’erreur d’exécution’’)
Sinon
Si (n==0)
Alors Ecrire (‘’Bonjour’’)
Sinon Bonjour(n-1)
FinSi
FinSi
Fin
Théorème de Gödel : Il n’existe pas de moyen automatique pour savoir si un programme
termine ou pas. Si aucune méthode n’est générale, le principe de récurrence aide souvent.
2.4 Exemple d’algorithme récursif : les tours de Hanoï
Le problème
Le jeu est constitué d’une plaquette de bois où sont plantées trois tiges. Sur ces tiges sont enfilés
des disques de diamètres tous différents. Les seules règles du jeu sont que l’on ne peut déplacer
qu’un seul disque à la fois, et qu’il est interdit de poser un disque sur un disque plus petit. Au
début tous les disques sont sur la tige de gauche, et à la fin sur celle de droite.
Algorithme
Complexité
On compte le nombre de déplacements de disques effectués par l’algorithme HANOÏ invoqué
sur n disques.
d’où l’on en déduit que C(n) = 2n-1. On a donc ici un algorithme de complexité exponentielle.
2.5 Diviser pour régner
La méthode de diviser pour régner est une m´méthode qui permet, parfois de trouver des
solutions efficaces à des problèmes algorithmiques. L’idée est de découper le problème initial,
de taille n, en plusieurs sous-problèmes de taille sensiblement inférieure, puis de recombiner
les solutions partielles.
a- Exemple Algorithme de trie par fusion
L’exemple typique est l’algorithme de tri fusion : pour trier un tableau de taille n, on le découpe
en deux tableaux taille n/2 et l’étape de fusion permet de recombiner les deux solutions en n −
1 opérations. On peut l’écrire ainsi :
b- Complexité
On compte le nombre T(n) de comparaison effectuées par l’algorithme
Ce type d’équation est résolue par le théorème maître énoncé comme suit
Théorème Maître :
On identifie les paramètres : a = 1 car on appelle soit à gauche, soit à droite (ou on a fini, mais
on se place dans le pire des cas), b = 2 car les sous-problèmes sont de taille n/2 et d = 0 car on
se contente de renvoyer la solution, donc en temps constant. La complexité de la dichotomie
est donc O(log n).
b- Exponentielle rapide
En informatique, l'exponentiation rapide est un algorithme utilisé pour calculer rapidement de
grandes puissances entières. Il s’agit de calculer xn pour x et n donnés, en calculant la
complexité par rapport à n. La méthode naïve (multiplier n fois 1 par x) donne une complexité
linéaire. On peut faire mieux en utilisant le fait que
Par définition c'est le nombre de nœuds du chemin le plus long dans l'arbre. La hauteur h d'un
arbre correspond donc au nombre de niveau maximum
b- Degré d’un arbre
Le degré d'un arbre est égal au plus grand des degrés de ses nœuds :
- Parcours suffixe
Dans le parcours suffixe, la racine est traitée après les appels récursifs sur les sous-arbres
gauche et droit (faits dans cet ordre). Le parcours suffixe de l'arbre ci-dessus parcourt les nœuds
dans l'ordre [3,2,5,7,6,4,1,11,12,10,9,14,15,13,8,0].
b- Parcours en largeur
Le parcours en largeur consiste à parcourir l'arbre niveau par niveau. Les nœuds de niveau 0
sont sont d'abord parcourus puis les nœuds de niveau 1 et ainsi de suite. Dans chaque niveau,
les nœuds sont parcourus de la gauche vers la droite. Le parcours en largeur de l'arbre ci-dessus
parcours les nœuds dans l'ordre [0,1,8,2,4,9,13,3,5,6,10,14,15,7,11,12].
3.3 Algorithme de parcours d’un arbre
a- préfixe
Procédure Préfixe(A: arbre)
Début
Si (NON est_arbre_vide (A)) alors
Ecrire (Racine (A))
Préfixe (FilsGauche (A))
Préfixe (FilsDroite(A))
Finsi
Fin
b- infixe
Procédure Infixe(A: arbre)
Début
Si (NON est_arbre_vide (A)) alors
Infixe (FilsGauche (A))
Ecrire (Racine (A))
Infixe (FilsDroite(A))
Finsi
Fin
c- suffixe
d- parcours en largeur
l’algorithme de parcours en largeur se fera dans la section où sera vue la file.
3.4 Arbre binaire de recherche
Un arbre binaire de recherche (ABR) est un arbre binaire ordonné talque pour tout nœud n:
- Toutes les valeurs du sous arbre gauche de n sont inférieures ou égales à la valeur de n
- Toutes les valeurs du sous arbre droit de n sont supérieures ou égales à la valeur de n
NA: Généralement, les valeurs dans un ABR sont uniques; on n’admet pas de répétition de
valeur pour éviter les confusion . Mais si jamais ceci arrive : par exemple si un arbre contient
deux fois la valeur 4, par convention, la deuxième valeur est stockée dans le sous-arbre droit
ayant pour racine 4
3.5 Opération sur arbre binaire de recherche (ABR)
a- Recherche d’un élément dans un arbre (ABR)
La recherche est dichotomique, à chaque étape, un sous arbre est éliminé.
Les listes doublement chaînées: Dans ces listes, les éléments sont chaînées à l’aide de
deux pointeurs, et non un seul, ce qui permet de parcourir la liste dans les deux sens.
Les listes circulaires: dans ces listes, le dernier élément pointe sur le premier.
8.2 Les opérations
Les code seronts en C
a- Création d’une liste
Description de la structure
Une donnée, ici un nombre de type int: on pourrait remplacer cela par n'importe quelle
autre donnée (un double, un tableau…). Cela correspond à ce que vous voulez stocker,
c'est à vous de l'adapter en fonction des besoins de votre programme.
D’une manière générique, l'idéal est de faire un pointeur sur void :void*. Cela permet
de faire pointer vers n'importe quel type de données.
Un pointeur vers un élément du même type appelé suivant. C'est ce qui permet de lier
les éléments les uns aux autres : chaque élément sait où se trouve l'élément suivant en
mémoire.
En revanche, il ne sait pas quel est l'élément précédent, il est donc impossible de
revenir en arrière à partir d'un élément avec ce type de liste
Liste *initialisation()
{
Liste *liste = malloc(sizeof(*liste));
Element *element = malloc(sizeof(*element));
element->nombre = 0;
element->suivant = NULL;
liste->premier = element;
return liste;
}
if (liste->premier != NULL)
{
Element *aSupprimer = liste->premier;
liste->premier = liste->premier->suivant;
free(aSupprimer);
}
}