Structures de données IFT-2000 Abder Alikacem La récursivité Semaine 5 Département dinformatique et de génie logiciel Édition Septembre 2009
Plan du cours Notions de base Exemple simple (fonction factorielle) Limitations (suite de Fibonacci) Tours de Hanoï Recherche binaire (ou dichotomique) Sous-séquence de somme maximale Backtracking
Notions de base Définition Un objet est récursif s'il est défini à partir de lui-même. Une fonction est récursive si elle peut s'appeler elle-même de façon directe ou indirecte Quand ? Suite récurrentes (exemple : factorielle) Type récursifs Liste non vide = premier + liste Arbre binaire = racine + sous-arbre droit + sous-arbre gauche (semaine 10)
Récursivité Idée générale : La réalisation d'une tâche par un algorithme récursif repose sur deux éléments: La résolution partielle du problème d'une façon simple. La réalisation du reste de la tâche étant déléguée aux appels récursifs successifs. La détermination d'une condition d'arrêt qui permet d'arrêter la cascade d'appels récursifs. Résoudre le problème au moment où la condition d'arrêt est détectée correspond en général à résoudre un cas trivial de celui-ci.
Récursivité Quatre règles régissent la récursivité: 1.Trouver la récursion 2.Présence de cas de base pouvant être résolu sans récursivité 3.Toujours progresser vers le cas de base à chaque appel récursif 4.Vérifier que les appels récursifs progressent réellement vers le cas de base. 5.Intérêt composé: Ne jamais dédoubler du travail dans deux appels récursifs différents (voir la fonction récursive de Fibonacci).
Récursivité Structure générale dune fonction récursive { ! if(/* !condition de convergence */) …; if(/*condition darrêt*/) return(/*Ce quelle doit retourné*/); else appel récursif } Traitement
Fonction récursive factorielle Soit la fonction Factorielle, effectuant lopération n! int fact (int n){ if (n < 0) throw logic_error( Fact:argument < 0 ); if (n = = 0) return 1; else return (n * fact (n-1)); } Règle récursive : fact(n) = n * fact (n-1) Condition darrêt : fact (0) =1 Condition de convergence: n 0
Pile des contextes La notion de récursivité très liée au concept des piles. A chaque appel dune fonction récursive, les arguments transmis par valeur et les variables locales sont empilés. Ces valeurs constituent le contexte dactivation de lappel. A chaque retour le contexte situé en haut de la pile est disponible Les contextes sont indépendants Ainsi, lorsqu'une fonction est appelée, ses paramètres formels, l'adresse de retour et les variables locales (automatiques) de la fonction appelée sont déposés sur la pile. Toutes ces données sont retirées de la pile à la fin de l'exécution de la fonction. Conséquemment, lors d'appels récursifs, les variables de la fonction sont empilées en cascade.
Exécution de factorielle(3) Retour au point appelant Pile vide (dépiler) Return (3 *2)N=3 (dépiler) => 1 Return (2 *1)N=2 (dépiler) => 2 Return (1*1)N=1 (dépiler) => 3 Return(1)N=04 (empiler) N*fact(0)N=13 (empiler) N*fact(1)N=22 (empiler) N*fact(2)N=31 (empiler) N° appelN° retourContexteAction
Avantages et inconvénients Solution naturelle et facile à concevoir si la fonction est récursive quand la structure de données traitée est récursive Avantages Formulation compacte, claire et élégante. Maîtrise des problèmes dont la nature même est récursive. Désavantages Possibilité de grande occupation de la mémoire. Temps d'exécution peut être plus long. Estimation difficile de la profondeur maximale de la récursivité.
Limitations: Fonction récursive Fibonnacci Soit la fonction Fibo (Fibonacci), effectuant lopération f(n) = f(n-1) + f(n-2) long fibo (long valeur) { if (valeur< 0) throw logic_error( Fibo:argument < 0 ); if (valeur <= 1) return valeur; else return(fibo(valeur-1) + fibo(valeur-2)); }
Exécution de Fibonnacci Soit les appels effectués pour fibo(n) : fibo(n) fibo(n-2) fibo(n-4) fibo(n-6)fibo(n-5) fibo(n-3) fibo(n-1) fibo(n-2)fibo(n-3) Règle 5: Ne jamais dupliquer le travail par la résolution dune même instance dun problème dans plusieurs appels récursifs. fibo(n-3) fibo(n-4)
Danger de Fibonnacci Note : Cette fonction récursive effectue plusieurs appels au même calcul. Par exemple pour déterminer f(n), on calcule dabord f(n-1), puis au retour de lappel, f(n-2) est calculé. Or, dans le calcul de f(n-1), f(n-2) est déjà calculé! Ce problème saggrave en descendant larborescence, puisque f(n-3) sera appelé à 3 reprises : chaque appel récursif entraînera de plus en plus dappel redondant. Règle 5: Ne jamais dupliquer le travail par la résolution dune même instance dun problème dans plusieurs appels récursifs.
Champs d'application des algorithmes récursifs Structures de données définies récursivement (listes, arbres, graphes, etc.) Équations de récurrence (algébriques, vectorielles, booléennes, formelles, ensemblistes, etc.); Rétroaction ("backtracking"). … Nous allons voir ensemble quelques exemples typiques sur la récursivité.
Les tours de Hanoï Problème : Soit n tours de tailles décroissantes sur un socle A, transférer les n tours sur le socle B en utilisant un socle intermédiaire C. Socle A Socle B Socle C Déplacement dune tour : on ne peut empiler quune tour de plus petite taille sur une autre tour
Paramétrage void Hanoi(int n, socle& A, socle& B, socle&C); n : nombre de tours A : socle de départ (valeur initiale : suite de n tours décroissantes) B : socle de darrivée (valeur initiale : vide) C : socle intermédiaire (valeur initiale : vide)
Les tours de Hanoï : résolution Socle A Socle BSocle C Socle A Socle B Socle C
RESOLUTION RECURSIVE Cas trivial (arrêt) n=1 : il suffit de déplacer lunique tour de A vers B Règle récursive n>1 : Hanoi (n-1, A,C, B) Déplacer (A,B) Hanoi (n-1, C, B, A)
FONCTION HANOI void Hanoi (int n, socle& A, socle& B, socle&C){ if (n = = 1) Déplacer (A,B); else { Hanoi (n-1,A,C,B); Déplacer (A,B); Hanoi (n-1,C,B,A); } } A : départB : arrivéeC: intermédiaire
ARBRE DES APPELS (n=3) Déplacer (A,B) Déplacer (A,C) Hanoi (3,A,B,C) Hanoi (2,A,C,B) Hanoi (1,B,C,A)Hanoi (1,A,B,C) Déplacer (A,B)Déplacer (B,C) Hanoi (2,C,B,A) Déplacer (C,A) Déplacer (C,B) Déplacer (A,B)
Recherche binaire (dichotomique) Idée générale : La recherche dune valeur particulière dans un tableau ordonné se fait par divisions successives du tableau en deux parties. Le fait que le tableau soit ordonné permet de déterminer rapidement la moitié dans laquelle se trouve lélément recherché.
Recherche binaire programmation non-récursive int RechercheBinaire( int * tab, int debut, int fin,int val) { int i = debut - 1; int j = fin + 1; while ( i+1 != j) { int milieu = (i+j)/2; if ( val < tab[milieu]) j = milieu; if (val == tab[milieu]) return milieu; if (val> tab[milieu]) i = milieu; } return -1; }
Fonction récursive pour la recherche binaire int rechercheBinaire(int * tab, int debut, int fin, int val) { int milieu; if (debut > fin) return -1; else { milieu= (debut + fin)/2; if( val == tab[milieu]) return milieu; else { if( val < tab[milieu]) return(rechercheBinaire(tab,val,debut, milieu-1)); else { return(rechercheBinaire(tab,val,milieu+1,fin)); }
Sous-séquence de somme maximale Définition: Étant donnée des entiers (possiblement négatifs) A 1, A 2,..., A N, trouver et identifier la séquence correspondante à la valeur maximale de. Résolution: Application de la technique de diviser et conquérir (divide-and-conquer), une Méthode de résolution de problèmes basée sur la récursivité: Diviser en plusieurs sous problèmes résolus récursivement. Conquérir, où la solution du problème original est composé de la solution des sous-problèmes.
Sous-séquence de somme maximale Cas possible: Cas 1: La séquence est situé dans la première moitié. Cas 2: La séquence est situé dans la deuxième moitié. Cas 3: La séquence commence dans la première moitié et se termine dans la deuxième moitié. Sommaire de lalgorithme: 1. 1.Déterminer récursivement la sous-séquence de somme maximale uniquement dans la première moitié Déterminer récursivement la sous-séquence de somme maximale uniquement dans la deuxième moitié Avec deux boucles consécutives, déterminer la sous-séquence de somme maximale qui débute dans la première moitié et qui termine dans la deuxième moitié Choisir la somme la plus élevée des trois.
SousSequence trancheMax4(Sequence t, int d, int f) { SousSequence rep = { 0, 0, t[0] }; int m = (d+f)/2; if(d==f){rep.debut=d; rep.fin=f; rep.somme= t[d]; return rep;} Sousequence sm1 = trancheMax4(t, d, m); Sousequence sm2 = trancheMax4(t, m+1, f); int s1=t[m]; int s1m=s1; int dmg=m; for(int k = m-1; k>=d; --k) s1+=t[k]; if(s1>s1m){s1m=s1; dmg=k; int s2=t[m+1]; int s2m=s2; int dmd=m+1; for(int k = m+2; k s2m){s2m=s2; dmd=k; int sm3 = s1m+s2m; if(sm1.somme>=sm2.somme && sm1.somme>=sm3){ return sm1;} if(sm2.somme>=sm1.somme && sm2.somme>=sm3){ return sm2;} rep.debut= dmg; rep.fin = dmd; rep.somme= sm3; return rep; }
Sous-séquence de somme maximale (Exemple) max(2,-5,2-5) 2 max(3,2,3+2) 3 0 max(-1,0,-1+0) 2 max(-1,2,-1+2) max(0,2,0+1) max(5,2,0+0 )=5
Algorithme à essais successifs (AES) Problématique Pour résoudre certains types de problèmes, il peut être souvent utile de procéder à l'exploration systématique des différentes solutions possibles. Il est donc essentiel de développer des techniques algorithmiques qui permettent une telle exploration. AES ou encore algorithme de back tracking idée : construire progressivement une solution à un problème donné en essayant toutes les possibilités à chaque étape de la construction. Remarque : la complexité de ce genre d algorithme est de nature exponentielle => à utiliser si l on n en connaît pas de meilleur.
Backtracking Exemple. Calcul du trajet du robot Il s'agit de concevoir un algorithme qui permet à un robot de trouver un chemin menant à la sortie d'un labyrinthe. Pour ce faire le robot explore systématiquement les quatre directions vers lesquelles il peut se déplacer. De plus, afin d'éviter que le robot ne se retrouve sur des cases déjà explorées chacune de celles-ci seront marquées à mesure qu'elles sont visitées. C'est de façon récursive que les cases faisant partie du chemin seront identifiées.
Algorithme du retour-arrière
Plan du site
Programmation du trajet dun robot mobile #define Y 9 /* Les dimensions du labyrinthe */ #define X 7 char laby[X][Y] = { {'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', 's', ' ', ' ', ' ', ' ', ' ', ' ', 'X'}, {'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X'}}; int main() { int i, j; bool Parcours(int x, int y); if (Parcours(1,1)) { // Imprimer le résultat de la recherche for (i = 0; i < X; i++, cout<<endl) for (j = 0; j < Y; j++) cout<<laby[i][j]; } else cout<<"Il n'existe aucun chemin.\n"; return 0; }
Programmation du trajet dun robot mobile (suite) bool Parcours(int x, int y) { if (laby[x][y] == 's') { /* on a trouvé la sortie */ laby[x][y] = 'S'; return true; } else if (laby[x][y] == ' ') { /* la case est libre */ laby[x][y] = 'o'; /* marquer la case visitée */ if (Parcours(x-1, y)) /* Parcours nord */ return(true); else if (Parcours(x, y+1)) /* Parcours est */ return(true); else if (Parcours(x+1, y)) /* Parcours sud */ return(true); else if (Parcours(x, y-1)) /* Parcours ouest */ return(true); else laby[x][y] = '-'; /* on laisse une marque */ } return false; }
Retour en arrière - exercice Quel est le chemin parcouru? Combien de retours en arrière? Utiliser des appels récursifs et lordre dappel suivant: Nord Sud Est Ouest A B S C D E F E G H I J
Retour en arrière - exercice A B S C D E F E G H I J bool Parcours(int x, int y) { if (laby[x][y] == 's') { laby[x][y] = 'S'; return true; } else if (laby[x][y] == ' ') { laby[x][y] = 'o'; if (Parcours(x-1, y)) return(true); else if (Parcours(x+1, y)) return(true); else if (Parcours(x, y+1)) return(true); else if (Parcours(x, y-1)) return(true); else laby[x][y] = '-'; } return false; }
Retour en arrière - réponse A B C D E F G H I J X E S oo o o ooX X o o o o o ooo o oX o oo o ooo o o o oo oX Xo o