Adloun

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 :

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 cours

En accès libre, sans inscription.

Exercices corrigés de ce chapitre

Lire la suite du cours →