De nombreux problèmes algorithmiques peuvent être vus comme de problèmes de recherche d'une « solution » parmi un espace de valeur potentiellement grand
Pour tout ces problèmes, énumérer de façon exhaustive toutes les configurations possibles, puis vérifier pour chaque configuration si celle-ci est une solution est trop coûteux. Il y a deux techniques complémentaires :
La technique de recherche avec retour arrière (backtracking) consiste à partir d'un du problème initial et à s'en rapprocher récursivement (en faisant diminuer une certaine mesure, ce qui va assurer que l'on termine).
Pour se « rapprocher », on essaye toutes les façons d'avancer « en un coup » vers la solution
Pour chacune d'elle, si c'est un morceaux de solution valide, alors on s'appelle récursivement sur la nouvelle configuration, sinon on passe à la suivante sans explorer plus ce début de solution.
La recherche avec retour arrière consiste à parcourir l'arbre de toutes les configurations possibles en profondeur. Cependant, lorsque l'on détecte que la configuration courante ne peut déjà pas mener à une solution, on évite de parcourir le sous-arbre correspondant.
On illustre cette technique avec deux problèmes différents
Étant donné un échiquier de taille N×N, comment placer dessus N reines de façon à ce qu'aucune ne soit en prise.
Rappel: aux échecs, une reine peut capturer à n'importe quelle distance en ligne, en colonne et en diagonale.
On utilise l'approche récursive suivante. Pour placer N reines
La ligne ** effectue le retour arrière.
La ligne * est un endroit où on pourra mettre une optimisation spécifique
On réfléchit ensemble à l'implémentation.
On finira en TP et on verra d'autres variations (compter les solutions, tirer aléatoirement une solution, …).
Comme toujours :
Terme inventé en 1968 Donald Michie. En programmation, cela consiste à optimiser des calculs en se souvenant de résultats partiels.
Cette technique peut s'appliquer à toute fonction
Pas uniquement pour la résolution de problème, c'est une technique générale
let rec fib n =
if n <= 1 then n
else
fib (n-1) + fib (n-2)
fib 6 = fib 5 + fib 4
= (fib 4 + fib 3) + (fib 3 + fib 2)
= ((fib 3 + fib 2) + fib 3) + (fib 3 + fib 2)
La mémoization est une technique générale qui ne se soucie pas de la fonction mémoizée. Pour une fonction f x , on définit f_memo x munie d'un dictionnaire global:
Si on se débrouille bien, l'étape 1 coûte moins que calculer f x. On va gagner si on rappelle souvent f_memo sur le même argument (si tous les appels sont uniques, on fait du travail pour rien et f_memo est plus lente.
Par contre, on stocke des données: on gagne en temps de calcul mais on perd en espace mémoire (souvent c'est un compromis acceptable).
Une façon simple est d'utiliser le module Hashtbl d'OCaml :
let f x =
... corps de f ...
devient
let f_memo =
let memo = Hashtbl.create 16 in (* table de hachage *)
let f x =
match Hashtbl.find_opt memo x with
Some r -> r
| None ->
let r = ... corps de f ... in
Hashtbl.add memo x r;
r
in f
f peut être récursive, ça ne change rien
Exemples d'application:
Le plus dur est de voir comment énumérer tous les chemins