Documente Academic
Documente Profesional
Documente Cultură
Synthèse de cours
Méthodes de Programmation
Partie 2 : Constructions avancées
Marc Dessoy
Préambule 5
I Concepts communs 7
1 La récursion 9
3
4 BIHD3 - Meth Pgm
Sources
Ces notes sont majoritairement inspirées des notes [Sch12] du Professeur Pierre-Yves Schobbens
de la faculté d’informatique de Namur ainsi que des notes personnelles prises lors des cours et des
TP.
Ces notes complètent (et parfois corrigent) ma synthèse de cours sur les notions de Méthodes de
programmation vues en bac 2 [Des11]. Cette partie 1 constitue d’ailleurs un pré-requis important
pour la compréhension des notions de cette deuxième partie.
Mise en garde
Le seul objectif de ce document privé est de servir de support d’étude et de mémento structuré à
l’usage de son rédacteur. Il n’est certainement pas réputé exempt d’erreurs ou d’omissions.
Par camaraderie, ce document (PDF) est parfois mis à disposition de tout étudiant qui en fait la
demande, soit directement par le rédacteur, soit "via-via". Vous êtes toutefois invité à n’utiliser
les données qui y apparaissent qu’à des fins éducatives personnelles et certainement pas à des fins
commerciales.
Si vous avez apprécié ce partage, d’autres y trouveront peut-être aussi un bénéfice académique.
Pensez-y ! Toute remarque, toute erreur détectée, toute suggestion pour rendre certains passages
plus clairs, peut (devrait) être communiquée par courriel à l’adresse de l’administrateur du site
www.madit.be. Nous nous ferons un plaisir de corriger le document.
6 BIHD3 - Meth Pgm
Concepts communs
7
1
La récursion
Sommaire
1.1 Récursion bien fondée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.2 Récursion terminale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.3 Structures de données récursives . . . . . . . . . . . . . . . . . . . . . . . . 14
1.4 Exercices de TP - procédures récursives . . . . . . . . . . . . . . . . . . . . 18
9
101.1 Récursion bien fondée BIHD3 - Meth Pgm
Définition 1.1
Une définition est (directement) récursive si le terme défini apparaît lui-même dans la définition.
1
1.1 Récursion bien fondée
1.1.1 Relation bien fondée et cas de base
Définition 1.2
Une relation binaire quelconque est bien fondée s’il ne peut y avoir de suite infinie strictement décroissante.
Exemple 1.1.1
factorielle(0) = 1
Il n’y a aucune occurrence dans la définition, donc elles sont toutes plus simples.
factorielle(n) = n * factorielle(n-1) si n > 0
L’occurrence factorielle(n-1) est plus simple que factorielle(n) si on utilise la valeur de l’argument
comme mesure : n − 1 < n.
A contrario, factorielle(n) = factorielle(n) est correct mais pas bien fondé !
Définition 1.4
Les éléments b qui n’ont pas d’élément "plus simple" sont appelés les "cas de base".
Exemple 1.1.2
comme la mesure d’une liste est sa longueur, le cas de base d’un algorithme sur les listes est une liste de
longueur 0, c’est-à-dire une liste vide.
Exemple 1.1.3
comme la mesure d’un arbre est sa hauteur, le cas de base d’un algorithme sur les arbres est une arbre de
hauteur 0, c’est-à-dire un arbre vide.
Les récursions croisées sont admises en Pascal. Il faut d’abord déclarer les procédures et fonctions comme forward
pour que le compilateur connaisse leur type avant qu’elles ne soient utilisées :
X X X X X X X X X X X X
X X X X
X X X X X X
X X X X X
X X X X
X X X X X X X X X X X X
type
xdim, ydim { intervalles } ;
direction = nord,est,sud,ouest;
labychar = char { ’X’ , ’ ’ , ’.’} ;
position = record x: xdim; y: ydim end ;
var
laby: array [xdim,ydim] of labychar;
Où
accessible teste que la position n’est pas un mur et n’est pas déjà marquée
pas(pos,d) avance d’un pas dans la direction d.
Base de la preuve : Le programme termine car le nombre de cases blanches (non marquées) diminue toujours,
c’est notre variant. Il atteint toutes les cases accessibles car il essaie toutes les directions possibles.
NB : à chaque impasse, on revient à la position de départ.
Le chemin vers la sortie est constitué par les arguments de sortir ; on peut les conserver dans une liste pour les
afficher.
1 int factorial(int n) {
2 int res=1;
3 for(; n; --n) res *= n;
4 return res;
5 }
Ce code fonctionne parfaitement bien. Il a l’unique défaut d’être finalement assez éloigné de la définition
mathématique de la factorielle, c’est-à-dire que la plupart des gens le jugeront peu naturel.
Considérons maintenant une implémentation plus naturelle, très proche de la définition mathématique ... mais
non-terminale !
Cette implémentation récursive est assurément très mauvaise. Elle consomme beaucoup de mémoire en pile, donc
c’est lent et ça peut aussi se terminer prématurément si on l’appelle avec un paramètre trop grand (stack overflow).
La version récursive terminale pallie précisément ce problème :
Le paramètre accu joue le rôle d’un accumulateur. L’évaluation de f(5,1) conduit à la suite d’appels
f(5,1) -> f(4,5) -> f(3,20) -> f(2,60) -> f(1,120).
1.2.3 L’accumulateur
Pour écrire une fonction récursive sous forme terminale, il est souvent nécessaire de modifier sa signature en y
ajoutant un paramètre.
Ce paramètre accu joue le même rôle que la variable de boucle res. On peut aussi le considérer comme un
paramètre qui fait sens et qui améliore notre fonction. Ainsi dans l’exemple tfactorial, ce paramètre accu sert
à choisir la valeur du cas de base (factorielle(0)).
En général, on souhaite masquer ce paramètre en trop. On définit alors deux fonctions : la première que l’on cache
et qui prend deux paramètres, la seconde qui appelle la première et expose la signature voulue. Pour faire cela, il
existe diverses méthodes selon le langage (et les paradigmes) utilisé.
[1]. Cette notion a été abordée en TP, mais sans support. Source intéressante : http ://blog.fastconnect.fr/ ?p=174
En C, on peut utiliser le mot static pour empêcher une fonction d’être exposée dans l’interface du module.
1
2
static int sFactorial (int n, int accu) {
if (n == 0) return accu;
1
3 else return sFactorial(n-1, n*accu);
4 }
5 int factorial (int n) {
6 return sFactorial(n, 1);
7 }
La plupart des langages objets offrent des modificateurs d’accès. En Java, on utilisera les modificateurs public et
private :
1 public Mathematiques {
2 private static sFactorial(int n, int accu) {
3 if (n == 0) return accu;
4 else return sFactorial(n-1, n*accu);
5 }
6 public static factorial (int n) {
7 return sFactorial(n, 1);
8 }
9 }
Certains langages, tels que Python ou C], permettent de donner une valeur par défaut à un paramètre. Ce qui
peut s’avérer utile.
1 int fibonacci_naive(int n) {
2 if (n == 0) return 0 ;
3 if (n == 1) return 1 ;
4 return fibonacci_naive(n-1) + fibonacci_naive(n-2) ;
5 }
Pour écrire la fonction de Fibonacci sous forme récursive terminale, nous avons besoin d’utiliser deux accumulateurs
(accu0 et accu1).
L’origine vient de l’implémentation itérative qui utilise deux variables de boucle (res0 et res1) et du fait que la
définition récursive donne deux cas de base (fibonacci(0) et fibonacci(1)).
Dans ce cas, l’implémentation récursive terminale apporte beaucoup plus qu’une bonne gestion de la pile d’exécution.
Codage en Pascal
Les structures de données récursives sont interdites en Pascal, sauf pour les pointeurs.
On les implémente donc par des pointeurs. Ce codage se dérive automatiquement de la définition récursive.
type
liste = ^cell ;
cell = record
tête: information;
reste: liste ;
{ inv : longueur ( l^.reste) < longueur(l) } ;
end ;
Cette façon de coder les listes s’appelle les listes (simplement) chaînées.
Remarque : il faut ajouter l’invariant de données sur liste, car rien ne garantit en Pascal que les pointeurs sont
sans cycles (bien fondés).
type
arbreBinaire = ^cell;
cell = record
tête: information;
gauche: arbreBinaire
{ invariant : hauteur (a^.gauche) < hauteur (a) } ;
droit : arbreBinaire
{ invariant : hauteur (a^.droit) < hauteur (a) } ;
end ;
Attention à l’invariant
3 8
2 4 7 9
dans(e, newNode(f,g,d)) =
true si e = f
or dans(e,g) si e < f
or dans(e,d) si e > f
On ne parcourt qu’une branche dans l’arbre. La complexité est la hauteur de l’arbre, donc en O(n) plutôt que le
nombre d’éléments en O(2n )
père
petit-fils
type
listearbreOrd = ^cell;
arbreOrd = listearbreOrd; { inv(a) : a <> nil }
cell = record
e: information;
son: listearbreOrd;
brother: listearbreOrd;
end;
Notons que son représente (un pointeur vers) la liste des fils, tandis que brother est le pointeur vers le cadet (le
reste de la liste).
arbreOrd
son
Invariant de représentation
Un arbre ordonné est
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int fact(int n) {
5 if (n<0) exit(1) ; // check precondition
6 if (n==0) return 1 ;
7 return n * fact(n-1);
8 }
9
10 /**/
11 void main() {
12 int n = 10;
13 printf("\n Le factoriel de %d est %d \n ", n , fact(n));
14 }
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 static int fact_terminale(int n, int accu) {
5 if (n==0) return accu ;
6 return fact_terminale(n-1, n*accu);
7 }
8
9 /* fonction appelante */
10 int tfact{int n} {
11 if (n<0) exit(1) ; // check precondition
12 return fact_terminale(n,1); // 1 = cas de base
13 }
14
15 /**/
16 void main() {
17 int n = 10;
18 printf("\n Le factoriel de %d est %d \n ", n , tfact(n));
19 }
1 #include <stdio.h>
2 #include <stdlib.h>
3 #define SIZE 4
4
5 int minimum(int a, int b) {
6 if (a < b) return a;
7 return b;
8 }
9
10 static int binarySearchMin(int[] a, int first , int last) {
11 if (last < first)
12 exit(1);
13 if (last == first)
14 return a[first];
15 int mid = (first+last)/2;
16 int leftmin = binarySearchMin (a, first, mid);
17 int rightmin = binarySearchMin (a, mid+1, last);
18 return minimum(leftmin,rightmin);
19 }
20
21 int findMinimum (int[] a) {
22 int size = sizeof(a)/sizeof(*a); // calcul de la taille du tableau
23 return findMin(a,0,SIZE-1);
24 }
25
26 void main() {
27 int t[SIZE];
28 printf("\n Enter a sorted array of %d digits", SIZE);
29 for(x = 0; x < SIZE; x++){
30 scanf("%d",&num[x]); // saisie et affectationd dans un tableau
31 }
32 printf("\n The minimum value is %d",findMinimum(t));
33 }
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 void mergesort(int a[], int b[],int first, int last);
5 void merge(int a[], int b[], int firstw, int mid, int high);
6
7 int main() {
8 int a[8]={10,1,15,6,9,17,2,13};
9 int b[8];
10 mergesort(a,b,0,7);
11 printf("the sorted array is\n");
12 for(int i=0; i<8; i++) printf("%d\n",a[i]);
13 return 0;
14 }
15
16 void mergesort(int a[], int b[], int first, int last) {
17 if (first <= last) {
18 int med=(first+last)/2;
19 mergesort(a, b, first, med);
20 mergesort(a, b, med+1, last);
21 merge(a, b, first, med, last);
22 }
23 }
24
25 // Efficient variant
26 void merge(int a[], int b[], int first, int med, int last) {
27 // copy first half of array a to auxiliary array b
28 int j = first ;
29 int i = 0;
30 while (j <= med) b[i++]=a[j++];
31 // copy back next-greatest element at each time
32 int k = first ;
33 i = 0;
34 while (k<j && j<=last) {
35 if (b[i]<=a[j])
36 a[k++]=b[i++];
37 else
38 a[k++]=a[j++];
39 }
40 // copy back remaining elements of first half (if any)
41 while (k<j) a[k++]=b[i++];
42 }
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 typedef struct node {
5 int val;
6 struct node *next;
7 } node;
8
9 typedef struct node* list;
Construction
Tester la présence d’une valeur donnée dans une liste simplement chaînée d’entiers.
Calculer le nombre d’occurrences d’une valeur donnée dans une liste simplement chaînée d’entiers.
Supprimer toutes les occurrences d’une valeur donnée dans une liste simplement chaînée d’entiers.
1
2
list copie(list l) {
list p;
1
3 if (l==NULL) p = NULL ;
4 else p = ajouterEnTete1(copie(l->next),l->val);
5 return p;
6 }
1 list renverser(list l) {
2 list p;
3 if(l==NULL) p=NULL;
4 else {
5 p=renverser(l->next);
6 p=ajouterEnQueue1(p,l->val);
7 }
8 return p;
9 }
1 void main() {
2 list l1=NULL;
3 list l2=NULL;
4 list l3=NULL;
5
6 for (int i=1 ; i<10 ; i++) l1 = ajouterEnTete1(l1,i);
7
8 printf("\n Affichage de la liste l1 ");
9 afficher(l1);
10
11 printf("\n \n Le nombre d'occurrences de '2' est %d \n ", occurence(l1,2));
12
13 l2=copie(l1);
14 printf("\n Le résultat de la copie de l1 est l2 = ");
15 afficher(l2);
16
17 l3=commun(l1,l2,l3);
18 printf(" \n \n Les valeurs communes entre deux listes sont ");
19 afficher(l3);
20
21 l1=renverser(l1);
22 printf(" \n \n L'inverse de l1 est \n");
23 afficher(l1);
24
25 detruire(&l1);
26 printf(" \n \n La liste l1 est détruite \n");
27 }
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int num(int a[],int i, int size);
5 int exp(int);
6
7 int main(void) {
8 int a[8]={1,0,0,0,0,0,0,0};
9 printf("size of array %d", (int)(sizeof(a)/sizeof(*a)));
10 printf("!!!The number %d = ", num(a,0,7));
11 return EXIT_SUCCESS;
12 }
13
14 int num(int a[], int i, int size){
15 if(i >= size) return 0;
16 return a[i] * exp(size-i) + num(a,i+1,size);
17 }
18
19 int exp(int e){
20 if(e==0) return 1;
21 return 2 * exp(e-1);
22 }
0.a = 0
(2.a).b = 2.(a.b)
(2.a + 1).b = 2.(a.b) + b
Sommaire
2.1 Preuves de programmes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.2 Temps d’exécution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.3 Espace en mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.4 Exemples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
29
302.1 Preuves de programmes BIHD3 - Meth Pgm
P ⇒ P0 wp
{P 0 } S {Q0 }
sp Q0 ⇒ Q
{P } S {Q}
Donc pour conclure à {P } S {Q}, il suffit de prouver la correction de S par rapport à une postcondition Q0 plus
forte que Q et/ou une précondition P 0 plus faible P . Ce qui conduit à deux approches.
A chaque structure algorithmique correspond une méthode adéquate. En général avant de prouver une méthode, il
faut prouver sa précondition.
pre(x) = init(x)
pre(ab) = pre(a) ∧ ¬f ree(a)
pre(a.c) = pre(a)
pre(a[i]) = pre(i) ∧ (min ≤ i ≤ max) ∧ init(a[i])
pre(~e) = ∧ ni=1 pre(ei ) = pre(e1 ) . . . pre(en )
pre(f (~e)) = pre(f )[~x:=~e] ∧ pre(~e)
Définition 2.2 2
S = (x := e) ⇒ sp(x := e , P ) ≡ P[x/x0 ] ∧ x = e[x/x0 ]
Définition 2.3
S = (a[i] := e) ⇒ sp(a[i] := e , P )
≡ P[a/a0 ] ∧ a[i[a/a0 ] ] = e[a/a0 ] ∧ ∀j 6= i[a/a0 ] : a[j] = a0 [j]
Séquence
Définition 2.4
sp(S1 ; S2 ; · · · ; Sn , P ) = An
A0 = P
où
Ai = sp(Si , Ai−1 ) ∀i : 1 ≤ i ≤ n
Méthode :
Sélection
soit une condition C,
Définition 2.5
sp(S1 , P ∧ C)
sp(if C then S1 else S2 , P ) = ∨
sp(S , P ∧ ¬C)
2
Méthode :
Itération
Invariant
Définition 2.6
Un invariant (ou relation invariante) d’une boucle,
2 while C do S
sp( while C do S , P ) = I ∧ ¬C
où
• ¬C est la condition d’arrêt ;
• et I est l’invariant de boucle tel que P ⇒ I (invariant vrai en entrée dans la boucle)
Preuve de terminaison
Définition 2.8
Prouver la terminaisond’une itération revient à trouver une fonction
{f (~x) = f0 }S{f (~x) < f0 }
f : ~x 7→ N telle que :
{f (~x) = 0} ⇒ ¬C
Méthode
(b) Traiter la séquence et déterminer la plus forte postcondition juste avant la boucle
Q0 = sp(S1 ..Si , P 0 ) ≡ { P 0 ∧ Sinit }
2. Déterminer l’invariant de la boucle
(a) Trouver le plus fort Inv (intuitivement)
?
(b) Contrôle 1 (Après) : { Inv ∧ ¬C ∧ Scloture } ⇒ P ost. Sinon : renforcer Inv.
?
(c) Contrôle 2 (Avant) : Q0 ⇒ Inv
?
(d) Contrôle 3 (Pendant) : { Inv ∧ C ∧ Sboucle } ⇒ Inv
3. Déterminer la plus forte postcondition de la boucle (invariant et condition d’arrêt) :
sp(while C do S , P ) = Inv ∧ ¬C
4. Prouver la terminaison de la boucle
(a) Déterminer les paramètres de variation ~x et leur domaine de variation
(b) Déterminer la fonction f (~
x)
?
(c) Prouver la décroissance, soit { f0 ∧ Sboucle } ⇒ f1 < f0
?
(d) Prouver l’arrêt, soit { f (~
x) = 0 } ⇒ ¬C
Définition 2.9
Séquence
Définition 2.10
wp(S1 ; S2 ; · · · ; Sn , Q) = A0
An = Q
où
Ai = wp(Si , Ai+1 ) ∀i : 0 ≤ i < n
Sélection
Définition 2.11
C ∧ wp(S1 , Q)
wp(if C then S1 else S2 , Q) = ∨
¬C ∧ wp(S , Q)
2
Itération
Définition 2.12
Voir aussi [Sch12, slides 38,39] sur boucle for et boucle while
Exemples : voir [Des11, § 3.4.4]
2 Appel de fonction
a)) ∧ V p(~
pre(f (~ a) < v0 ⇒ post[f (~
a)/f, ~
a/~
x]
où
~a arguments effectifs
~x paramètres formels
Inv invariant de contexte. Il ne contient pas de variables modifiées par p
V p le variant de chaque appel récursif doit être plus petit doit être plus petit que chaque sous-programme q
dans lequel il se trouve. v0 est la valeur initiale de V q(~x).
Règles de calcul
Séquence
Additionner les temps de chaque instruction
Sélection
Additionner le temps du test + max(’then’,’else’)
Tif C then S1 else S2 (n) ≤ TC (n) + max { TS1 (n) + TS2 (n) }
Boucle for
Calculer les bornes l et h puis h − l + 1 fois le test, le corps et l’incrément
En pratique, on a souvent
Tfor i=l...h do S = (h − l + 1) ∗ TS
Boucle while
Trouver le variant V qui diminue à chaque passage dans la boucle. Sa valeur initiale est une borne supérieure du
nombre d’itérations
Twhile C do S ≤ (V0 + 1) ∗ TC + V0 ∗ TS
La formule devient une égalité si le variant diminue de 1 à chaque passage, et qu’on sort de la boucle quand le
variant vaut 0.
En pratique on a souvent TC ≤ TS
Twhile C do S ≤ V0 ∗ TS
Appel de méthode
Le temps d’évaluer un appel de sous-programme (procédure ou fonction) est le temps d’évaluer ses arguments,
d’appeler le sous-programme, d’exécuter le corps du sous-programme.
Vf est le variant de f , une fonction des paramètres de f qui mesure la complexité.
m
X
Tf (a1 ,...,am ) = Tai + O(1) + Tf (Vf (~
a))
i=1
n T (n)
1 2k
2 4k
3 6k
4 8k
... ...
n 2n ∗ k
Équations récurrentes
Établir une équation récursive donnant T (n) en fonction soit de T (n − 1) soit de T (n/d).
c est le nombre d’appels récursifs.
c=1 ⇒ T (n) = O(nk+1 )
T (n) = c ∗ T (n − 1) + a.nk
c>1 ⇒ T (n) = O(cn )
c > dk ⇒ T (n) = O(nlogd c )
n
T (n) = c ∗ T ( ) + a.nk
c = dk ⇒ T (n) = O(nk log n)
d
c < dk ⇒ T (n) = O(nk )
Pour les sous-programmes récursifs, on crée une copie des paramètres et variables locales à chaque appel récursif.
Il faut donc multiplier l’espace de ceux-ci par la profondeur d’appel, qui est bornée par la valeur initiale du variant.
2.4 Exemples
2.4.1 Primalité
Code Pascal
2 function estPremier(n: integer) : boolean ;
{ Pré : n > 0 }
{ Post : premier = exists i : i divise n et 1 < i < n }
var
i : integer ;
p : boolean ; { premier }
begin
i := 2 ;
p := true ;
while ((sqr(i) <= n) and p) do
{ inv : p = forall j, 1 < j < i : j ne divise pas n }
{ variant: floor(sqrt(n)) - i }
begin
p := (n mod i <> 0) ;
i := i + 1;
end ;
estPremier := p;
end ;
NB :
– dans une fonction en Pascal, ne jamais utiliser le nom de la fonction comme variable au sein de l’algorithme.
– sqr : carré ; sqrt : racine carrée ! !
Temps
Temps d’exécution avant et après la boucle : affectations en O(1)
Invariant de la boucle I = { ∀j : 1 < j < i : n mod j 6= 0 }
√
Variant de la boucle V = b nc − i
√
Soit V0 avec i = 2 en entrée de la première itération : V = b nc − 2
Temps de la boucle :
Twhile C do S
≤ (V0 + 1) ∗ TC + V0 ∗ TS
√ √
≤ ( n − 2 + 1) ∗ O(1) + ( n − 2) ∗ O(1)
√ √
≤ ( n − 1) ∗ O(1) + ( n − 2) ∗ O(1)
√ √
≤ O( n) ∗ O(1) + O( n) ∗ O(1)
√
≤ O( n)
√
⇒ Tp (n) = O( n)
Espace mémoire
Ep (n) = O(1)
2.4.2 Fibonacci
Définition
√ n √ n
1+ 5
− 1−2 5
F ib(n) =
2
√
5 2
de manière récursive :
Preuve
En posant :
√ √
1 1+ 5 1− 5
a= √ R1 = R2 =
5 2 2
Code Pascal
Temps d’exécution
équation de récurrence
T (0) = T (1) = c
T (n) = T (n − 1) + T ((n − 2) + b(: n > 1)
= c ∗ F ib(n + 1) + b ∗ (F ib(n + 1) − 1)
= O(R1n )
Espace mémoire
On a (espace var et paramètres * la profondeur des appel), soit 1 ∗ V0 = O(n)
⇒ T (n) = O(2n )
Un temps exponentiel très inefficace !
⇒ T (n) = O(n)
T (n) = b.(n + (n − 1) + · · · + 2 + 1)
Xn
= b. i
i=1
n(n + 1)
= b.
2
= O(n2 )
n
X n(n + 1)
i=
2
i=1
n
X
i2 = 2(n+1) − 1
i=1
2.5 Exercices
2.5.1 Exercices du TP2 - temps d’exécution
Exercice 2.1
Construire deux fonctions, une non-récursive (utilisant une boucle) et une récursive, correspondant à la
spécification suivante. Calculer leur complexité théorique.
2
En-tête : int sommeimpairs(int n)
Précondition : n est positif
Pn ou nul
Postcondition : res = i=1 (2i − 1) (res = la somme des n premiers naturels impairs)
Exercice 2.2
Construire deux fonctions, une non-récursive (utilisant une boucle) et une récursive, correspondant à la
spécification suivante. Calculer leur complexité théorique.
En-tête : int nbch(tab t, int n)
Précondition : 0 ≤ n ≤ 200 ∧ t[1..n] est initialisé
Postcondition : res est le nombre de chiffres dans t[1..n]
Remarque : Pour déterminer si un caractère c est un chiffre ou pas, on peut utiliser l’expression de test (c ≥ ’0’)
and (c ≤ ’9’).
Exercice 2.3
Pour chercher le minimum d’un tableau d’entiers, on peut procéder comme suit : on divise le tableau en deux
parties aussi égales que possible ; on calcule le minimum de chacun de ces deux "sous-tableaux" ; puis on prend
le plus petit de ces minima.
Construire une fonction récursive correspondant à cette méthode ; calculer ensuite sa complexité.
En-tête : int mintab(tab t, int bi,int bs)
Précondition : 1 ≤ bi ≤ bs ≤ 200 ∧ t[bi..bs] est initialisé
Postcondition : mintab = mint[k] | bi ≤ k ≤ bs
Exercice 2.4
On peut calculer le plus grand commun diviseur (pgcd) de deux naturels strictement positifs a et b comme suit.
Si a = b, alors a est le pgcd. Sinon, il s’agit du pgcd de b et a - b (en supposant que a est la plus grande des
deux valeurs). Ainsi, pour calculer le pgcd des entiers 126 et 54, on se ramène à 54 et 126-54 = 72 ; puis à 54
et 72-54 = 18 ; puis à 18 et 54-18 = 36 ; puis à 18 et 36-18 = 18 : il s’agit donc de 18.
Construire une fonction récursive en appliquant ce principe.
En-tête : bool pgcd(int a,int b)
Précondition : a, b > 0
Postcondition : pgcd est le pgcd de a et b
Exercice 2.5
(Spécification)
Soient n un entier naturel, a[1..n], b[1..n], c[1..n] des tableaux d’entiers. Spécifiez les énoncés suivants :
• b est une copie triée de a
• b contient les plateaux de a par ordre de longueur décroissante. Un plateau est un intervalle où le tableau
contient des valeurs égales.
• b[i] contient le plus grand commun diviseur de a[1..i] ;
Exercice 2.6
(Spécification)
Soient n, m des entiers strictement positifs, a[1..n], b[1..n], c[1..n] des tableaux d’entiers. Spécifiez les énoncés
suivants :
1. c est l’intersection de a et b, considérés comme des multi-ensembles et c est décroissant, m est la taille
de c. Exemple : si a = [1|3|2|2|1|1], b = [1|2|4|2|5|0], c = [2|2|1].
2 2. c contiendra le sous-tableau de a de somme maximale et m sa taille.
3. c contiendra le segment de a de somme maximale et m sa taille.
4. c contiendra le plus grand segment de a strictement croissant et m sa taille
Exercice 2.7
(Temps d’exécution)
Le Prof. Conquère propose de calculer la somme des éléments d’un tableau en utilisant "diviser en deux parties
égales". Il fait donc un appel récursif sur chaque moitié du tableau, puis somme les résultats.
1. Donnez le programme obtenu en suivant son idée.
2. Quel est son temps d’exécution ?
Exercice 2.8
(Temps de calcul)
Le programme ci-dessous est-il correct ? Donnez l’ordre de grandeur et son temps de calcul. Donnez un autre
algorithme plus efficace pour puissance, et l’ordre de grandeur de son temps de calcul.
Exercice 2.9
(Temps de calcul : voyageur de commerce)
On reçoit un tableau D qui donne la distance du trajet pour aller d’un noeud à un autre. On veut construire un
algorithme qui donne le circuit le plus court qui passe une fois par tous les noeuds.
Donnez l’ordre de grandeur du temps de calcul du programme suivant, qui construit les circuits qui commencent
par visited, et garde la distance la plus courte :
Exercice 2.10
(Temps de calcul)
Un plateau d’un tableau est un intervalle d’indices où la valeur du tableau est toujours la même. Le Prof.
Conquère propose de calculer la longueur du plus grand plateau d’un tableau en utilisant "diviser en un élément/le
reste". On risque de couper le bon plateau par cette division. Il propose donc l’algorithme : Tester si toutes les
valeurs du tableau sont les mêmes ; Si oui, le bon plateau est tout le tableau, on renvoie sa longueur u − l + 1 ;
Si non, faire des appels récursifs sur l..u − 1 et l + 1..u et renvoyer la meilleure longueur trouvée.
1. écrivez le programme obtenu en suivant son idée.
2
2. Est-il correct ?
3. Quel est son temps d’exécution ?
4. Donnez l’algorithme le plus rapide (en ordre de grandeur).
Exercice 2.11
(Temps de calcul)
1. Cet algorithme est-il correct ? (Pour répondre à cette question, il faut spécifier le problème, préciser
l’algorithme, puis faire la preuve grâce à l’invariant et au variant)
2. Donnez l’ordre de grandeur de son temps de calcul.
3. Existe-t-il un autren algorithme plus efficace pour ce problème ? Lequel ?
table1=malloc((fin1-deb1+1)*sizeof(int));
for(i=deb1;i<=fin2;i++)
{
if (compt1==deb2) //c'est que tous les éléments du premier tableau ont été utilisés
{
break; //tous les éléments ont donc été classés
}
else if (compt2==(fin2+1)) //c'est que tous les éléments du second tableau ont été
utilisés
{
tableau[i]=table1[compt1-deb1]; //on ajoute les éléments restants du premier
tableau
compt1++;
}
else if (table1[compt1-deb1]<tableau[compt2])
{
tableau[i]=table1[compt1-deb1]; //on ajoute un élément du premier tableau
compt1++;
}
else
{
tableau[i]=tableau[compt2]; //on ajoute un élément du second tableau
compt2++;
}
}
free(table1);
}
if (deb!=fin)
{
2 int milieu=(fin+deb)/2;
tri_fusion_bis(tableau,deb,milieu); -> T(n/2)
tri_fusion_bis(tableau,milieu+1,fin); -> T(n/2)
fusion(tableau,deb,milieu,fin);-> O(n) (A démontrer)
}
}
c. Complexité
T(n) = O(1) ; si n = 1 (Appel à la fonction)
ème
2 . T(n/2) + O(n) = 2. T(n/2) + n ; si n > 1 -> O ( n .log(n) ) (4 ligne du tableau avec c = 2, d =2, b =1, k=1 )
c. Complexité
b.
Principe
Insérer un élément dans la partie déjà triée
Solution
2
void inserer(int element_a_inserer, int tab[], int taille_gauche)
{
int j;
for (j = taille_gauche; j > 0 && tab[j-1] > element_a_inserer; j--)
tab[j] = tab[j-1];
tab[j] = element_a_inserer;
}
c. Complexité
Meilleur cas : Chaque élément est inséré à la fin de la partie triée (tout le tableau est trié). -> O(n)
ème
Pire des cas : Le tableau est trié dans l’ordre inverse. C’est-à-dire que la i itrétion génère (i-1) comparaisons et
2
échanges. -> O(n )
2
Moyenne : -> O(n / 4)
51
3
Les types abstraits (TA)
Sommaire
3.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
3.2 Avantages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
3.3 Syntaxe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
3.4 Donner les propriétés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.5 Complétude . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
3.6 Quelques types abstraits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
3.7 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
53
543.1 Définition BIHD3 - Meth Pgm
3.1 Définition
Définition 3.1
Un type abstrait =
• un type
• des sous-programmes qui exécutent ce type
• des propriétés
Définition 3.2
3 Un type concret = un type défini par une combinaison de types de données (ex : structures en C)
Exemple : TA Liste
Une liste contient une suite finie d’éléments : le premier, le deuxième, . . .
On se donne des fonctions :
3.2 Avantages
• Abstraction : on peut construire un programme utilisant le TA sans connaitre son implémentation
• Modifiabilité : on peut modifier l’implémentation du TA, pourvu que les propriétés restent vraies, SANS
devoir changer le programme utilisateur.
• Encapsulation : les données ne sont changées que par les sous-programmes de l’interface, ce qui garantit
un invariant des données
• Réutilisation : les TA sont employés dans un grand nombre de programmes.
3.3 Syntaxe
Voir [Sch12, slides 75-77]
∀a ∈ A, ∃c ∈ C : R(a, c)
D(c) = ∃a : R(a, c)
Les preuves reposent sur les hypothèses fixées (dans l’exemple : jamais de boucle).
Pour cette partie du type concret, on a par définion la propriété inverse :
∀c ∈ D, ∃a ∈ A : R(a, c)
En résumé : on suppose l’invariant de données avant chaque opération, mais on doit le prouver pour les résultats.
par exemple la liste (5, 4, 3) est représentée par l’ensemble { (1, 5), (2, 4), (3, 3) }
Invariant de données :
3 Type liste = P(N × elem) tel que
@e : (0, e) ∈ l
@i, e1 , e2 : (i, e1 ) ∈ l ∧ (i, e2 ) ∈ l ∧ e1 6= e2
∀(i, e) ∈ l, i > 2 ⇒ ∃e2 : (i − 1, e2 ) ∈ l
Modèles :
Fonctions
Pour les fonctions (sans effet de bord) on emploie la logique du premier ordre.
Exemple 3.4.1
∀x : integer ; A, B : ensemble : dans(x, union(A, B)) ⇔ (dans(x, A) ∨ dans(x, B))
Procédures
Pour les procédures On ajoute la logique dynamique :
[Prog]Formule signifie : Après toute exécution de Prog, la formule est vraie.
Exemple 3.4.2
[ajouter(x,A)]dans(x,A)
3.5 Complétude
Comment être sûr d’avoir donné assez d’axiomes ?
Lorsqu’il n’y a que des fonctions, 2 méthodes :
1 – head(listeVide) = indéfini
2 – head(cons(x,l)) = x
3 – tail(listeVide) = indéfini
4 – tail(cons(x,l)) = l
5 – null(listeVide) = true
6 – null(cons(x,l)) = false
7 – append(listeVide,l2) = l2
8 – append(cons(x,l),l2) = cons(x,append(l,l2))
1 – head(listeVide) = indéfini
2 – tail(listeVide) = indéfini
3 – null(listeVide) = true
4 – head(cons(x,l)) = x
5 – tail(cons(x,l)) = l
6 – null(cons(x,l)) = false
7 – head(append(l1,l2))
8 = head(l1) si null(l1) = false.
9 = head(l2) si null(l1) = true.
10 – tail(append(l1,l2))
11 = append(tail(l1),l2) si null(l1) = false
12 = tail(l2) si null(l1) = true
13 – null(append(l1,l2)) = null(l1) and null(l2)
Fonctions
• pileVide donne une pile vide
3 • empile (ou push ) ajoute un élément en sommet de pile
• dépile (ou pop ) retire le sommet de pile
• sommet (ou top ) renvoie le sommet de pile
Axiomes
• sommet(pileVide) = indéfini.
• sommet(empile(x,l)) = x.
• dépile(pileVide) = indéfini.
• dépile(empile(x,l)) = l.
Implémentation
Implémentation classique des piles
Fonctions
Axiomes
Constructeurs : {ensVide, ajout}.
3.6.3 TA Dictionnaire
Ce TA permet de retrouver la "définition" d’un "mot", aussi appelé "clé".
On l’appelle aussi Map en Java 2.
Fonctions
type dico
function ajout(d : def; m : mot; di : dico) : dico;
function dicoVide : dico;
function def_de(m : mot; di : dico) : def;
3 function défini(m : mot; di : dico)
function apres(di1,di2:dico) :dico;
: boolean ;
{superposition pour recherche dico multiples}
Toutes nos implémentation d’ensembles peuvent être adaptées pour le type dictionnaire.
3.7 Exercices
3.7.1 Exercices du TP4
1 Types Abstraits
Spécification des piles d’entiers par modèle mathématique (de haut niveau)
On représente les piles d’entiers par des tuples (couples, triplets, quadruplets, ...) d’entiers. Si P est
l’ensemble de ces objets, on a donc :
P = {<>} ∪ {< z >: z ∈ Z} ∪ {< z1 , z2 >: z1 , z2 ∈ Z} ∪ {< z1 , z2 , z3 >: z1 , z2 , z3 ∈ Z} ∪ . . .
Une autre manière de décrire un type abstrait consiste à citer les propriétés que les opérations doivent
vérifier, sans donner de représentation/modèle. Par exemple, si on applique l’opération "ajouter 3 au som-
met de la pile" puis l’opération "retirer le sommet de la pile", il faut retrouver la pile de départ.
On note P le type des piles d’entiers. Les opérations sur les piles sont les suivantes.
Exercice 4 (Fractions)
Faire de même pour les fractions. L’implémentation devra permettre d’additionner, soustraire, multiplier
et diviser des fractions, d’obtenir le numérateur et le dénominateur d’une fraction, de transformer un
entier en une fraction, ou un couple d’entiers (a, b) en la fraction a/b. On tentera, dans l’implémentation,
de toujours fournir des représentations où numérateur et dénominateur sont simplifiés.
Sommaire
4.1 Tableau de Booléens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
4.2 Liste avec doublons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
4.3 Liste sans doubles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
4.4 Listes triées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
4.5 Tables de hachage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4.6 Arbres binaires de recherche (ABR) . . . . . . . . . . . . . . . . . . . . . . . 76
4.7 Arbres rouges/noirs (ARN) . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
4.8 B-Arbres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
4.9 Exercices de TP - implémentation des ensembles . . . . . . . . . . . . . . . 98
65
664.1 Tableau de Booléens BIHD3 - Meth Pgm
NB : packed = moins de place en mémoire, chaque élément n’étant plus un mot complet (de 32 ou 64 bits)
Invariant de représentation :
e ∈ A ⇔ C[e] = true
Notes
4 • Pour les boucles for il faut disposer des bornes MIN et MAX.
• le résultat d’une fonction (ens ) doit être un type non structuré en Pascal pur, mais la plupart des Pascal
admettent cette extension.
ajout → cons
ensV ide → listeV ide
4
4.2.2 Fonctions et procédures
dans
L’implémentation de dans se déduit de ses équations ; Deux cas de base et un cas récursif.
union
intersection
f Tf
listeVide O(1)
cons O(1)
append O(length(x))
head O(1)
tail O(1)
f Tf
ensVide
ajout
O(1)
O(1) 4
dans O(length(x))
singleton O(1)
union O(length(x))
intersection O(length(x) ∗ length(y)) = O(n2 )
4.3.2 Fonctions
Il faut garantir que chaque fonction respecte bien le nouvel invariant :
— ensVide : OK.
4 — singleton : OK.
– ajout : teste qu’on ne crée pas de double
Apparemment, le temps augmente, mais pour les listes avec doubles, l n’est PAS bornée !
4.4.2 Fonctions
Il faut garantir que chaque fonction respecte bien le nouvel invariant :
— ensVide : OK.
— singleton : OK. 4
– ajout : insérer à la bonne place (en récursif)
4 f
ensVide
Tf
O(1)
ajout O(length(x))
dans O(length(x))
singleton O(1)
union O(length(x)+length(y)) = O(n)
intersection O(length(x)+length(y)) = O(n)
où elem est grand, mais indice est petit, par exemple 0..M par ex. en prenant mod (M + 1).
Problème : Lorsque deux éléments distincts ont le même indice, il y a COLLISION.
Solutions :
1. 4.5.2 : Représenter l’ensemble des éléments de même indice (p.ex. par une liste triée).
2. 4.5.3 : Représenter la liste dans la table : Adressage ouvert.
3. 4.5.4 : Éviter les collisions en agrandissant la table .
4
4.5.2 Implémentation par un ensemble des indices
type
indice = 0..M;
ens = array [indice] of ens2;
{ens2 = par ex. liste chaînée }
Temps d’exécution
Place en mémoire
O(M ) = O(|x|) si M est bien choisi
Conclusion
Améliore le temps d’exécution de dans, insérer, supprimer.
Dégrade le temps d’exécution de ensVide, ajout.
Pourtant souvent employé car ensVide et ajout sont rares.
Pour insérer, on parcourt la séquence d’indices jusqu’à trouver une case vide.
Pour rechercher, on parcourt la liste d’indices jusqu’à trouver la clé ou tomber sur une case vide.
Temps d’exécution
Dépend du risque de collision en chaîne. Si négligeable, même temps moyen.
Place en mémoire
L’économie ne change pas l’ordre de grandeur : O(M ) = O(|x|)
[1]. "bonne pratique" : toujours définir la taille des tableau en puissance de 2 (-1)
tree tree
4 e
g d
n\oe ud = record
e : elem;
g : arbre;
d : arbre;
end ;
{ inv : x.g < x , x.d < x pour "<" bien fondé càd pas de cycles }
abr
5
<5 >5
3 7
6 8
Recherche
Insertion
Avec une fonction
Supprimer
Cas 1. Si le nœud à supprimer n’a pas de fils gauche, on peut l’enlever
3 3
4 1 5 1 5
2 4 6 4 6
Cas 2. Sinon, il nous manque une valeur pour séparer les deux sous-arbres : on remonte le max du fils gauche. On
pourrait mettre un sous-arbre sous l’autre, mais ça déséquilibrerait l’arbre
3 2
1 5 1 5
2 4 6 4 6
supprimerRacine
-> trois^.g <> NIL
-> trois^.d = un
-> supprimerMax(un, trois^.e)
supprimerMax
-> un^.d <> NIL
-> un^.d = deux
-> supprimerMax(deux, trois^.e)
supprimerMax
-> deux^.d = NIL
-> trois^.e = m = deux^.e = 2 { valeur copiée par Ref }
-> p = deux
-> deux = deux^.g = NIL { modification du pointeur }
-> dispose p { ancien n\oe ud 2 libéré }
4.7.1 Définition
Un arbre rouge et noir est un arbre binaire de recherche où on ajoute une "couleur" binaire à chaque nœud, et
surtout un invariant de représentation pour que la hauteur soit logarithmique :
Les nœuds rouges donnent un peu de souplesse à la contrainte d’équilibre. Si on oublie ces nœuds rouges, on
obtient un arbre binaire parfaitement équilibré.
4 La racine est soit noire, soit rouge. Mais on peut supposer que cette racine est toujours noire car le changement
de couleur ne change pas les propriétés.
En contrôlant cette information de couleur dans chaque nœud, on garantit qu’aucun chemin ne peut être deux fois
plus long que n’importe quel autre chemin, de sorte que l’arbre reste relativement équilibré. Même si l’équilibrage
n’est pas parfait, on montre que toutes les opérations de recherche, d’insertion et de suppression sont en O(log(n)).
17
7 23
3 11 21 31
18 22 29
19
Certaines sources nomment "nœuds externes" les pointeurs NULL vers des sous arbres vides. Les nœuds internes
sont donc ceux qui possèdent une valeur. Un ARN ayant n nœuds internes possède n + 1 nœuds externes. La suite
ne fait mention que des nœuds internes en omettant le qualificatif.
Déclaration Pascal
arn = ^node;
node = record
e : elem ;
g : arn ;
d : arn ;
red : boolean
end ;
a <> nil ⇒ { ∀e1 ∈ inf o(ab.g) ⇒ e1 < ab.e } ∧ { ∀e2 ∈ inf o(ab.d) ⇒ e2 > ab.e }
hn(ab.g) = hn(ab.d)
h(a) ≤ 2 log(n + 1)
La hauteur noire
Pour un ARN, on appelle hauteur noire le nombre hn(x) de nœuds noirs le long d’une branche de la racine à une
feuille.
La hauteur d’un ARN est donc au moins celle des nœuds noirs (sans nœuds rouges), soit de 2 fois la hauteur noire
si on compte les rouges et les noirs, +1 si la racine est rouge.
hn ≤ h ≤ 2hn + 1
h
hn ≥
2
2hn − 1 ≤ n ≤ 2h − 1
La première partie est démontrée par récurrence (voir [Cor04, lemme 13.1]) :
4 2hn − 1 ≤ n
• Si h = 0, alors a est obligatoirement une feuille nulle. Les sous-arbres enracinés en a contiennent donc bien
2hn − 1 = 20 − 1 = 0 nœuds internes ;
• Si h > 0, alors chacun des fils d’un nœud interne x a une hauteur noire égale soit à hn si x est rouge, soit
à hn − 1 si x est noir.
Comme la hauteur des fils de x est inférieure à celle de x, on peut donc appliquer l’hypothèse d’induction
au sous arbre de x qui contient au moins (2hn−1 − 1) + (2hn−1 − 1) + 1 = 2hn − 1 nœuds internes.
La deuxième partie . . . ? ? ?
n ≤ 2h − 1
[TODO]
Ordre de grandeur
On démontre que
h = O(log n)
Construire
Lors de cons d’un nœud x, il va recevoir la couleur (red = 0 ∨ 1), la précondition doit être plus forte :
• hn(xb.g) = hn(xb.d)
• hn(xb.red) = 0 ∨ { hn(xb.red) = 1 ∧ hn(xb.gb.red) = 0 ∧ hn(xb.db.red) = 0 }
• ∀y ∈ inf o(xb.g) ⇒ y < e
• ∀z ∈ inf o(xb.d) ⇒ z > e
Rechercher
Pas de changement, la recherche s’exécute en temps logarithmique.
4.7.5 Rotations
Les rotations sont des modifications locales d’un arbre binaire. Elles consistent à échanger un nœud avec l’un de
ses fils. Dans la rotation droite, un nœud devient le fils droit du nœud qui était son fils gauche. Dans la rotation
gauche, un nœud devient le fils gauche du nœud qui était son fils droit. Les rotations gauche et droite sont inverses
l’une de l’autre. Elle sont illustrées à la figure ci-dessous (les triangles désignent des sous-arbres).
x x
Q P
P C A Q
4 A B B C
Rotation gauche
Une rotation gauche prend un temps constant.
Elle raccourcit les branches de C et rallonge celles de A.
L’arbre reste trié mais pas toujours rouge-noir !
x x x
2 3
P Q Q
y y y
A Q A P A P
1
B C B C B C
x x x
Q Q Q
6
y y y
A P C P C P C
4
5
B A B A B
4.7.6 Insertion
1. Insertion d’une feuille rouge
2. Si son père est rouge, nous avons ce qui est appelé une bulle : il faut rétablir l’invariant
La procédure inserer débute la recherche à la racine a. Elle ne traite que a et ses fils examinés à partir de a. Si
l’élément n’a pu y être inséré à un emplacement NIL, elle passe ensuite le relais à la procédure insererRec avec
une chaîne de père (root) + fils + petit-fils.
ab.g ab.d
La procédure insererec prend en paramètre une filiation de 3 nœuds a, b, c tels que ab.bb.c (avec a, b non NIL).
Si c n’est pas NIL, elle descend récursivement dans la branche correspondante.
Sinon, elle insère une feuille rouge en c et doit ensuite rétablir l’invariant.
La remontée dans la pile des appels garanti le rétablissement de l’invariant vers le haut de l’ARN.
then rotationGauche(b);
{ cas 3 : rotation coté oncle sur grand-père }
rotationDroite(a);
end
end
else begin {cas droite}
y := a^.g; { oncle à gauche }
if y^.red
then begin { oncle rouge = cas 1 : permutation de couleur }
b^.rouge := false ;
y^.rouge := false ;
a^.rouge := true ;
end
else
begin { oncle noir }
if c = b^.g { neveu même coté que oncle = cas 2 : rotation opposée sur père }
then rotationDroitee(b);
{ cas 3 : rotation coté oncle sur grand-père }
rotationGauche(a) ;
end
end
end 4
end;
b.red
∧ c.red
b = ab.g b = ab.d
y := ab.d y := ab.g
CAS 3 CAS 3
rotation rotation
gauche(a) droite(a)
4.7.7 Supprimer
Comme pour l’insertion d’une valeur, la suppression d’une valeur dans un ARN commence par rechercher le nœud
à supprimer comme dans un arbre binaire de recherche (voir page 78).
Cependant, les propriétés des ARN doivent être respectées
• Si le nœud supprimé est rouge, la hauteur noire n’est pas modifiée et la propriété (1) reste vérifiée.
• Si le nœud supprimé est noir, alors sa suppression va diminuer la hauteur noire de certains chemins dans
l’arbre. La propriété est retrouvée en rectifiant les couleurs.
Posons s le nœud dont la valeur est à supprimer. Plusieurs cas sont à traiter :
Rétablir l’invariant
On considère que le nœud qui remplace le nœud supprimé porte une couleur noire en plus, cela signifie qu’il devient
double-noir s’il était déjà noir. La hauteur noire reste ainsi vérifiée mais il faut supprimer cette double propriété
sur un noeud.
Soit x le nœud doublement noir. Il vient les cas suivants
x x
g d g d
p p f
x f x f p d 7
g d g d x g
p p f
4
f x f x g p 7
g d g d d x
p p
x f x f 3
g d g d
p p
x f x f 7
g d g d
p p f
x f x f p d 3
g d g d x g
4
p p f
x f x f p d 7
g d g d x g
p p f
f x f x g p 3
g d g d d x
p p f
f x f x g p 7
g d g d d x
p p p
x f x f x g
g d g d l f
l r l r r d
4
g g
p f p f 3
x l d x l d
4.7.8 Web
Une animation sur les ARN : http ://www.cse.yorku.ca/-aaw/Sotirios/RedBlackTree.html
4.8 B-Arbres
4.8.1 Définition
Un arbre B (ou B-Tree) est un arbre binaire de recherche équilibré dans lequel tout nœud (excepté la racine) a
entre m/2 et m fils (avec m > 1 un entier fixé, appelé l’ordre de l’arbre).
Cette structure de données est très intéressante lorsqu’une grande partie de l’arbre est stocké en mémoire lente
(mémoire auxiliaire) car la hauteur de l’arbre (et donc le nombre d’accès à la mémoire) peut être assez réduite.
Plus m est élevée, plus la hauteur de l’arbre est réduite.
Les B-arbres utilisent la même idée que les arbres rouges-noirs. Ils regroupent un paquet de nœuds bicolores dans
un nœud de B-arbres, typiquement de la taille d’une page mémoire (ex : un bloc de 8ko).
1. Si on regroupe un nœud noir avec son éventuel fils rouge, et qu’on met la racine à noir, on obtient un arbre
où toutes les branches ont la même longueur mais dont le nombre de fils varie.
4 2. On peut en regrouper plusieurs niveaux – pour obtenir un nœud de B-arbre qui a la taille d’un bloc disque
3. Dans une feuille, tous les fils sont vides, on peut donc supprimer les pointeurs pour mettre plus de valeurs
(par ex. dans un tableau).
Exemple 4.8.1
Un B-arbre d’ordre 2
– chaque nœud (sauf la racine) contient k clés avec 2 ≤ k ≤ 4
– la racine contient k clés avec 1 ≤ k ≤ 4
On choisit
const
{ pour un n\oe ud interne non feuille : }
Minelems = ... ; { > 0: nombre min de clefs }
Maxelems = ... ; { > 2 * Minelems : nombre max de clefs }
{ pour une feuille : }
Mindonnees = ... ; { > 0: nombre min d ’ éléments }
Maxdonnees = ... ; { > 2 * Mindonnees : nombre max d’éléments }
Déclaration
Invariant de données
Avec uti le nombre d’élément utilisés
1. 0 ≤ uti
2. 0 < uti si la racine n’est pas une feuille 4
3. uti ≤ M ax
4. M in ≤ uti pour tout nœud sauf la racine
5. ∀ i ∈ [1..uti] : e ∈ Inf o(f ils[i − 1]) ∧ e < elems[i] (les valeurs inférieures à gauche)
6. ∀ i ∈ [1..uti] : e ∈ Inf o(f ils[i]) ∧ e > elems[i] (les valeurs supérieures à droite)
7. tous les fils ont la même hauteur.
8. la partie utilisée de donnees et elems est triée
Hauteur
L’invariant nous donne une hauteur logarithmique : la racine a au min une clé et deux fils, ses fils ont au minimum
t fils, etc.
Xh
n ≥ 1 + (t − 1) ∗ 2ti−1
i=1
n+1
h ≤ logt
2
Ensemble vide
L’ensemble vide est représenté par une feuille avec uti=0 .
Diviser
Un nœud plein peut être divisé en deux nœuds, et la clé centrale est remontée dans le père pour séparer ces deux
4 nœuds.
1. Technique préventive : On met dans la précondition que le père n’est pas plein.
2. Technique corrective : On divise le père après si nécessaire par un appel récursif.
Insérer
On voudrait insérer une nouvelle clef dans la feuille où on devrait la trouver, mais ceci peut faire déborder la feuille
si elle était pleine.
Exemple 4.8.2
Insertion d’un enregistrement de clé 25 [2]
avant
après
Cas récursifs
Fusion
Symétriquement, la fusion de deux blocs (lors d’une suppression) supprime une clef du père qui pourrait ainsi
passer en dessous du minimum.
Supprimer
Technique préventive : on ne descend dans un fils que s’il est au-dessus du min.
Pour garantir cela, soit on prend des fils d’un frère (s’il n’est pas au min), soit on fusionne avec un frère.
De même lors de la suppression du minimum/maximum ci-dessous.
Si la valeur à supprimer est dans un nœud intérieur, il faut la remplacer soit :
Exemple 4.8.3
Suppression d’un enregistrement de clé 7 [3]
4 avant
après
Animation web
http ://deptinfo.cnam.fr/Enseignement/CycleA/SD/demonstration/B-Arbre.html
http ://www.youtube.com/watch ?v=coRJrcIYbF4
Exercice 1 (Définition)
Donner une “définition récursive” d’un type concret pour un arbre binaire strictement trié (d’éléments de
type t).
p :
?
2
@
@
R
@
3 5
@
@
R
@
4 6 8
@
@
R
@
7 4
1. Quelle sera la séquence d’entiers rencontrée lors d’un parcours de l’arbre p dans l’ordre préfixé, dans
l’ordre infixé, dans l’ordre postfixé ?
Implémenter les fonctions de parcours d’un arbre suivantes.
Exercice 3 ()
Exercice 4 ()
2 Arbres rouge-noir
Premier aperçu et premières définitions Un arbre rouge-noir est une arbre binaire dont chaque noeud
est coloré soit en noir, soit en rouge et vérifiant les propriétés suivantes
On appelle profondeur noire le nombre de noeuds noirs visités pour aller de la racine d’un arbre rouge-
noir jusqu’à une de ces feuilles. (D’après la condition 3, le nombre obtenu est le même, quelle que soit la
feuille choisie.)
Exercice 5 (Réflexion)
1. Quel est l’intérêt de ces arbres par rapport aux arbres binaires (triés) ?
2. Les arbres rouges-noirs sont plus complexes que les "simples" arbres strictement triés : quel est le
gain apporté pour cette complexité ajoutée ?
3. Combien y-a-t’il d’arbres rouges-noirs différents (sans tenir compte des valeurs des noeuds) de hau-
teur noire 0 ?
4. Combien y-a-t’il d’arbres rouges-noirs différents (sans tenir compte des valeurs des noeuds) de hau-
teur noire 1 ?
5. Quelle est la forme des arbres rouges-noirs constitués uniquement de noeuds noirs ?
6. Quelle est la forme des arbres rouges-noirs constitués uniquement de noeuds rouges ?
On cherche à faire de même pour les arbres rouges-noirs. On décide de construire une définition in-
ductive divisée en trois cas : un cas de base, l’arbre vide ; et deux cas de récurrence, qui concernent d’une
part les arbres dont la racine est noire et d’autre part les arbres dont la racine est rouge. Quelles sont les
conditions à ajouter pour que cette définition inductive soit correcte ?
Exercice 7 (Réflexion)
1. Combien de valeurs, au minimum, un arbre rouge-noir de hauteur noire hn peut-il contenir ?
2. Quelle relation existe entre la hauteur (normale) h et la hauteur noire (hn) d’un arbre rouge-noir ?
3. Si on place n valeurs dans un arbre rouge-noir, quelle sera, au pire, sa hauteur noire ? Et sa hauteur ?
Rotation gauche :
Le noeud père et le noeud fils peuvent être de n’importe quelle couleur avant la rotation. Après la
rotation, le noeud père doit avoir la même couleur que le noeud père avant l’opération ; de même pour le
noeud fils.
Cette opération sera utilisée pour transformer un arbre binaire trié dont les noeuds sont colorés en un
arbre rouge-noir. L’arbre de départ n’est pas forcément un arbre rouge-noir, mais on sait qu’il est "bien
défini" (au sens où, si on suit ses branches, on ne tombe pas sur des valeurs ou des pointeurs indéfinis).
On utilisera ce terme ("bien défini") comme précondition dans la spécification de ces opérations.
Rotation droite :
4
Il vous est demandé de spécifier et d’implémenter les fonctions suivantes.
Exercice 01
En-tête : procedure rotationgauche(t :arbreRN)
Précondition : t est "bien défini" et sa racine a un fils droit
Postcondition : Une rotation gauche a été effectuée sur t
Exercice 02
En-tête : procedure rotationdroite(t :arbreRN)
Précondition : t est "bien défini" et sa racine a un fils gauche
Postcondition : Une rotation droite a été effectuée sur t
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <math.h>
4 #include <time.h>
5 #include <stdbool.h>
6
7 typedef struct Arbre{
8 int val;
9 struct Arbre* SAG;
10 struct Arbre* SAD;
11 }Arbre;
12
13 struct List{
14 int val;
15 struct List* next;
16 };
4 17
18 typedef struct List* List;
19
20 void insert(struct Arbre** tree, struct Arbre * item);
21 void aplatir(struct Arbre * tree) ;
22 void pre_aplatir(struct Arbre * tree);
23 void post_aplatir(struct Arbre * tree);
24 int size_Arbre(struct Arbre* arbre);
25 int presense_element(struct Arbre* arbre, int key);
26 int number_occurence(struct Arbre* arbre, int key);
27 int hauteur(struct Arbre* arbre);
28 int maxValue(struct Arbre* arbre);
29 int nfeuilles(struct Arbre* arbre);
30 void detruireabre(struct Arbre ** arbre);
31
32 int main(void) {
33 struct Arbre * curr, * root;
34 int i;
35
36 root = NULL;
37 srand((unsigned)time(NULL));
38 for(i=1;i<=20;i++) {
39 curr = malloc(sizeof(struct Arbre));
40 curr->SAG = curr->SAD = NULL;
41 int x = rand()%20 + 1;
42 curr->val = x;
43 insert(&root, curr);
44 }
45 // detruireabre(&root);
46 /*printf("Element exist binary search tree: %d\n",presense_element(root,3));
47 printf("Number of occurrences in binary search tree: %d\n",number_occurence(root,3));
48 printf("The height of the binary search tree: %d\n",hauteur(root));
49 printf("The maximum value of the binary search tree: %d\n",maxValue(root));
50 printf("The number of leaf nodes of the binary search tree: %d\n",nfeuilles(root));
51 printf("size of binary search tree: %d\n",size_Arbre(root));*/
52
53 printf("In order display of binary tree\n");
54 aplatir(root);
55 printf("pre-order order display of binary tree\n");
56 pre_aplatir(root);
57 printf("post- order display of binary tree\n");
58 post_aplatir(root);
59
60 return EXIT_SUCCESS;
61 }
62
63 void insert(struct Arbre** tree,struct Arbre * item) {
64 if(!(*tree)) {
65 *tree = item;
66 return;
67 }
68 if(item->val<=(*tree)->val)
69 insert(&(*tree)->SAG, item);
70 else if(item->val>(*tree)->val)
71 insert(&(*tree)->SAD, item);
72 }
73
74 void display(List list){
75 while(list!= NULL){
76 printf("%d\n",list->val);
77 list = list->next;
78 }
79 }
80
81 /*
82 * Précondition : a est un arbre
83 * Postcondition : aplatir est une liste chainee reprenant les valeurs de a reprises dans un ←-
ordre indexé
84 */
85 void aplatir(struct Arbre * tree) {
86 if(tree== NULL)
87 return;
88 aplatir(tree->SAG);
printf("%d\n",tree->val);
89
90
91 }
aplatir(tree->SAD); 4
92
93 /*
94 * Précondition : a est un arbre
95 * Postcondition : pre-aplatir est une liste chainée reprenant les valeurs de a reprises dans ←-
un ordre préfixé
96 */
97 void pre_aplatir(struct Arbre * tree) {
98 if(tree== NULL)
99 return;
100 printf("%d\n",tree->val);
101 pre_aplatir(tree->SAG);
102 pre_aplatir(tree->SAD);
103 }
104
105 /*
106 * Précondition : a est un arbre
107 * Postcondition : post-aplatir est une liste cha”née reprenant les valeurs de a reprises dans←-
un ordre postfixé
108 */
109 void post_aplatir(struct Arbre * tree) {
110 if(tree== NULL)
111 return;
112 post_aplatir(tree->SAG);
113 post_aplatir(tree->SAD);
114 printf("%d\n",tree->val);
115 }
116
117 /*
118 * Précondition : a est un arbre
119 * Postcondition : size-arbre est le nombre de noeuds
120 */
121 int size_Arbre(struct Arbre* arbre) {
122 if (arbre==NULL) {
123 return(0);
124 } else {
125 return (size_Arbre(arbre->SAG) + 1 + size_Arbre(arbre->SAD));
126 }
127 }
128
129 /*
130 * Précondition : a est un arbre
131 * Postcondition : nombre d'occurrences de key dans a
132 */
133 int number_occurence(struct Arbre* arbre, int key) {
134 if (arbre == NULL) {
135 return (0);
136 } else {
137 if(arbre->val == key)
138 return 1 + presense_element(arbre->SAG,key) + presense_element(arbre->SAD,key);
139 if(arbre->val < key)
140 return presense_element(arbre->SAG,key);
141 else
142 return presense_element(arbre->SAD,key);
143 }
144
145 }
146
147
148 /*
149 * Précondition : a est un arbre
150 * Postcondition : vrai/faux selon que x apparait dans a
151 */
152 int presense_element(struct Arbre* arbre, int key){
153 if(arbre == NULL)
154 return (false);
155 if(arbre->val == key)
156 return (true);
157 else if(arbre->val < key)
158 return presense_element(arbre->SAG,key);
159 else
return presense_element(arbre->SAD,key);
4 160
161
162
}
163 /*
164 * Précondition : a et b sont des arbres
165 * Postcondition : vrai/faux selon que a et b sont identiques
166 */
167 int egalArbre(struct Arbre* a, struct Arbre* b) {
168 // 1. both empty -> true
169 if (a==NULL && b==NULL) return(true);
170 // 2. both non-empty -> compare them
171 else if (a!=NULL && b!=NULL) {
172 return(
173 a->val == b->val &&
174 egalArbre(a->SAG, b->SAG) &&
175 egalArbre(a->SAD, b->SAD)
176 );
177 }
178 // 3. one empty, one not -> false
179 else return(false);
180 }
181
182 /*
183 * Précondition : a est un arbres
184 * Postcondition : hauteur est la hauteur de a
185 */
186 int hauteur(struct Arbre* arbre) {
187 if (arbre==NULL) {
188 return(0);
189 }
190 else {
191 // compute the depth of each subtree
192 int lHauteur = hauteur(arbre->SAG);
193 int rHauteur = hauteur(arbre->SAD);
194 // use the larger one
195 if (lHauteur > rHauteur) return(lHauteur+1);
196 else return(rHauteur+1);
197 }
198 }
199
200 /*
201 * Précondition : a est un arbre d'entriers strictement positifs
202 * Postcondition : maxValue est le maximum des valeurs de l'arbre a ou 0 si l'arbre est vide
203 */
204 int maxValue(struct Arbre* arbre) {
205 struct Arbre* current = arbre;
206 // loop down to find the leftmost leaf
207 while (current->SAD != NULL) {
208 current = current->SAD;
209 }
210 return(current->val);
211 }
212
213 /*
Sommaire
5.1 Spécification par modèle . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
5.2 Implémentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
5.3 Le tri par Tas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
107
1085.1 Spécification par modèle BIHD3 - Meth Pgm
insérer
supprimerMax
NB : avec une procédure (et non une fonction) car l’ensemble passé en paramètre est modifié (suppression et
renvoi du maximum m)
5
5.2 Implémentation
5.2.1 Possibilités
5.2.2 Tas
Invariant de l’APO : un père est toujours plus grand que ses fils (racine = le max).
Invariant
[1]. Tas = heap, mais différent que celui destiné à la gestion de la mémoire.
Représentation
[1]
11
[2] [3]
10 7
[8] [9]
6 4
i
5
2i 2i + 1
Cette numérotation autorise l’utilisation des index dans un tableau (structure plus rapide). par rapport aux B-arbres
et aux ARN, un tableau ne conserve que les valeurs. Aucun espace n’est perdu pour les pointeurs.
i 1 2 3 4 5 6 7 8 9
A[i] 11 10 7 8 3 5 1 6 4
Le ’bubbleUp’ est utilisé à l’insertion pour faire remonter une valeur à sa place
bubbleDown, utilisé en suppression, fait descendre un élément responsable de la violation de la propriété APO
jusqu’à ce qu’une feuille soit atteinte.
Insertion
Extraction du maximum
Exécuté en O(log n)
Tri en tas
Tri par tas dans un tableau.
Conception d’algorithmes
113
Question de départ
Comment passer d’un problème à un programme ?
(a) Preuve ?
(b) Ne fonctionne pas → 7→ point 3.
(c) Si preuve bonne + pas de recalculs → 3.
(d) Si preuve bonne mais on a des recalculs : programmation dynamique avec mémoïsation.
3. Si les autres ne fonctionnent pas, utiliser la méthode générer et tester.
Sommaire
6.1 Idée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
6.2 Exemples : quelques tris . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
6.3 Exemples : multiplications . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
117
1186.1 Idée BIHD3 - Meth Pgm
6.1 Idée
Utiliser la récursion (appel un sur invariant décroissant), ce qui implique de diviser un problème en sous-problèmes
similaires mais plus simples.
6.2.1 La Spécification
En entrée : l : liste
En sortie : e : liste
Postcondition : e est triée et est une permutation [1] de l.
Si on veut qu’une solution existe toujours, on suppose que l’ordre de tri est réflexif ce qui permet les doublons.
On déduit combiner = insérer un élément dans une liste triée. (soit en O(n))
Le temps d’exécution sera donc de l’ordre de O(n2 ).
1 insert(e,constr(t,r)) = {e <= t} ? {constr(e, constr(t,r))} : {cons(t, insert(e, r))} ; 6
Algo complet [Des11, § 6.6.4]
TYPE
indice = 0..MAX;
tabint = array[indice] of Integer ;
BEGIN
FOR i:=2 TO n DO inserer(i)
END;
Case de base |l| ≤ 1, dans lequel il faut prendre 0 mais aussi 1 comme taille des plus petites listes triées.
On déduit combiner = fusionner deux listes triées soit exécuté en O(n).
Le temps d’exécution T (n) = 2T (n/2) + n sera donc de l’ordre de O(n log(n)).
combiner = cons(e,l)
Le nombre de feuilles est au moins n! soit le nombre de permutations possibles de la liste d’entrée.
Le temps d’exécution au pire (en ne comptant que les comparaisons) est la hauteur de l’arbre d’exécutions.
Un arbre binaire de hauteur h a au plus 2h feuilles.
n! ≤ 2h ⇔ log2 (n!) ≤ h
or log(n!) = O(n log n) par l’approximation de Stirling.
⇒ T ≥ O(n log n)
⇒ O(n log n) est le meilleur ordre de grandeur possible pour un tri par comparaisons.
Code C
Complexité
T (1) + T (n/2) : k = 0, c = 1, d = 2, dk = 1 = c ⇒ O(nk log n)
O(log n)
Sommaire
7.1 Memoïsation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
7.2 La récursivité ascendante . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
7.3 La programmation dynamique . . . . . . . . . . . . . . . . . . . . . . . . . . 130
7.4 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
123
1247.1 Memoïsation BIHD3 - Meth Pgm
7.1 Memoïsation
7.1.1 Principe
On remarque que l’arbre des appels d’une fonction récursive provoque de nombreux calculs redondants
Pour éviter les re-calculs, on met les valeurs calculées dans un tableau.
En plus de la fonction de départ f , on introduit la fonction fmemo et on remplace tous les appels de f par des
appels de fmemo .
Celle-ci vérifie d’abord si le résultat ne se trouve pas dans son tableau ; sinon elle appelle f et met le résultat dans
son tableau.
Exemple 7.1.1
7 Dans la fonction Fib, les appels récursifs se font à travers Fibmemo qui conserve les données déjà traitées
dans l’arbre des appels.
Inconvénient
Il faut limiter le nombre de valeurs possibles des arguments pour avoir un tableau de taille raisonnable.
Inconvénients
• La forme du programme est complètement changée.
• Attention aux calculs inutiles !
Les sous-problèmes utiles sont ceux qui ont un chemin d’appels depuis le problème de départ.
7.2.5 Combinaisons
Le nombre de combinaisons (ensembles) de m éléments choisis dans un ensemble de taille n (avec m ≤ n) est
noté ici Cnm . Certains le notent m
n ou inversent m et n.
Précondition : 0 ≤ m ≤ n
Postcondition : Cnm = ] { s ⊆ { 1..n } | ] = m }
Diviser pour régner nous permet de trouver une définition récursive :
1. Cnm = 0 si m > n ou m < 0 car il est impossible de prendre un nombre négatif d’objets ainsi que d’en
prendre plus de n.
2. Cn0 = 1 car seul l’ensemble vide ne contient aucun élément. Cn
3. Cnn = 1 car seul l’ensemble plein contient tous les objets.
m−1
4. Cnm = Cn−1m
+ Cn−1
car, si on ajoute un élément supplémentaire 0 n0 dans un ensemble de n − 1 éléments,
m
(a) Toutes les anciennes combinaisons Cn−1 restent valables (ce sont les cas où 0 n0 n’est pas pris).
m−1
(b) Les combinaisons qui contiennent 0 n0 sont au nombre de Cn−1 (car on prend n − 1 anciens).
C62 = 15
7
C52 = 10 C51 =5
C11 = 1 C10 = 1
C62 = 15
C52 = 10 C51 = 5
C11 = 1 C10 = 1
C62 = 15
7
C52 = 10 C51 = 5
C11 = 1 C10 = 1
m n−m
Cn = Cn
Remarques
3. La place mémoire est linéaire en O(m) (en mémoisation simple, sans réduction elle est de O(n2 )).
4. Ceci est juste une illustration de la programmation dynamique ! On peut faire plus rapide (linéaire) et plus
7
économe en mémoire (constante) en se ramenant à la relation factorielle :
m n m!
Cn = =
m n!(m − n!)
On l’applique surtout sur des problèmes d’optimisation : trouver une solution faisable de coût minimal (ou de
gain maximal).
2. Récursivité ascendante
(d) On minimise l’emploi de mémoire en supprimant la mémoire qui ne sera plus utilisée, autrement dit
on ne garde que les départs d’une flèche qui franchit la frontière de calcul.
(e) On reconstruit la solution : chaque choix est mémorisé dans un tableau. A la fin, on reconstruit une
solution optimale grâce à ces choix.
Données
• Charge utile (ou capacité) du sac : C ∈ N(nombre naturel)
• Il y a n articles dans le magasin. Pour chaque article du magasin i ∈ 1..n on a :
— sa valeur vi ∈ N (nombre naturel)
— son poids ti ∈ N+ (nombre naturel positif, sinon une infinité d’articles de poids nul peut être emporté)
Résultat
Donner un tableau qi ∈ N (quantités emportées) qui maximise la valeur V du contenu :
n
X
V = qi ∗ vi
i=1
Formalisation récursive
Quelles sont les variables qui peuvent être rendues plus petites ?
– un plus petit sac à dos
– un magasin avec moins d’articles
Les données immutables sont placées en variables globales : le poids et la valeur 7
On pose gain(S, c) le gain maximum pour
– un ensemble S ⊆ { 1, . . . , n }
– une capacité c ∈ [0 . . . C]
Ces ensembles sont biens fondés, donc OK
Cas de base
— Si le sac n’a pas de capacité, on ne peut rien y mettre : gain(S, 0) = 0
— Si le magasin est vide, on ne peut rien emporter : gain(ø, c) = 0
c 1 2 3 ... C
n g(n, 1) g(n, 2) g(n, 3) ... g(n, C)
n−1 g(n − 1, 1) g(n − 1, 2) g(n − 1, 3) ... g(n − 1, C)
..
.
3 g(3, 1) g(3, 2) g(3, 3) ... g(3, C)
2 g(2, 1) g(2, 2) g(2, 3) ... g(2, C)
1 g(1, 1) g(1, 2) g(1, 3) ... g(1, C)
Les couleurs montrent les dépendances de calcul, les valeurs vertes permettent de calculer les valeurs jaunes.
Conséquences
L’implémentation récursive implique des recalculs si il y a deux façons différentes de diviser la même quantité.
Invariants
• boucle extérieure (lignes) for k:= 0 to n do ...
0≤k≤n
gain j pour le k courant : ∀j ⇒ 0 ≤ j ≤ C : gain[j] = g(k, j)
Améliorations
Si il y a un article i et un article j tels que i 6= j et
∀q : q ∗ ti ≤ tj ∧ q ∗ vi ≥ vj
Alors j ne sera jamais employé (car j est moins cher et plus lourd que i).
En généralisant cette constatation, si
q ∗ t ≤ q ∗ t (plus léger)
i i j j
∃ qi , qj :
q ∗ v ≥ q ∗ v (plus cher)
i i j j
M1 × M2 × M3 × · · · × Mn | Mi : pi−1 × pi
La multiplication matricielle n’est pas commutative, mais elle est associative : on peut choisir de mettre les
parenthèses comme on veut !
La question ici est de minimiser ce nombre de multiplications.
Exemple 7.3.1
Les matrices à multiplier, dans l’ordre :
A : 10 × 1
B : 1 × 10
C : 10 × 10
D : 10 × 1
E : 1 × 10
7 AB = 10 × 1 × 10 = 100
.C = 10 × 10 × 10 = 1000
.D = 10 × 10 × 1 = 100
.E = 10 × 1 × 10 = 100
X
= 1300
Spécification
Posons m(i, j) le nombre minimum de multiplications nécessaires pour calculer Mi Mi+1 . . . Mj
Notre objectif de départ s’écrit alors m(1, n).
La mesure bien fondée est j − i.
Cas de base : m(i, i) = 0.
Cas récursif : pour j > i on a :
m(i, j) = min { m(i, k) + m(k + 1, j) + pi−1 ∗ pk ∗ pj }
i≤k<j
Implémentation
Initialiser en variable globale un tableau des dimensions pour la suite de matrices reçues en entrées M1 (i, j) . . . Mn (i, j)
p[0] := M[0].i;
for k := 1 to n do p[k] := M[k].j ;
La méthode d’appel utilise un tableau de dimensions n × n indicé [i, j] où seule la partie diagonale supérieure
est utilisée (les cas j ≥ i).
Son variant v = i − j est le nombre de multiplications de matrices.
La méthode best utilise toutes les valeurs calculées précédemment dans le tableau. Il n’est pas possible de
diminuer le nombre de valeurs en mémoire. Elle s’exécute en O(n)
Soit m[i,j] pour notre exemple. Les couples de même couleur représentent les comparaisons succesives de m[i,k]
+ m[k+1,j] lors du dernier appel à best
i↓ A B C D E
j: 1 2 3 4 5
A 1 0 100 200 120 220
B 2 - 0 100 110 120
C 3 - - 0 100 200
D 4 - - - 0 100
E 5 - - - - 0
Temps d’exécution
La double boucle for du programme d’appel exécute n ∗ n itérations d’appel à best (en O(n)).
Soit un temps d’exécution en O(n3 ) et un espace mémoire en O(n2 )
Diviser pour régner, pour énumérer tous les parenthèsages possibles aurait demandé un temps exponentiel de
O(2n ) (le nombre de partitions de n) .
7.4 Exercices
7.4.1 Questions du TP 5
1 Programmation dynamique
Exercice 1 (Récompense pour une reine (Examen juin 2008) )
Vous avez une reine que vous devez déplacer sur un échiquier nxn depuis le coin inférieur gauche de
coordonnées (1,1) jusqu’au coin supérieur droit (n,n). Vous pouvez la déplacer à droite, ou vers le haut,
ou en diagonale vers la droite et vers le haut, d’autant de cases que vous le voulez.
Vous ne pouvez pas ni descendre ni aller à gauche. Aprés chaque déplacement, vous recevez le nombre
entier positif inscrit dans la case d’arrivée.
– On demande de donner le nombre de trajets possibles
– On demande de maximiser la somme des points récoltés et de donner le trajet correspondant.
Il faut bien sûr généraliser ce problème avant de pouvoir le résoudre par récurrence, puis utiliser une
approche ascendante. On peut faire cela en calculant des valeurs T ∗ (i, S) où 0 ≤ i ≤ n et S est un sous-
ensemble de {1, . . . , n} d’incluant pas i. Chaque T ∗ (i, S) représente le temps minimal pour, en partant
de xi , visiter tous les lieux xj tels que j ∈ S puis aller en x0 . Les valeurs T ∗ (i, ∅) peuvent être calculées
directement ; pour les valeurs T ∗ (i, S) où S n’est pas vide, on peut utiliser une récurrence.
Implémenter une fonction qui résout ce problème présente plusieurs défis : outre la conception d’une
définition récursive pour les T ∗ (i, S), il faut également construire une représentation en Turbo-Pascal pour
les ensemles S, ainsi qu’ajouter de l’information pour que, au final, la fonction puisse afficher les détails
7
des parcours de longueur minimale.
Il est donc intéressant de trouver la manière la plus économique de découper la séquence A1 . . . An pour
minimiser le nombre de multiplications. Cela peut se faire de manière récursive : une parenthésation opti-
male de A1 . . . An découpe la séquence en A1 . . . Ai et Ai+1 . . . An et parenthèse ces deux sous-séquences
de manière optimale (le principe d’optimalité est vérifié). Et cette méthode peut être améliorée grâce à la
programmation dynamique.
1. Il est naturel de penser que la plus longue sous-séquence commune à a et b doit pouvoir se construire
à partir des plus longues sous-séquences communes à certains préfixes de a et préfixes de b. On pose donc
la notation Li,j , avec 0 ≤ i ≤ n et 0 ≤ j ≤ m, pour désigner la longueur de la plus longue sous-séquence
commune aux préfixes a1 . . . ai et b1 . . . bj . Pour quels i et j peut-on donner la valeur de Li,j directement ?
A quoi correspond la solution du problème principal ?
2. Il reste à traiter le cas récursif. Quels i et j sont concernés ? Et comment peut-on calculer Li,j à
partir des "valeurs plus petites" dans ces cas-là ?
Indice. On peut séparer les cas récursifs en deux sous-cas, selon que ai = bj ou non par exemple, c’est-à-
dire selon que les dernières lettres des préfixes considérés sont égales ou non. Si elles le sont, on obtient
7 une réponse assez facilement. Sinon, la plus longue sous-séquence commune ne contient pas ai et bj en
même temps ; cela permet de se ramener à deux sous-cas.
3. Construire une fonction récursive qui calcule les Li,j puis une autre qui indique la longueur de la
plus longue sous-séquence commune aux tableaux a[1..n] et b[1..m].
4. Transformer les fonctions de l’exercice précédent en fonctions qui donnent non seulement la longueur
de la plus longue sous-séquence commune, mais également une liste chaînée contenant les valeurs de cette
sous-séquence.
6. Construire une fonction qui remplit le (une partie du) tableau long avec les valeurs Li,j de "bas en
haut".
Comme indiqué plus haut, pour pouvoir reconstruire la plus longue sous-séquence commune, il faut
plus d’information : les Li,j ne suffisent pas. Plutôt que de créer un tableau dans lequel on stockerait
toutes les sous-séquences communes des préfixes a et b, on peut se contenter d’informations nécessitant
moins d’espace-mémoire.
7. Il y a trois sous-cas au cas récursif de calcul de Li,j . Il suffit de se souvenir de quel sous-cas a été
utilisé pour pouvoir reconstruire la sous-séquence commune la plus longue. Vérifier que c’est bien le cas.
1. Il est clair que celui-ci n’est pas nécessairement unique ... mais, on désire en connaître un seul.
Par la suite, on numérote les sous-cas 1, 2 et 3 et on note Ci,j l’identifiant du sous-cas utilisé.
8. Modifier la fonction précédente remplir_long pour qu’elle remplisse non seulement long mais aussi
cas, une variable globale déclarée par
9. Finalement, construire une fonction qui résout le problème initial en affichant non seulement la
longueur mais aussi les éléments de la plus longue sous-séquence commune.
1 #include <stdio.h>
2 #include <limits.h>
3 #define nm 10
4
5 #define mini(a,b) a<b? a:b
6
7 int NBCALL1=0;
8 int NBCALL2=0;
9 int t[nm+1];
10
11 int ML[nm+1][nm+1];
12 int MAT[nm+1][nm+1];
13 int PRNTSE[nm+1][nm+1];
14
15 // Fonction récursive naïve calculant le nombre de multiplication
16 int Cout_mul (int i, int j) {
17 int m3;
18 NBCALL1++;
19 if (i==j) return 0;
20 else {
21 int M =INT_MAX;
22 for (int k =i; k<j; k++) {
23 m3 = Cout_mul(i,k)+ Cout_mul(k+1,j) + t[i]*t[k+1]*t[j+1];
24 if( M>m3) M=m3;
25 }
26 return M;
27 }
28 }
29
30
31 // Fonction récursive avec mémoïsation
32 int Cout_mul2 (int i, int j) {
7 33
34
int m1;
NBCALL2++;
35 if (i==j) return 0;
36 else {
37 int M = INT_MAX;
38
39 for (int k =i; k<j; k++) {
40 if (ML[i][k]==0 ) ML[i][k] = Cout_mul2(i,k);
41 if (ML[k+1][j]==0) ML[k+1][j] = Cout_mul2(k+1,j);
42 m1 = ML[i][k]+ML[k+1][j] + t[i]*t[k+1]*t[j+1];
43 if( M>m1) M=m1;
44 }
45 ML[i][j]=M;
46 return M;
47 }
48 }
49
50 // Fonction itérative
51 int Cout_mul3 (int n) {
52 int m1 ;
53 int i,j,k,d;
54 for (i=1; i<=n ; i++) PRNTSE[i][i] =0;
55 for (i=1; i<=n ; i++) MAT[i][i] =0;
56 /**** remplissage des éléments de MAT diagonale par diagonale ***/
57 for (d=2; d<=n; d++) // d : numéro d'une diagonale
58 for (i=1; i<=n-d+1; i++) { // i : numéro de ligne pour un élément d'une diagonale d
59 j=i+d-1; // j : numéro de colonne pour un élément d'une diagonale d
60 MAT[i][j]=INT_MAX;
61 for (k=i; k<j;k++) {
62 m1= MAT[i][k] + MAT[k+1][j] + t[i]*t[k+1]*t[j+1];
63 if (m1<MAT[i][j]) {
64 MAT[i][j]= m1;
65 PRNTSE[i][j]=k;
66 }
67 }
68 }
69 return MAT[1][n];
70 }
71
72 // Fonction affichage
73 void affichage(int i, int j) {
74 if (i==j) printf(" %d ", i );
75 else {
76 printf("(");
77 affichage(i,PRNTSE[i][j]);
78 printf(" * ");
79 affichage(PRNTSE[i][j]+1,j);
80 printf(")");
81 }
82 }
83
84 void main() {
85 int i,j;int n=6;
86 t[1]= 10; t[2] =100 ;t[3] =5 ;t[4] =50 ;t[5] = 10;t[6] =5 ;t[7] =18 ;
87 /***************************************/
88 printf(" \n FN REC naive : le nombre de multiplication est %d ", Cout_mul(1,n));
89 printf(" \n le nombre d'appel est %d \n\n\n", NBCALL1);
90 /*****************************************/
91 for( i=1 ; i<=n;i++)for( j = 0 ; j<=n;j++) ML[i][j]=0;
92 printf(" \n FN REC avec MEMO : le nombre de multiplication est %d ", Cout_mul2(1,n));
93 printf(" \n le nombre d'appel est %d \n\n\n", NBCALL2);
94 for (i=1; i<=n ; i++) {
95 printf(" \n");
96 for (j=1; j<=n ; j++) printf(" %d ", ML[i][j]);
97 }
98 /*************************************/
99 printf(" \n \n \n FN ITER : le nombre de multiplication est %d \n\n\n", Cout_mul3(n));
100 for (i=1; i<=n ; i++) {
101 printf(" \n");
102 for (j=1; j<=n ; j++) printf(" %d ", MAT[i][j]);
103 }
104 printf(" \n\n");
105 for (i=1; i<=n ; i++) {
printf(" \n");
106
107
108 }
for (j=1; j<=n ; j++) printf(" %d ", PRNTSE[i][j]); 7
109 printf(" \n\n\n\n\n\n");
110 affichage(1,n);
111 }
1 #include <stdio.h>
2 #include <stdlib.h>
3 #define maxi(a,b) a>b? a:b
4
5 int a[15],b[15]; int c[15][15];int cas[15][15];
6
7 struct cell { int val; struct cell * suiv; };
8 typedef struct cell CELL;
9 typedef struct cell * liste;
10
11
12 void ajout(int val, liste *l) {
13 liste ele=(liste) malloc(sizeof(cell));
14 ele->val = val;
15 ele->suiv = *l;
16 *l=ele;
17 }
18
19
20 void concat(liste l1, liste *l){
21 if (l1 != NULL) {
22 concat(l1->suiv, l);
23 liste ele=(liste) malloc(sizeof(cell));
24 ele->val = l1->val;
25 ele->suiv = *l;
26 *l=ele;
27 }
28 }
29
30 void afficher(liste l) {
31 if(l!=NULL) {
32 printf("%d ", l->val);
33 afficher(l->suiv);
34 }
7 35
36
}
70 void remplir(void) {
71 int i,j;
72 for(i=0; i<15; i++)
73 for( j=0; j<15; j++)
74 c[i][j] = 0;
75 for(i=1; i<15; i++)
76 for( j=1; j<15; j++)
77 if (a[i]==b[j]) c[i][j]=c[i-1][j-1] +1;
78 else c[i][j] = maxi(c[i-1][j],c[i][j-1]);
79 }
80
81 /*************Question 6 : *******/
82 void remplir2() {
83 int i,j;
84 for(i=0; i<15; i++)
85 for(j=0; j<15; j++)
86 { c[i][j] = 0;cas[i][j]=0; }
87 for(i=1; i<15; i++)
88 for( j=1; j<15; j++)
89 if (a[i]==b[j]) {
90 c[i][j]=c[i-1][j-1] +1;
91 cas[i][j]=1;
92 }
93 else {
94 c[i][j] =maxi(c[i-1][j],c[i][j-1]);
95 if (c[i][j]==c[i-1][j]) cas[i][j]=2;
96 else cas[i][j]=3;
97 }
98 }
99
100 void affiche2( ) {
101 int i=15; int j=15;
102 while ((i>=1) && (j>=1))
103 if(cas[i][j]==1) {
104 printf("%d ", a[i]);
105 i--;j--;
106 }
else
107
108
109
if(cas[i][j]==2) i--;
else j--;
7
110 }
111
112 void main() {
113 int i,j;
114 liste l =NULL;
115 a[1]=8;a[2]=1;a[3]=2;a[4]=18;a[5]=1;a[6]=3;a[7]=1;a[8]=4;a[9]=1;a[10]=2;a[11]=18;a[12]=1;a←-
[13]=1;a[14]=2;
116 b[1]=3;b[2]=8;b[3]=1;b[4]=15;b[5]=4 ;b[6]=5 ;b[7]=12 ;b[8]=9 ;b[9]=5 ;b[10]=18 ;b[11]=1;b←-
[12]=1;b[13]=1;b[14]=1;
117 printf("\n\n\n FN REC naive: La longueur de la PLSSC est %d \n", Long_PLSSC_rec(14,14));
118 printf("\n\n\n FN REC naive avec sauvegarde: La longueur de la PLSSC est %d \n", ←-
Long_PLSSC_rec2(14,14,&l));
119 printf("\n la PLSSC est : ");
120 afficher(l);
121 printf("\n\n\n\n");
122 remplir();
123 for( i=1; i<15;i++) {
124 printf("\n");
125 for( j=1; j<15;j++) printf(" %d ", c[i][j]);
126 }
127 printf("\n\n\n\n");
128 printf("\n\n\n\n");
129 remplir2( );
130 for( i=1; i<15;i++) {
131 printf("\n");
132 for( j=1; j<15;j++) printf(" %d ", cas[i][j]);
133 }
134 printf("\n\n\n\n");
135 affiche2();
136 }
1 #include <stdio.h>
2 #define n 5
3 #define mini(a,b) a<b ? a : b
4 int NBCALL1=0;
5 int NBCALL2=0;
6 int NBCALL3=0;
7 int NBCALL4=0;
8 int t[n+1][n+1];
9
10 // Fonction récursive naïve calculant le nombre de trajets (déplacement d'un seul pas)
11 int NBT_rec (int i, int j) {
12 NBCALL1++;
13 if (i==1 ||j==1) return 1;
14 return NBT_rec(i-1,j)+NBT_rec(i-1,j-1)+NBT_rec(i,j-1);
15 }
16
17 // Fonction récursive avec mémoïsation calculant le nombre de trajets (déplacement d'un seul ←-
pas)
18 int NBT_Memo (int i, int j) { // un seul pas à la fois
19 int x, y, z;
20 NBCALL2++;
21 if ((i==1) ||(j==1)) return 1;
22 else
23 if (t[i-1][j] == 0) t[i-1][j]= NBT_Memo(i-1,j);
24 x=t[i-1][j];
25 if (t[i-1][j-1] == 0) t[i-1][j-1]= NBT_Memo(i-1,j-1);
26 y=t[i-1][j-1];
27 if (t[i][j-1] == 0) t[i][j-1]= NBT_Memo(i,j-1);
28 z=t[i][j-1];
29 t[i][j]=x+y+z;
30 return t[i][j];
31 }
32 }
33
103
NBCALL3);
/********************************************************/
7
104 for(i=1; i<=n; i++)
105 for (j=1; j<=n; j++)
106 t[i][j]=0;
107 t[1][1]=1;
108 printf("\n \n \n FN memo (plusieurs pas ): Le nombre de trajet pour n= %d est %d \n",n, ←-
NBT_pp_memo(n,n));
109 printf("\n \n FN REC avec MEMO ( plusieurs pas) : Le nombre d'appels est %d \n \n",NBCALL4←-
);
110 printf(" \n\n\n\n\n " );
111 for(i=1; i<=n; i++) {
112 printf(" \n " );
113 for (j=1; j<=n; j++) printf(" %d",t[i][j]);
114 }
115 printf(" \n\n\n\n\n " );
116 }
Sommaire
8.1 Principes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
8.2 Exemples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
8.3 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
147
1488.1 Principes BIHD3 - Meth Pgm
8.1 Principes
De manière intuitive, prendre la plus grande quantité possible d’un maitre-choix en trouvant localement un choix
qu’on peut toujours faire dans un optimum.
Ne marche pas très souvent (< 5% des cas).
Les exemples
8.2.1 Sac à dos continu borné* : Prendre d’abord les articles de meilleur rapport valeur/poids.
8.2.2 Pleins* : Faire le plein le plus tard possible.
8.2.4 Salle de spectacle* : Prendre l’activité qui se termine le plus tôt.
8.2.5 Codes de Huffman* : Fusionner les sous-arbres de moindre fréquences.
8.2.7 Voyageur de commerce : Commencer par la ville la plus proche.
Correct ?
Pour les problèmes marqués d’une * la solution est optimale. Sinon il donne une solution faisable, mais pas
optimale.
Preuve
Le schéma d’une preuve gloutonne a deux étapes.
On prouve que :
• on suppose un optimum,
• et on montre que "notre" solution a la même valeur d’objectif
8.2 Exemples
8.2.1 Sac à dos continu ou borné
A partir de l’exemple du sac à dos discret 7.3.2 page 131
Continu
Dans le cas où les articles sont vendus au poids ou au mètre, les qi peuvent donc être fractionnaires.
⇒ la solution est triviale : prendre uniquement l’article de meilleur rapport ⇔ qm = C/tm
Borné
Si une limite est imposée sur la quantité présente dans le magasin : qi ≤ Qi .
⇒ quand le stock du maitre choix est épuisé, prendre les articles selon les rapports décroissants jusqu’à remplir le
sac (ou vider le magasin).
Objectif
Donald a planifié son itinéraire de vacances, le long duquel il a relevé le kilométrage des stations d’essence.
Il veut s’arrêter le moins possible, sans tomber en panne sèche.
Données
• Une capacité de réservoir CM ≥ 0. Par facilité on suppose une consommation constante ; cette capacité est
donc exprimée en kilomètres.
• Le contenu du réservoir au départ C0 avec 0 ≤ C0 ≤ CM .
• La distance à parcourir L ≥ 0
• La position des n stations Si . . . Sn le long de la route avec
◦ 0 ≤ Si < Si+1 < · · · < L
◦ S0 = 0
Res
CM
C0
... km
S0 S1 S2 S3 S4 L
Analyse
On attend
8
• Soit un tableau q contenant les quantités achetées qi à chaque station i avec 0 ≤ qi ≤ CM
• Soit de signaler qu’il n’y a pas de solution faisable.
Ck = C(Sk ) + qk
A = { i ∈ 1..n | qi > 0 }
Le problème est de minimiser le nombre d’arrêts |A| sans tomber en panne avant de rallier l’arrivée :
ce qui constitue un invariant (une contrainte) le long du trajet mais pas dans la planification.
CM
CM
C0
q
... km
S0 S1 S2 S3 S4 L
res
CM
C0 q0
q
8
... km
S0 S1 S2 S3 S4 L
Preuve :
A0 = A donc la valeur de l’objectif est la même.
q 0 reste faisable (on ne tombe pas en panne) car CM ≥ C 0 (l) ≥ C(l) ≥ 0.
res
CM
C0 q”
q
? ?
... km
S0 S1 S2 S3 S4 L
Preuve
La fonction C”(l) est identique à C(l) sauf entre Sk et Sk+1 , où C”(l) = C(l) − qk , mais elle satisfait toujours la
contrainte.
Le variant de la récursion : le nombre de stations diminue.
La récursion est terminale car il est inutile de revenir en arrière. On peut donc l’implémenter avec une boucle.
Implémentation
C := C0 ;
{ étude des choix à chaque station S[i] }
for i := 1 to n do begin
{ de quoi arriver à cette station ? Valable surtout pour la première ! }
if C < (S[i] - S[i-1])
then panneEssence
{ si oui, dois-je y faire le plein ? }
else if (S[i+1] - S[i-1]) > C
then begin { je fais le plein }
q[i] := CM - (C - (S[i] - S[i-1])) ;
C := CM
8
end
else begin { non ; mais mise à jour des variables }
q[i] := 0 ;
C := C - (S[i] - S[i-1]) ; { ce que j'ai consommé }
end
Complexité
Temps d’exécution : O(n)
Mémoire : O(1) si on néglige les données S[1..n],q[1..n] que tous les autres algo doivent aussi avoir !
Données
On ajoute le prix du carburant à chaque station pi > 0
Analyse
Le coût à minimiser est
n
X
pi ∗ qi
i=1
Choix glouton
Remplir le plus possible à la station m la moins chère :
Pour résoudre les trajets avant et après m on fait deux appels récursifs.
Temps
Retrouver la station la moins chère est linéaire, ce qui mène a un coût total quadratique O(n2 ).
8 Un arbre rouge-noir trié sur le kilométrage des stations et augmenté par le prix minimum des sous-arbres permet
de retrouver la station de prix minimum entre Si et Sj en temps O(n log n).
arn = ^node;
node = record
sta : int ; { position de la station, en km depuis le départ }
prix : real ; { prix au km, pour rester cohérent}
g : arn ;
d : arn ;
prix_min_g : real ; { prix min à gauche }
prix_min_d : real ; { prix min à droite }
red : boolean
end ;
Données
Une liste d’activités candidates ki (1 ≤ i ≤ n) avec pour chacune :
– si : temps de début
– fi : temps de fin (avec si ≤ fi )
k2
k7 k6
k3
k1 k4
k5
Analyse
Le résultat doit produire un ensemble A ⊂ { 1 . . . n } de taille maximale d’activités compatibles (soit chronologi-
quement disjointes)
⇔ ∀ i, j ∈ A : si ≥ fi ∨ sj ≥ fi
Choix glouton
Faisable ?
Une solution optimale contient l’activité k qui (commence et) se termine le plus tôt.
A partir d’une solution optimale A et m sa première activité, on a A0G = A\ { m } ∪ { k }
⇒ A0G reste optimale.
Après avoir choisi k, on résoud récursivement pour les activités compatibles avec k.
8
Le variant est le nombre d’activités qui diminue (les incompatibles disparaissent)
Algorithme
• Trier les activités par temps de fin (en O(n log n)).
• A := { }
• f := −1
• Pour chaque activité i prise dans cet ordre (O(n))
si ≥ f ⇒ A := A ∪ { i } ∧ f := fi
Codage de caractères
Fixe : p.ex. ISO-Latin-1 fixe 8 bits pour chaque caractère.
Morse : les caractères plus fréquents (par ex. ’E’) ont un code plus court.
Le problème du morse est de séparer deux caractères :
• ¯
e t
i a n m
8 s u r w d k g o
h v f l p j b x c y z q
Exemple 8.2.1
C = { a, b, c, d, e, f } et f = 45, 13, 12, 16, 9, 5 (soit un total de 100%).
codage ISO-Latin-1 : 8 ∗ (f )100 = 800 bits.
calcul du nombre de bits pour un codage fixe :
donc codage fixe sur 3 bits : 3 ∗ (f )100 = 300 bits (dlog2 |C|e = 3).
codage optimal = 224 bits (en multipliant chaque longueur de code par sa fréquence)
a b c d e f
0 101 100 110 1111 1110
0 1
a 0 1
0 1 0 1
c b d 0 1
f e
Lemmes
Lemme 8.1
8
Un code est sans préfixe ssi un caractère n’apparaît que sur une feuille de l’arbre.
Lemme 8.2
Dans un code sans préfixe optimal un noeud a 0 ou 2 fils. En effet, dans le cas contraire, on peut "raccourcir la
branche".
X X
cout(T ) − cout(T 0 ) = f [c] ∗ d(c) − f [c] ∗ d0 (c)
c c
= f [x] ∗ d(x) + f [b] ∗ d(b) − f [x] ∗ d0 (x) − f [b] ∗ d0 (b)
or d0 (x) = d(b)
= (f [b] − f [x]) ∗ (d(b) − d(x))
= (≥ 0) ∗ (≥ 0) (car x moins fréquent et b plus profond)
Récursion
Après avoir regroupé les deux caractères les moins fréquents, peut-on se ramener à un problèmes similaire ?
8 Soit T une solution, x, y deux caractères frères de T .
Alors T est optimal ⇔ T \setx, y ∪ { z } est optimal pour le problème
avec C 0 = C\ { x, y } ∪ { z }
où z est un nouveau caractère de frésuence f 0 [z] = f [x] + f [y]
de façon que la meilleure solution pour le reliquat demeure la solution optimale.
X
cout(T ) = f [c] ∗ d(c)
c∈C
!
X
= f [c] ∗ d(c) + f [x] + f [y]
c∈C 0
= f [x] ∗ d(x) + f [y] ∗ d(y) + f [x] + f [y]
car f 0 [z] = f [x] + f [y]
et d(z) + 1 = d(x) = d(y)
= cout(T 0 ) + (f [x] + f [y])
Algorithme
Tant qu’il reste au moins deux caractères :
8.2.6 Exposants
Voir le livre de référence [Cor04, exercice 16.2.7] et les slides [Sch12, s. 305-306].
Problème
Un représentant de commerce doit visiter n clients puis rentrer chez lui, par le plus court chemin possible.
Données
Un ensemble de "villes" V = 0, . . . , n et leurs distances d : V × V → R+ .
Résultat
Une permutation p des villes telle que
n−1
X
d(pi , pi+1 ) + d(pn , p0 )
i=0
est minimal.
Exemple 8.2.3
Considérons les villes suivantes chacune munie de sa coordonnée (x, y), le tout sous la distance euclidienne.
c(1, 7) d(15, 7)
b(4, 3) e(15, 4)
a(0, 0) f (8, 0)
8
Le parcours optimal est [a, b, f, e, d, c] pour un coût (distance) de 48, 39.
Exemple 8.2.4
Pour notre exemple précédent, cet heuristique va donner le parcours suivant
c(1, 7) d(15, 7)
b(4, 3) e(15, 4)
a(0, 0) f (8, 0)
De proche en proche, le parcours obtenu [a, b, c, d, e, f ] à un coût (distance) de 50, 0, soit supérieur au
coût optimal, car le dernier trajet est souvent long pour revenir au point de départ.
Noter que si on partait de b, cette solution serait aussi la solution optimale !
8 Si les arcs sont dans un tas, on trouve une complexité de O(n2 log n) car il y a ≈ n2 arrêtes.
8.3 Exercices
8.3.1 Questions du TP 7
1 Algorithmes gloutons
Exercice 1 (Les pièces de monnaie)
On dispose des pièces de monnaie correspondant aux valeurs {a1 , a2 , ..., an }
Pour chaque valeur le nombre de pièces est non borné. Etant donnée une somme S entière, on veut
trouver une façon de rendre la somme S avec un nombre de pièces minimum.
De manière générale, l’algorithme glouton ne donne pas de solution optimale pour ce problème. on peut
par contre montrer que qu’il la donne pour certains ensembles de pièces disponibles. Il s’agit ici de montrer
que pour le système monétaire européen {50,20,10,5,2,1}, l’algorithme donne une solution optimale.
Question Proposer un algorithme qui renvoie la valeur optimale du butin du voleur. Ce choix est-il
unique ? Programmer une fonction qui implémente cet algorithme (on pourra supposer que le tableau p
est trié).
On suppose maintenant que les objets sont non fractionnables (c’est le cas d’une chaise ou d’un télé-
viseur). Le i-ème objet a une valeur pi et pèse un poids qi .
Question Proposer une méthode dérivée de la question 1 pour résoudre le nouveau problème. Donne-
8 t-elle un choix optimal ?
Exercice 3 (Empaquetage)
On dispose d’un ensemble d’objets E= {O1 , O2 , ..., On } de poids respectifs P = {P1 , P2 , ..., Pn } et des
boites de capacité C.
Le problème consiste à placer les objets dans les boites en respectant leurs capacités et en utilisant le
moins possible de boites.
Sommaire
9.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
9.2 Génération . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
9.3 Améliorations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
9.4 Branch and Bound . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
9.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
163
1649.1 Principe BIHD3 - Meth Pgm
9.1 Principe
Idée
Essayer toutes les solutions. Pour un problème d’optimisation, on garde la meilleure.
Méthode
On divise la spécification du problème en deux parties :
9.2 Génération
1. Un programme récursif permettra de générer toutes les solutions
2. chaque niveau de récursion reçoit une solution partielle (état) et essaie toutes les alternatives d’un nouveau
choix : La pile de récursion contient l’information utile pour continuer la génération. Son exécution peut
être représentée par un arbre d’exploration. La récursion classique l’explore en profondeur (DFS).
3. Lorsque la solution courante est complète, il teste les contraintes
4. Pour un problème d’optimisation, on évalue la valeur de l’objectif et on mémorise la solution courante si
c’est la meilleure jusqu’ici.
9.3 Améliorations
Elagage
On a intérêt à évaluer les contraintes dès que possible sur une solution partielle, soit pour éliminer toutes les
solutions descendantes.
Propagation
Trouver des conséquences plus simples des contraintes permet de les évaluer plus tôt.
Domaine
En particulier, on peut retenir les alternatives encore possibles pour un choix. S’il n’y en a plus qu’une, on la prend.
Intuitivement c’est ce que l’on fait manuellement quand on place les reines sur la grille.
Pour les 4 reines, supposons qu’on choisisse pour chaque colonne de gauche à droite. La première solution générée
est 1,1,1,1, ensuite 1,1,1,2, etc. Mais on aurait pu backtracker dès 1,1 (même ligne).
(a, b)?
oui non
Borne inférieure
• Arc inclus ⇒ coût connu.
• Chaque noeud doit avoir un arc entrant et un sortant : on complète avec les arc minimaux
Exemple 9.4.1
Pour trouver un chemin de la ville A vers la ville B la distance à vol d’oiseau peut constituer une borne
qui limite l’exploration afin de ne pas calculer la distance de toutes les villes du monde.
Algorithme
• Un choix est dit "ouvert" si on ne l’a pas exploré mais bien son père ;
• Heuristique : explorer le choix ouvert de moindre borne inférieure
• On élague lorsqu’une borne inférieure dépasse la meilleure valeur trouvée.
9.5 Exercices
9.5.1 Questions du TP 6
9 5. Définir précisément les représentations C des différents types d’objets manipulés (i.e. donner des
conventions de représentations) et traduire les primitives “abstraites” en termes des représentations
adoptées. Justifier (expliquer) le choix effectué pour les représentations.
6. Donner l’algorithme concret qui découle des deux étapes précédentes.
7. Implémenter un petit programme permettant de rentrer au clavier une “liste” d’obstacles et de, pour
cette liste d’obstacles, permettre le calcul de plusieurs trajets du petit robot. Pour chaque trajet, le
programme demandera un point de départ et un point d’arrivée et puis affichera successivement les
différents états de la grille le long du trajet trouvé.
Il est évident que toute fonction intermédiaire doit être spécifiée.
On demande de ne pas trop s’attarder sur les fonctions d’affichage : un simple quadrillage avec des
caractères est largement suffisant.
[Cor04] Thomas Cormen. Introduction à l’algorithmique, cours et exercices. DUNOD, 2004. 81, 82, 85, 149, 153,
154, 158
[Des11] Marc Dessoy. Méthodes de programmation, partie 1 (notes personnelles du cours de bac 2). www.madit.be,
2011. 5, 31, 32, 33, 45, 119, 120, 121
[Sch12] Pierre-Yves Schobbens. Méthodes de programmation partie 2, slides du cours de bac 3. Université de
Namur, 2012. 5, 33, 54, 122, 158
171
172BIBLIOGRAPHIE BIHD3 - Meth Pgm
Préambule 5
Sources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Mise en garde . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
I Concepts communs 7
1 La récursion 9
1.1 Récursion bien fondée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.1.1 Relation bien fondée et cas de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.1.2 Preuves par induction générale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.1.3 Récursion croisée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.1.4 Exemple de procédure récursive : le labyrinthe . . . . . . . . . . . . . . . . . . . . . . . . 11
1.2 Récursion terminale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.2.1 Terminale ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.2.2 Exemple avec un accumulateur : la fonction ’factorielle’ . . . . . . . . . . . . . . . . . . . 12
1.2.3 L’accumulateur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.2.4 Deux accumulateurs (la suite de Fibonacci) . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.2.5 Récursivité terminale et les langages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.3 Structures de données récursives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.3.1 Liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.3.2 Arbres binaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.3.3 Arbres binaires triés (ABT) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.3.4 Arbres ordonnés (AO) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.4 Exercices de TP - procédures récursives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.4.1 Fonctions récursives terminales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.4.2 Recherche du minimum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.4.3 Algorithme du tri fusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
173
174BIBLIOGRAPHIE BIHD3 - Meth Pgm
4.7.7 Supprimer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
4.7.8 Web . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
4.8 B-Arbres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
4.8.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
4.8.2 Procédures et fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
4.9 Exercices de TP - implémentation des ensembles . . . . . . . . . . . . . . . . . . . . . . . . . . 98
4.9.1 Arbres binaires ABR et ARN . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
4.9.2 Code C complet pour un arbre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102