' $ Algorithmes de recherche & % 1 ' $ 1. La recherche dichotomique et applications — Calcul rapide de l’exposant — Calcul de la racine carrée 2. Introduction à la technique diviser pour régner — Somme maximale des sous-tableaux — Recherche de la k -ème plus petite valeur dans un tableau 3. La recherche de sous-chaı̂ne — Recherche exhaustive — L’algorithme de Knuth, Morris, et Pratt — L’algorithme de Boyer-Moore & % 2 ' $ Recherche dichotomique et applications • Idée principale : faire avancer le calcul en divisant en deux l’espace de recherche • Exemple-clé : la recherche dichotomique • Autres exemples : — Calcul rapide de l’exposant — Calcul de la racine carrée & % 3 ' $ Calcul rapide de l’exposant - Approche 1 P ≡ x = x0 ∧ n = n0 > 0 Q ≡ x = x0 ∧ n = n0 ∧ w = xn I1 ≡ x = x0 ∧ n = n0 ∧ wk = xn ∧ 1 ≤ k ≤ n & % 4 ' $ float fexp1(float x, int n) { float w = x; int k = n; while (k>1) { w=w*x; k--; } return w; } Listing 1 – Calcul classique de l’exposant – O(n) & % 5 ' $ Calcul rapide de l’exposant - Approche 2 P ≡ x = x0 ∧ n = n0 > 0 Q ≡ x = x0 ∧ n = n0 ∧ w = xn I2 ≡ x = x0 ∧ n = n0 ∧ y × wk = xn ∧ 1 ≤ k ≤ n & % 6 ' $ float fexp(float x, int n) { int k=n; float w=x; float y=1; while (k>1) if (k%2==0) { k=k/2; w=w*w; } else { k=(k-1)/2; y=y*w; w=w*w; } return (w*y); } Listing 2 – Calcul rapide de l’exposant – O(log2 n) & % 7 ' $ Calcul de la racine carrée #define PRECISION 0.00001 double rcarree(double a) { double s,e,m; s=0; e=a; while (e-s>PRECISION) { m=(s+e)/2; if (m*m==a) return m; else if (m*m<a) s=m; else e=m; } return (s+e)/2; } & Listing 3 – Calcul de la racine carrée 8 % ' $ Introduction à la technique diviser pour régner • Idée-clé : afin de résoudre un problème donné, — on divise le problème en n sous-problèmes (n étant minimum 2) ; — on résout chaque sous-problème de façon récursive ; — on combine les n solutions obtenues en une solution pour le problème complet • Exemples : — Somme maximale des sous-tableaux — Recherche de la k -ème plus petite valeur dans un tableau & % 9 ' $ Somme maximale des sous-tableaux Pour un environnement #define N int a[N]; int somme; on définit les assertions P et Q suivantes comme, respectivement, pré- et postcondition : P ≡ Q ≡ a = a0 ∧ N > 0 a = a0 ∧ 0 ≤ i ≤ j + 1 ≤ N Pk=j somme = k=i a[k] ∀n, l : 0 ≤ n ≤ l + 1 ≤ N ⇒ Pk=l a[k] ≤ somme k=n & % 10 ' $ 3 −4 6 2 9 −5 & 8 −9 −2 8 % 11 ' $ int maxsomme_bf(int a[N]) { int i,j,k,sum; int max=0; for(i=0;i<N;i++) { sum=0; for(j=i;j<N;j++) { sum=sum+a[j]; max=max2(max,sum); } } return max; } Listing 4 – Somme maximale, force brute & % 12 ' $ int maxs_a(int a[N], int d, int f) { int m, max_m; if (d<=f) { m=(d+f)/2; max_m=max_repartie(a,d,f,m); return max3(max_m, maxs_a(a,d,m-1), maxs_a(a,m+1,f)); } else return 0; } int maxsomme_dq(int a[N]) { return maxs_a(a,0,N-1); } Listing 5 – Somme maximale, diviser pour régner & % 13 ' $ int max_repartie(int a[N],int d, int f, int m) { int sum, mgauche, mdroite,i; sum=0; mgauche=0; for(i=m-1;i>=d;i--) { sum=sum+a[i]; mgauche=max2(mgauche,sum); } sum=0; mdroite=0; for(i=m+1;i<=f;i++) { sum=sum+a[i]; mdroite=max2(mdroite,sum); } return mgauche+a[m]+mdroite; } Listing 6 – La fonction maxRepartie & % 14 ' $ Recherche de la k -ème plus petite valeurs dans un tableau — soit le tableau — redistribuer les éléments autour d’une valeur pivot v , aléatoirement choisie. Prenons v =5: — rediriger et limiter la recherche à l’une des trois parties. Par exemple, chercher la 8-ème plus petite valeur de S revient à chercher la 3-ème plus petite valeur de SR . — Représentation : & % 15 ' $ La procédure redistribuer (invariant) ∀j : d ≤ j < i − m ⇒ a[j] < v ∀j : i − m ≤ j < i ⇒ a[j] = v ∀j : i ≤ j < p ⇒ a[j] > v & % 16 ' $ void redistribuer(int a[],int d, int f, int v, int *i, int *m) { int p,t; *i=d; p=d; *m=0; while ((p<=f) && (*i<=f)) { if (a[p]==v) { a[p]=a[*i]; a[*i]=v; (*m)++; (*i)++; & % 17 ' $ } else if (a[p]<v) { t=a[*i]; a[*i-*m]=a[p]; a[p]=t; if (*m>0) a[*i]=v; (*i)++; } p++; } } & % 18 ' $ int k_plus_petite(int a[], int d, int f, int k) { int i,m,v; v=a[f]; redistribuer(a,d,f,v,&i,&m); if (k<=i-d-m) { return k_plus_petite(a,d,(i-d-m-1),k); } else if (k<=i-d) { return v; } else { return k_plus_petite(a,i,f,(k-i+d)); } } & % 19 ' $ 3. Recherche de sous-chaı̂ne Pour un environnement #define N #define M char s[N]; char p[M]; int k; & % 20 ' $ on définit pré post ≡ N ≡ ≥ M > 0 ∧ s = s0 ∧ p = p0 s = s0 ∧ p = p0 0≤k ≤N −M ⇒ match(k , M ) ∧ ∀k 0 < k : ¬match(k 0 , M ) k = −1 ⇒ ∀k 0 : ¬match(k 0 , M ) où match(i, j) ⇔ ∀r : 0 ≤ r < j : s[i + r] = p[r] Quel sera l’invariant ? & % 21 ' $ Approche 1 : recherche exhaustive Proposition pour l’invariant : I≡ s = s0 ∧ p = p0 0≤k <N ∧0≤j ≤M ∀r : 0 ≤ r < j ⇒ p[r] = s[k + r] Exploiter cet invariant donne lieu à un algorithme implémentant une recherche exhaustive (angl. brute force) & % 22 ' $ int match(char *s, char*p) { int j,k,n,m; n=strlen(s); m=strlen(p); k=0; j=0; while ((j<m) && (k<=n-m)) { if (p[j]==s[k+j]) { j++; } else { k=k+1; j=0; } } if (j<m) return -1; else & return k; % } 23 ' $ • Quel est le pire des cas pour l’algorithme ? • Quelle est la complexité, au pire des cas ? & % 24 ' $ Approche 2 : l’algorithme KMP • inventé par D. Knuth, J.H. Morris, V. Pratt (1977) • l’idée : exploiter des informations concernant le pattern (p) pour éviter de comparer une deuxième fois les caractères du texte (s) déjà comparés : Echec de comparaison (s[k+1]6=p[j+1] (a), déplacement effectué par l’algorithme exhaustif (b), déplacement effectué par KMP (c) & % 25 ' $ Besoin de connaı̂tre, pour chaque position j dans p, la longueur du plus long préfixe étant aussi suffixe de p[0..j − 1]. & % 26 ' $ void compute_prefix_f(char *p, int pi[]) { int m,k,q; m=strlen(p); pi[0]=0; k=0; q=1; while (q<m) { if (p[q]==p[k]) { k++; pi[q]=k; q++; } else if (k!=0) { k=pi[k]; } else { pi[q]=0; q++; } &} % } 27 ' $ int match_kmp(char *s, char *p) { int n,m,j,k; int pi[strlen(p)]; compute_prefix_f(p,pi); n=strlen(s); m=strlen(p); k=0; j=0; while ((j<m) &&(k<=n-m)) { if (p[j]==s[k+j]) { j++; } else if (j==0) { k++; } else { k=k+j; j=pi[j-1]; & } % } 28 ' $ if (j<m) return -1; else return k; } & % 29 ' $ • Quelle est la complexité de l’algorithme KMP au pire des cas ? • Comment l’algorithme se comporte dans le cas moyen ? • Y-a-t il un moyen pour faire mieux, et de passer en-dessous de la linéarité ? & % 30 ' $ Approche 3 : Boyer-Moore (version simplifiée) • inventé par R.S. Boyer and J.S. Moore (1977) • idée : utiliser des informations à propos du pattern pour éviter de considérer des caractères du texte dont on sait qu’ils ne pourraient pas faire partie d’un match • Complexité au cas moyen : O(N/M ) & % 31 ' $ int match_bm(char *s, char *p) { int n,m,j,k; int last[CHAR_MAX] = { 0 }; n=strlen(s); m=strlen(p); for(j=0;j<m;j++) { last[p[j]]=j; } k=0; j=m-1; while ((j>=0) && (k<=n-m)) { if (p[j]==s[k+j]) { j--; } else { k=k+max2(1,j-last[s[k+j]]); j=m-1; } &} % 32 ' $ if (j!=-1) return -1; else return k; } & % 33 ' $ I p = p0 ∧ s = s0 ≡ −1 ≤ j ≤ M ∧ 0 ≤ k ≤ N − M p[j + 1..M − 1] = s[k + j + 1..k + M − 1] & % 34 ' $ A noter : • Pour cette version simplifiée, la complexité au pire des cas est de O(N M ) • L’algorithme complet de Boyer-Moore utilise des connaissances supplémentaires à propos du pattern (comme KMP) pour arriver à une complexité au pire de cas de O(N + M ). & % 35 ' $ Le tri et ses applications & % 36 ' $ 1. Algorithmes de tri — Tri par sélection — La méthode Quicksort 2. Quelques algorithmes de tri avancé — L’algorithme heapsort — Le tri en ordre linéaire : radix sort 3. Applications de tri — Calcul de l’enveloppe convexe — Le problème du mariage stable & % 37 ' $ Tri par sélection Iext a permutation de a0 0≤i<N ≡ ∀k : i + 1 ≤ k < N − 1 ⇒ a[k] ≤ a[k + 1] ∀k : 0 ≤ k ≤ i ⇒ a[k] ≤ a[i + 1] Iint ≡ max = a[pos] −1 ≤ j ≤ i − 1 ∀k : j + 1 ≤ k ≤ i ⇒ a[k] ≤ max & % 38 ' $ int i,j,pos; float max; i=N-1; while (i>=1) { max=a[i]; pos=i; j=i-1; while (j>=0) { if (a[j]>max) { max=a[j]; pos=j; } j--; } a[pos]=a[i]; a[i]=max; i--; & } % } 39 ' $ La méthode Quicksort void quicksort(int a[N]) { quicksort_a(a,0,N-1); } void quicksort_a(int a[N], int d, int f) { int p; if (d < f) { p = partager(a,d,f); quicksort_a(a,d,p-1); quicksort_a(a,p+1,f); } } Listing 7 – L’algorithme Quicksort (a) & % 40 ' $ int partager(int a[N], int d, int f) { int v,x,i,j; v = a[f]; i = d-1; j = f; do { do i++; while (a[i] < v); do j--; while (a[j] > v); if (i < j) { x = a[i]; a[i] = a[j]; a[j] = x; } } while (j > i); a[f] = a[i]; a[i] = v; return i; } & % 41 ' $ L’algorithme Heapsort Pour rappel : le tas comme structure de données — arbre binaire presque complet — la valeur de chaque noeud est supérieure (ou égale) aux valeurs de ses descendants 1 100 N 19 36 17 3 25 & 1 2 7 % 42 ' $ Algorithme pour effectuer le tri d’un tableau a : 1. transformer le tableau a en un tas t 2. ∀i ∈ {N, . . . , 1} : a) a[i] ← root(t) b) retransformer t en un tas en prenant la dernière feuille comme nouvelle racine et la ”poussant” vers le bas & % 43 ' $ iheap_t init_heap(void) { iheap_t h; int i; h = malloc(sizeof(struct iheap)); h->nb = 0; for (i = 0; i < MAX_TAS; i++) h->els[i] = 0; return h; } void add_element(iheap_t h, int x) { if (has_room(h)) { h->els[h->nb] = x; reheap_up(h, h->nb); h->nb = h->nb + 1; } } & % 44 ' $ void reheap_up(iheap_t h, int i) { int p; p = parent(i); while (p >= 0 && h->els[p] < h->els[i]) { exchange(h, i, p); i = p; p = parent(i); } } & % 45 ' $ int take_root(iheap_t h) { int x; if (h->nb >= 0) { x = h->els[0]; h->els[0] = h->els[h->nb - 1]; h->nb = h->nb - 1; reheap_down(h); return x; } else { return -INT_MAX; } } & % 46 ' $ void reheap_down(iheap_t h) { int i, maxchild; bool stop; i = 0; stop = false; do { if (right(i) < h->nb) { if (h->els[left(i)] < h->els[right(i)]) { maxchild = right(i); } else { maxchild = left(i); } } else { if (left(i) < h->nb) { maxchild = left(i); } else { maxchild = i; & } % } 47 ' $ if (h->els[i] < h->els[maxchild]) { exchange(h, i, maxchild); i = maxchild; } else { stop = true; } } while (!stop); } & % 48 ' $ void heap_sort(int a[], int n) { iheap_t h; int i; h = init_heap(); for (i=0;i<n;i++) { add_element(h,a[i]); } for(i=n-1;i>=0;i--){ a[i] = take_root(h); } } & % 49 ' $ Remarques : • On peut prouver que O(nlog2 n) est une borne inférieure au tri basé sur la comparaison des clés ; ce qui veut dire qu’il n’y a pas vraiment moyen de faire mieux • Mais : si le type des clés est discret (et limité), il y a moyen de faire mieux : radix sort. & % 50 ' $ Le tri en ordre linéaire : Radix Sort & % 51 ' $ & % 52 ' $ struct list { int info; struct list *next; }; typedef struct list *list_ptr; int charac(int i, int x) { while (i>1) { x=x/BASE; i--; } return x%BASE; } & % 53 ' $ void radix(list_ptr *r) { list_ptr head[BASE], tail[BASE]; int i,j,h; for(i=1;i<DIGITS;i++) { for(j=0;j<BASE;j++) head[j]=NULL; /* partition de la liste */ while (*r!=NULL) { h=charac(i,(*r)->info); if (head[h]==NULL) { head[h]=*r; } else tail[h]->next=*r; tail[h]=*r; *r=(*r)->next; tail[h]->next=NULL; } & % 54 ' $ /* reconstruction de la liste */ *r=NULL; for(j=BASE-1;j>=0;j--) { if (head[j]!=NULL) { tail[j]->next=*r; *r=head[j]; } } } } & % 55 ' $ Evaluation expérimentale N sélection par tas radix sort 1.000 2 0 0 5.000 38 1 0 10.000 152 3 2 100.000 15.127 35 20 TABLE 1 – Quelques temps d’exécution des 3 algorithmes de tri (en millisecondes). & % 56 ' $ Applications du tri • Calcul de l’enveloppe convexe • Le problème du mariage stable & % 57 ' $ Calcul de l’enveloppe convexe • Définition (dans le plan 2D) • Algorithme naı̈f : O(n3 ) où n représente le nombre de points. & % 58 ' $ Approche 1 : Algorithme exhaustive procedure EC(a set of points P ) H←∅ for all (p, p0 ) ∈ P × P do v ← true for all q ∈ P with q 6= p and q 6= p0 do → 0 if q is on the left of pp then v ← f alse if v then → 0 H ← H ∪ {pp } return H & % 59 ' $ Approche 2 : L’algorithme Graham scan Comment imposer une structure sur l’ensemble des points ? & % 60 ' $ Proposition (R. Graham, 1972) : trié au sens inverse des aiguilles d’une montre autour du point se trouvant le plus bas. & % 61 ' $ procedure EC(a set of points P ) let p1 be the lowest point in P let p2 , . . . , pN be the remaining points sorted counterclockwise (by angle) around p1 S ← empty stack S ← push(S, p1 , p2 , p3 ) for i from 4 to N do while (St−1 , St , pi ) do not form a left turn do S ← pop(S) S ← push(S, pi ) return S Dans l’algorithme, St et St−1 représentent, respectivement, l’élément au sommet de S et l’élément précédent dans S & % 62 ' $ & % 63 ' $ & % 64 ' $ & % 65 ' $ & % 66 ' $ Questions • Quelle est la complexité de l’algorithme ? • Quel choix pour les structures de données ? & % 67 ' $ Mariage stable Supposons N hommes et N femmes qui veulent se marier. Etant donné que • pour chaque homme la liste comprend les N femmes en ordre de préférence décroissante • pour chaque femme la liste comprend les N hommes en ordre de préférence décroissante trouver un mariage stable. Un mariage est stable s’il n’y a aucune personne X telle que — X préfère une personne Y à son partenaire actuel — Y préfère la personne X à son partenaire actuel Principe de cet algorithme à la base de plusieurs algorithmes utiles & % 68 ' $ Par exemple, soient les préférences suivantes : Julien → Amélie, Bernadette Amélie → Julien, Christian Christian → Amélie, Bernadette Bernadette → Christian, Julien (Julien,Bernadette), (Christian,Amélie) pas stable (Julien,Amélie), (Christian,Bernadette) stable Mariages possibles : (Gale & Shapley 1962) : il existe toujours une solution stable pour n’importe quel N & % 69 ' $ ∈ M and w ∈ W are free while ∃m ∈ M free and hasn’t proposed to every woman do choose such a man m ∈ M let w be the highest-ranked woman in m’s preference list to whom m has not proposed if w is free then (m, w) become engaged Initially all m else let m0 be the man w is currently engaged to if w prefers m to m0 then (m, w) become engaged m0 becomes free else m remains free return the set S of engaged pairs & % 70 ' $ Analyse de l’algorithme • Une fois associée à un partenaire, w ∈ W reste engagée et elle se voit attribuée des partenaires de plus en plus bonne qualité (selon la liste de ses préférences) • Lors du processus, un homme m ∈ M propose à une suite de femmes de moins en moins bonne qualité (selon la liste de ses préférences) • Quel est l’invariant de la boucle ? • Comment mesurer le progrès effectué par l’algorithme ? & % 71 ' $ Comment mesurer le progrès effectué par l’algorithme ? • Soit P (t) l’ensemble de couples (m, w) tels que m a proposé à w lors des t premières itérations. • Alors on a ∀t : P (t + 1) > P (t) et ∀t : P (t) ≤ N 2 où N = |M | = |W | • D’où O(N 2 ) & % 72 ' $ Analyse de l’algorithme (2) • L’algorithme est non-déterministe, mais toutes les exécutions calculent la même attribution : S = {(m, best(m)|m ∈ M } où best(m) représente le partenaire le plus préféré attribué à m dans l’une des attributions stables. • Mais. . .dans S chaque w ∈ W se voit attribuée le partenaire le moins préféré ! • Exemple : m préfère w sur w0 m0 préfère w0 sur w w préfère m0 sur m w0 préfère m sur m0 Quelle des deux attributions stables est calculée par l’algorithme ? & % 73