Méthode « diviser pour régner »

Numérique et sciences informatiques

donnees

En première, nous avons vu deux algorithmes :

  • le tri par sélection qui a une complexité quadratique dans le pire des cas, le meilleur des cas et en moyenne.
  • le tri par insertion qui a une complexité linéaire dans le meilleur des cas, et quadratique dans le pire des cas et en moyenne.

Ces algorithmes voient peu voire pas du tout d'utilisation en pratique en raison de leur faible efficacité. Nous allons découvrir une méthode de tri plus performante.
Euclide a introduit la méthode du "Diviser pour Régner" au IIIe siècle avant J-C. C'est une approche algorithmique où un problème complexe est subdivisé en de multiples sous-problèmes.
Le procédé Diviser pour régner est un cas particulier de la récursivité, où la taille du problème est divisée à chaque appel récursif plutôt que seulement décrémenté d'une unité.

La méthode "Diviser pour régner" :

La méthode "Diviser pour régner" consiste, pour résoudre un problème complexe de taille N, à :

  • (Diviser) : diviser le problème en sous-problèmes (par exemple, de taille N/2)
  • (Régner) : résoudre ces sous-problèmes distincts (généralement de manière récursive)
  • (Combiner) : fusionner les solutions pour obtenir la solution du problème initial

En général, cette méthode nécessite moins d'appels récursifs. Parfois, elle aboutit à un algorithme de résolution plus rapide pour certains cas.

L'idée fondamentale est que les "petits problèmes" sont souvent plus simples à résoudre que le problème initial. Une fois ces derniers résolus, leurs solutions combinées permettent d'obtenir la solution du problème d'origine.

Exemple

Pour illustrer la méthode "Diviser pour régner" avec le calcul de 2^6, on décompose le problème en deux parties égales : 2^3 et 2^3. Ensuite, chaque partie est fractionnée en deux parties égales : 2^2 et 2^1, et résolue récursivement. Enfin, les résultats sont combinés pour obtenir le résultat final.

Voici en détail les étapes pour calculer 2^6 à l'aide de cette méthode :

  • Diviser le problème en deux parties égales : 2^3 et 2^3.
  • Diviser chaque partie en deux parties égales : 2^2 et 2^1.
  • Résoudre chaque partie de manière récursive :
    • 2^1 = 2
    • 2^2 = 2 * 2 = 4
    • 2^3 = 2^2 * 2^1 = 4 * 2 = 8
  • Combiner les résultats pour obtenir le résultat final :
    • 2^6 = 2^3 * 2^3 = 8 * 8 = 64

Ainsi, la méthode "Diviser pour régner" permet une résolution efficace en subdivisant le problème en sous-problèmes plus simples et en consolidant les résultats. Cette approche s'avère précieuse pour aborder des problèmes complexes comme le calcul de puissances élevées.

Implémentation en Python du “diviser pour régner”

Nous allons écrire une fonction “puissance(x,n)” basée sur ce paradigme, où x et n sont deux entiers (positif pour n). Pour cela, nous allons prendre en compte que:

  • si n=0, alors xn = 1.
  • si n est pair, alors x n = (xn/2)2
  • si n est impair, alors x n = x * (xn/2)2

Cela donne alors la fonction suivante (exponentiation rapide):

def expo_rapide(x,n):
	"""
    Renvoie xn
    :param:   n (int) et x (int)
    :return: un entier
    :CU: n>=0
    """
	if n == 0:
		return 1
	elif n % 2 == 0:
		return expo_rapide(x * x,n // 2)
	else:
		return x*expo_rapide(x * x, n // 2)

Entraînement 1 :

Déterminer le coût, en nombre de multiplications, de l’algorithme corrigé pour n = 5, 10, 20, 50 et 100. Vous pouvez modifier la fonction expo_rapide(x,n) pour vérifier vos résultats.

n = 5 10 20 50 100
nombre d'opération 5 10 20 50 100
nombre d'opération avec expo_rapide(x,n)

Le tri fusion

Le tri fusion découvert par John von Neumann en 1945 consiste à trier récursivement les deux moitiés de la liste, puis à fusionner ces deux sous-listes triées en une seule. La condition d’arrêt à la récursivité sera l’obtention d'une liste à un seul élément, car une telle liste est évidemment déjà triée.

Merge-sort-example-300px
Swfung8, CC BY-SA 3.0, via Wikimedia Commons

Voici donc les trois étapes (diviser, régner et combiner) de cet algorithme :

  1. Diviser la liste en deux sous-listes de même taille (à un élément près) en la "coupant" par la moitié.
  2. Trier récursivement chacune de ces deux sous-listes. Arrêter la récursion lorsque les listes n'ont plus qu'un seul élément.
  3. Fusionner les deux sous-listes triées en une seule.

On considère la liste suivante de sept entiers :

On la scinde en deux sous-listes en la coupant par la moitié :

Sous-listes que l’on scinde à leur tour :

Sous-listes que l’on scinde à leur tour :

Ces sous-listes sont triées car elles n’ont qu’un élément. On va maintenant les fusionner deux par deux en de nouvelles sous-listes triées :

De nouveau une étape de fusionnement :

Une dernière fusion :

On a fusionné toutes les sous-listes obtenues lors des appels récursifs, le tri est terminé :

Regardons l'algorithme de fusion de deux tableaux triés.

FONCTION fusion (tab1, tab2)
    // tab1 et tab2 sont deux tableaux triés    
    // Initialisation
    pos1 <- 0   // marqueur de position du 1er tableau
    pos2 <- 0   // marqueur position du 2ème tableau
    resultat <- []   // liste vide destinée à accueillir les éléments triés
    // Boucle
    TANT QUE l'on a pas atteint la fin d'un des tableaux
        SI tab1[pos1] <= tab2[pos2] ALORS
            Insérer tab1[pos1] à la fin de resultat
            incrémenter pos1
        SINON
            Insérer tab2[pos2] à la fin de resultat
            incrémenter pos2
        FIN SI
    FIN TANT QUE    
    // Finalisation
    Insérer les éléments restants du tableau non vide à la fin de resultat    
    RENVOYER resultat

Et voici l'algorithme récursif de tri fusion qui utilise la fonction fusion définie précédemment..

FONCTION tri_fusion (tab)
    n <- Longueur de tab
    // Cas de base
    SI n == 1 ALORS
        RENVOYER tab
    FIN SI
    // Recursion sur les deux demi-tableaux
    SINON
    # Diviser
    	t1 <- tab[:n//2]
    	t2 <- (tab[n//2:]
    # Régner    
    	t1_tri <- tri_fusion(tab[:n//2]
    	t2_tri <- tri_fusion(tab[n//2:]
    # Combiner
        resultat <- fusion(t1_trie, t2_trie)
    // Renvoi des la fusion des deux tableaux
    	RENVOYER resultat
La complexité du tri fusion est T(n)=Θ(n log2(n)).
Il s'agit de la meilleure complexité que puisse avoir un algorithme de tri.

Entraînement 2 :

  1. Réaliser le schéma en arborescence permettant le tri du tableau suivant [19,4,7,6,9,8,10,2,1,3] avec l’algorithme de tri fusion («diviser pour régner»)2.
  2. Quel est l’avantage de l’algorithme de tri fusion par rapport aux algorithmes de tri vus en classe de 1ère ?
  3. A partir des algorithmes, implémenter un programme en python afin de tester le tableau de la question 1 (Ne pas oublier de commenter et d'ajouter le docstring au code).

Comparons avec les tris connus:

Nom Complexité Coût en opération pour un tableau de 10^8 valeurs
(assez fréquent dans les grandes entreprises)
Durée pour un ordinateur cadencé à 3GHz nécessitant 10 cycles pour une comparaison
Tri par sélection n^2 (10^8)2 = 1016 approx 381 jours
Tri par insertion n2 (108)2 = 1016 approx 381 j
Tri Fusion n*log2(n) (108)*log2(108) = 2,7.109 9s

Entraînement 3 :Recherche dichotomique d'un tableau trié

Nous avons vu en première la recherche dichotomique d'un tableau trié de façon itérative. Nous allons voir cet algorithme de recherche de façon récursive.

On cherche l'élément element dans le tableau T . La fonction rech_dicho(element, tableau) créée devra renvoyer True si l'élément cherché est dans le tableau, False sinon.

L'idée de cet algorithme récursif est la suivante:
Si l'élément du milieu du tableau T est égal à el alors on retourne True Sinon on effectue la recherche sur le sous-tableau de gauche ou de droite.

  1. Quel sera le cas d'arrêt de cet algorithme ?
  2. Implémenter cet algorithme de façon récursive.
  3. Écrire une fonction rech_dicho_position(element, tableau) récursive de la recherche dichotomique qui renvoie l'indice si ce dernier est dans le tableau, False sinon.


Savoir faire

  • Savoir écrire un algorithme utilisant la méthode « diviser pour régner ».

Fiche de cours