Diviser pour régner
Cours · algorithmique (école d'ingénieur), chapitre 13 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun
Objectifs du chapitre
- Reconnaître et appliquer le schéma diviser, résoudre récursivement, combiner.
- Écrire des fonctions récursives justifiées par une base et une hérédité, sur des intervalles .
- Établir l'équation du temps de calcul et la résoudre par un arbre des appels.
- Connaître quelques algorithmes célèbres : multiplication de Karatsuba, sélection du -ème élément, exponentiation rapide.
13.1 Le principe
La programmation dynamique résout un problème à partir de tous ses sous-problèmes, qui se recouvrent et sont mémorisés dans un tableau. La méthode diviser pour régner découpe au contraire le problème en quelques sous-problèmes indépendants, de tailles nettement plus petites (souvent la moitié), les résout récursivement, et combine leurs solutions.
Méthode — diviser pour régner
- Base : si le problème est assez petit (vide, une seule valeur…), le résoudre directement.
- Diviser : découper le problème de taille en sous-problèmes de taille (le plus souvent ).
- Régner : résoudre chaque sous-problème par un appel récursif. On suppose ces appels justes : c'est l'hypothèse de récurrence.
- Combiner : construire la solution du problème à partir des solutions des sous-problèmes.
La justification est une récurrence sur la taille du problème ; le temps de calcul est donné par une équation de récurrence (section 13.3).
13.2 Premiers exemples sur les intervalles
13.2.1 Afficher un intervalle
Écrivons une fonction récursive qui affiche les valeurs de l'intervalle dans l'ordre croissant. On coupe l'intervalle en son milieu : , et toutes les valeurs de sont inférieures à , elles-mêmes inférieures à celles de .
static void arc(int i, int j) { // affiche [i:j] dans l'ordre croissant
if (j - i <= 0) return; // [i:j] vide : rien à afficher
int k = (i + j) / 2; // i <= k < j
arc(i, k); // [i:k] affiché croissant
System.out.print(k + " "); // puis k
arc(k + 1, j); // puis [k+1:j] : [i:j] affiché croissant
}
static void ard(int i, int j) { // affiche [i:j] dans l'ordre décroissant
if (j - i <= 0) return;
int k = (i + j) / 2;
ard(k + 1, j); // [k+1:j] affiché décroissant
System.out.print(k + " "); // puis k
ard(i, k); // puis [i:k]
}
arc(0,8) : 0 1 2 3 4 5 6 7
ard(0,8) : 7 6 5 4 3 2 1 0
Les deux appels récursifs portent sur des intervalles strictement plus petits que (car ) : la récursivité se termine. Pour l'ordre décroissant, il suffit d'inverser l'ordre des deux appels. C'est exactement le schéma des parcours grd et drg d'un arbre binaire de recherche (chapitre 9).
13.2.2 Somme par moitiés
static int somme(int[] T, int i, int j) { // retourne la somme de T[i:j]
if (j - i == 0) return 0;
if (j - i == 1) return T[i];
int k = (i + j) / 2;
return somme(T, i, k) + somme(T, k, j);
}
Ce découpage n'apporte rien par rapport à une boucle (les deux sont en ), mais il a deux vertus : il se parallélise naturellement (les deux moitiés peuvent être calculées par deux processeurs), et, en calcul flottant, il accumule moins d'erreurs d'arrondi qu'une somme séquentielle.
13.2.3 Arbre binaire de recherche de hauteur minimum
Au chapitre 9 nous avons construit un arbre binaire de recherche de hauteur minimum sur les valeurs : la racine est la médiane , les sous-arbres gauche et droit sont des arbres de hauteur minimum sur et . C'est un « diviser pour régner » en cinq lignes :
static AB abrhm(int i, int j) {
if (j - i <= 0) return null; // [i:j] vide : arbre vide
int k = (i + j) / 2; // valeur médiane de [i:j]
return new AB(k, abrhm(i, k), abrhm(k + 1, j));
}
13.3 Temps de calcul d'un algorithme récursif
Notons le temps de calcul dans le pire cas sur un problème de taille . Pour un algorithme qui découpe en sous-problèmes de taille et dont la division et la combinaison coûtent :
13.3.1 L'arbre des appels
Prenons (deux moitiés, combinaison linéaire : c'est le cas du tri par fusion, chapitre 14). Dessinons l'arbre des appels, en notant dans chaque nœud le coût de la combinaison :
Au niveau il y a appels, chacun sur un problème de taille : le coût total du niveau est . La taille est divisée par à chaque niveau, il y a donc niveaux, et
Avec le même raisonnement :
- (dichotomie, chapitre 12) : niveaux de coût , donc ;
- (somme par moitiés) : les niveaux coûtent , au total moins de , donc ;
- : le niveau coûte ; le dernier niveau domine avec feuilles : .
La suite du cours est sur Adloun
Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.
Lire la suite du coursEn accès libre, sans inscription.