Il y a de nombreuses applications du principe "Diviser pour régner" : dès que l'on peut résoudre un gros problème en le scindant en plus petits problèmes indépendants, il faut songer à l'utiliser !
Voila des exemples pour lesquelles vous combinerez l'utilisation de la méthode "diviser pour régner" et la récursivité, les deux étant très souvent liées !
Pour comparer la vitesse d'exécution d'algorithme Diviser pour Régner avec d'autres, vous pourrez éventuellement ( mais dans Pyzo seulement ) utiliser le module timeit.
Voila une première "mise en bouche" 😋 assez simple à comprendre qui va bien vous faire saisir l’intérêt de l'approche Diviser pour Régner.
Vous connaissez la syntaxe qui permet d'obtenir une chaîne de caractères contenant n fois le même caractère/la même suite de caractères :
>>> 'Ha'*8
'HaHaHaHaHaHaHaHaHaHa'
La manière naïve de construire une telle chaîne est de concaténer n fois le motif à la suite :
Une manière plus efficace serait cependant de procéder plutôt ainsi :
chaîne('Ha', 8) = 'HaHaHaHaHaHaHaHa' = 'HaHaHaHa' + 'HaHaHaHa' = chaîne('Ha', 4) + chaîne('Ha', 4)
chaîne('Ha', 4) = 'HaHaHaHa' = 'HaHa' + ''HaHa' = chaîne('Ha', 2) + chaîne('Ha', 2)
chaîne('Ha', 2) = 'HaHa' = 'Ha' + 'Ha' = chaîne('Ha', 1) + chaîne('Ha', 1)
chaîne('Ha', 1) = 'Ha'
: cas de base !chaîne(motif, n) = chaîne(motif, n//2) + chaine(motif, n//2)
On a donc remplacé n concaténations du motif par log2(n) concaténations de sous-chaînes.
En quoi diminuer le nombre de concaténations accélère-t-il les choses ? C'est lié à la manière dont Python ( entre autres langages ) gère les chaînes de caractères : ce sont des objets immuables, ce qui signifie qu'à chaque concaténation, Python crée en réalité un nouvel objet en recopiant les caractères de l'ancienne chaîne dans la nouvelle, ce qui prend évidemment du temps...Diminuer le nombre de concaténations améliore donc la vitesse d’exécution.
Il faut cependant prendre garde à la parité de n : si à un appel récursif donné n est impair ( ce qui arrivera forcément si la valeur de n lors de l'appel initial de la fonction n'est pas une puissance de 2 ), il faut rajouter alors un motif supplémentaire à la concaténation des deux sous-chaînes; on distinguera donc deux cas généraux dans la récursion, l'un pour n pair, l'autre pour n impair.
Écrire une fonction chaine_multiple()
qui prend en paramètre un motif motif et un entier n, et renvoie la chaîne motif*n, en utilisant l'approche Diviser pour Régner.
Ou comment calculer efficacement la puissance d'un nombre ( xn ) ?
La puissance n d'un nombre x est définie par : xn = x . x . x ........ . x ( n fois )
Après avoir établi le cas général de la récursivité et son cas de base, écrire une fonction récursive puissance_naive(x, n)
qui calcule la puissance n d'un nombre x.
On peut faire mieux en adoptant une approche diviser pour régner.
Dans un premier temps, supposons pour simplifier que la puissance n est une puissance de 2 (1, 2, 4, 8, 16, ...).
Par exemple :
→ autrement dit, on remplace n multiplications de x par lui-même par log2n élévations au carré de de x, d'où l'énorme gain en complexité quand n devient grand !
Cependant, si n n'est pas une puissance de 2, la division par 2 de n donnera un nombre impair à certains niveaux de la récursion; il faudra donc considérer 2 situations pour le cas général :
Écrire une fonction récursive expo_rapide(x, n)
qui calcule la valeur de xn avec l'approche diviser pour régner.
Bien réfléchir au cas de base de la récursion...
Le tri rapide (en Anglais : quicksort ) d'un tableau ou d'une liste est peut-être l'algorithme de tri le plus utilisé. Il est relativement simple à comprendre et à implémenter, et, comme son nom l'indique, très rapide.
L'idée de l'algorithme, qui utilise l'approche divise pour régner, est très simple :
Si l'étape numéro 2 ci-dessus peut se faire avec une complexité O(n) et si le nombre d'éléments de la partie gauche et le nombre d'éléments de la partie droite ainsi obtenues ne sont pas trop différents, alors, la complexité de l'algorithme est O(n.log n).
Pour que les deux parties "gauche" et "droite" soient de tailles similaires, le pivot doit être proche de l'élément médian du tableau ou de la liste. Malheureusement, un algorithme optimal pour calculer l'élément médian est assez coûteux en temps...On choisira donc le pivot soit comme un élément donné du tableau ( le premier ou le dernier ), ou alors de manière complètement aléatoire.
Faire la trace de l'algorithme avec la liste suivante : [4, 7, 3, 6, 2, 1, 5]
en prenant à chaque fois le premier élément comme pivot
fonction tri-rapide(L: liste):
si L n'a plus qu'1 ou 0 élément
renvoyer L
sinon :
choisir le pivot
gauche <- liste des éléments de L plus petits ou égaux au pivot, le pivot exclus
droite <- liste des éléments de L plus grands que le pivot ( idem )
gauche <- tri_rapide(gauche)
droite <- tri_rapide(droite)
renvoyer gauche + pivot + droite
fin si
On notera qu'il n'y pas besoin de "fusionner" deux listes comme dans le tri-fusion, simplement d'une concaténation des parties "gauche" et "droite" ( sans oublier d'intercaler au milieu le pivot pour ne pas "perdre" cet élément ).
Écrire une fonction récursive implémentant le tri rapide d'une liste, en choisissant le pivot :
A la place des "tranches" de liste hors programme, penser à utiliser plutôt des listes par compréhension.
L'approche "diviser pour régner" est-elle toujours judicieuse à utiliser ? Faut-il penser qu'elle améliore toujours la complexité des algorithmes ?
Les deux exemples suivants vont vous montrer que la réponse est à nuancer...
Le problème est de déterminer la valeur du maximum ou du minimum d'une liste de nombres.
En adoptant le paradigme "diviser pour régner", l'idée pour résoudre cette question est de calculer récursivement le maximum de la première moitié de la liste et celui de la seconde,
puis de les comparer. Le plus grand des deux sera le maximum de toute la liste.
La condition d'arrêt à la récursivité sera l'obtention d'une liste à un seul élément, son maximum étant bien sûr la valeur de cet élément.
si la liste est réduite à un seul élément: # Régner
renvoyer cet élément
sinon:
max_gauche ← rechercher le maximum dans la partie gauche du tableau # Diviser
max_droit ← rechercher le maximum dans la partie droite du tableau
renvoyer le plus grand entre max_gauche et max_droit # Combiner
maximum()
qui renvoie le maximum d'une liste ou d'un tableau en utilisant le principe Diviser pour Régner.minimum()
.En réalité, l'approche Diviser pour Régner devient pertinente dans le cas de la recherche simultanée du maximum et du minimum dans la liste, car cela revient à faire beaucoup moins de comparaisons que si
on recherchait ces deux valeurs l'une après l'autre.
min_et_max
qui renvoie simultanément le maximum et le minimum d'une liste ( par exemple sous forme d'un
tuple (minimum, maximum) ) en utilisant l'approche diviser pour régner.[1, 3, 5, 2, 9]
, montrer que l'on fait effectivement moins de comparaisons en recherchant le maximum et le minimum en même temps en utilisant une approche Diviser
pour Régner.timeit
pour comparer la durée des recherches successive et simultanée du maximum et du minimum d'une liste.La recherche dichotomique d'une seule valeur dans une liste triée n'est pas intéressante, car il faut ( forcément...) avoir trié au préalable la liste; or, un théorème énonce qu'il n'y a aucun
algorithme de tri de complexité meilleure que O(n.lg n), complexité qui se "rajoute" donc à celle de la fonction de recherche dichotomique même.
N'est-il alors pas possible de s'affranchir de cette étape de tri et faire la recherche dans une liste non-triée ?
En adoptant le paradigme "diviser pour régner", l'idée serait alors de rechercher récursivement l'élément dans la première moitié de la liste et dans la seconde, puis de combiner les résultats avec l'opérateur logique OU.
La condition d'arrêt ( cas de base ) de la récursivité sera l'obtention d'une liste à un seul élément, car il est alors immédiat de conclure si l'élément recherché appartient à une telle liste ou non.
La fonction récursive implémentant cette relation de récurrence réalisera donc deux appels à elle-même :
fonction recherche(liste, valeur)
si liste n'a qu'un seul élément : # Régner
si il est égal à valeur:
renvoyer Vrai
sinon :
renvoyer Faux
fin si
fin si
gauche = moitié gauche de liste # Diviser
droite = moitié droite de liste
renvoyer recherche(gauche, valeur) OU recherche(droite, valeur) # Combiner
Écrire une fonction recherche()
récursive qui :
True
si n est dans L, False
sinon.