Programmation Fonctionnelle Avancée

Cours 11

kn@lmf.cnrs.fr
https://usr.lmf.cnrs.fr/~kn

Problèmes de recherche

De nombreux problèmes algorithmiques peuvent être vus comme de problèmes de recherche d'une « solution » parmi un espace de valeur potentiellement grand

Trop de solutions ?

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 :

Retour arrière

Retour arrière

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.

Retour arrière (2)

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

Étude de cas 1 : problème des N reines

É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.

Méthode générale

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

Illustration (1/2)

Illustration (2/2)

On écrit le code maintenant

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 :

Mémoization

Mémoization

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

Fibonacci (encore)

let rec fib n = if n <= 1 then n else fib (n-1) + fib (n-2)

Mémoization

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:

  1. Si la paire x, r est dans le dictionnaire, renvoyer r
  2. Sinon calculer r = f x
  3. Stocker (x, r) dans le dictionnaire
  4. Renvoyer r

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).

Mémoization en OCaml

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

Activité (2)

Exemples d'application:

Compter les chemins d'une grille

Le plus dur est de voir comment énumérer tous les chemins