Backtracking
============

Dans cette série d'exercices, vous allez programmer des algorithmes destinés à résoudre des problèmes de satisfaction de contraintes :

* La consistance de noeuds et d'arcs.
* L'algorithme de recherche par backtracking.

Dans la série suivante, vous appliquerez ces algorithmes au jeu du Sudoku.

Modules squelettes
------------------

Les modules ``moteur_psc`` suivants contiennent le squelette du programme que nous allons développer. Le module ``exemple_backtracking.py`` permettra de le tester. 

:download:`backtracking.zip <../../squelettes/backtracking.zip>`

Module ``.../moteur_psc/variable.py`` :

.. include::  ../../squelettes/backtracking/moteur_psc/variable.py
    :code:

Module ``.../moteur_psc/contrainte.py`` :

.. include::  ../../squelettes/backtracking/moteur_psc/contrainte.py
    :code:

Module ``.../moteur_psc/moteur_psc.py`` :

.. include::  ../../squelettes/backtracking/moteur_psc/psc.py
    :code:

Module ``.../exemple_backtracking.py`` :

.. include::  ../../squelettes/backtracking/exemple_backtracking.py
    :code:

Exercice 1 : Consistance des noeuds et des arcs
-------------------------------------------------

Les modules  ``variable.py`` et ``contrainte.py`` contiennent les classes ``Variable`` et ``Contrainte``, ainsi que les sous-classes de cette dernière, ``ContrainteUnaire`` et ``ContrainteBinaire``. Le module ``psc.py`` implémente la classe ``PSC`` - une librairie pour la gestion d'un ensemble de variables et de contraintes, ainsi que pour la résolution d'un système de contraintes.

Comme vous pouvez le constater, les fonctions ``consistance_noeuds`` et ``consistance_arcs`` la classe ``PSC`` ne sont pas implémentées. Vous pouvez commencer par compléter ces fonctions, en suivant les indications suivantes :

* La fonction ``consistance_noeuds`` doit appeler la méthode ``est_valide`` de la classe ``ContrainteUnaire``, que vous devez aussi compléter ;
* La fonction ``consistance_arcs`` doit appeler la méthode ``reviser`` de la classe ``ContrainteBinaire``, dans laquelle vous devez implémenter l'algorithme de Waltz (consistance d'arcs). Comme les contraintes binaires sont bidirectionnelles, vous devrez implémenter la fonction ``reviser`` de telle sorte que les domaines des deux variables de la contrainte soient réduits (si possible) par l'appel à ``reviser`` ;
* À son tour, la méthode ``reviser`` s'appuie sur les méthodes ``est_valide`` et ``est_possible`` de la classe ``ContrainteBinaire``, que vous devez aussi compléter.

Exercice 2 : Algorithme du backtracking
---------------------------------------

Le Backtracking est un algorithme de recherche en profondeur d'abord avec les charactéristiques suivantes :

*  Une noeud de recherche est une instanciation de variables :math:`x_{1} = v_{1}`, :math:`x_{2} = v_{2}`, ..., :math:`x_{k} = v_{k}` (où :math:`k` est la profondeur du noeud dans l'arbre de recherche) ;
*  La fonction de successeur ajoute une nouvelle instanciation :math:`x_{k+1} = v_{k+1}` de manière à respecter toutes les contraintes pour les variables :math:`x_{1}`, ..., :math:`x_{k}` ;
*  Le noeud initial est une instanciation vide ;
*  Un noeud but consiste en une instanciation de toutes les variables :math:`x_{1}`, ..., :math:`x_{n}`.

L'algorithme de recherche, dont nous vous donnons le pseudo-code ci-dessous, doit ainsi être implémenté dans la méthode ``backtracking`` de la classe ``PSC`` :

::

    solutions <- []
    variables <- [v1, v2, ..., vn]

    Backtracking(k, une_seule_solution)
    1.  IF k >= n THEN
    2.      IF NOT une_seule_solution THEN
    3.          ajouter la solution actuelle à solutions
    4.      ELSE
    5.          RETURN solutions = [solution actuelle]
    6.      END IF
    7.  ELSE
    8.      v <- variables[k]
    9.      FOR EACH valeur de domaine d de la variable v DO
    10.         assigner la valeur d à la variable v
    11.         vérifier la consistance de v=d avec les variables précédentes
    12.         IF v=d est consistant THEN
    13.             reste <- Backtracking(k+1, une_seule_solution)
    14.             IF reste != échec THEN
    15.                 RETURN reste
    16.             END IF  
    17.         END IF
    18.     END FOR
    19. END IF
    20. RETURN échec
    END Backtracking

``backtracking`` prend deux paramètres :

*  ``k`` : la profondeur courante (commence à 0) ;
*  ``une_seule_solution`` : si vrai, alors retourne la première solution trouvée, sinon retourne toutes les solutions possibles.

Les étapes 11 et 12 de l'algorithme ci-dessus seront implémentées à l'aide de la fonction ``consistance_avec_vars_precedentes`` de la classe ``PSC``, que vous devez aussi compléter.

Les solutions seront stockées dans la variable de classe ``self.solutions``, chacune étant représentée par un dictionnaire qui associera le nom de la variable à sa valeur. Comme ces solutions sont conservées dans un champ de la classe, il n'est donc pas indispensable de les retourner. En outre, au lieu de retourner une valeur spécial en cas d'échec, la méthode ``backtracking`` peut se terminer simplement sans valeur de retour.

Test du programme
-----------------

Une fois que vous avez terminé, vous pouvez tester votre implémentation sur le module ``exemple_backtracking.py`` :

::

    python3 exemple_backtracking.py

Exercice 3 : Permutations [OPTIONNEL]
-------------------------------------

S'il vous reste du temps, vous pouvez utiliser l'algorithme du backtrack pour afficher toutes les permutations possibles des entiers de :math:`0` à :math:`N`. Par permutation, nous entendons une solution dont les clés seront des indices et les valeurs, des entiers de :math:`0` à :math:`N`. Chacune de ces solutions doit contenir chaque valeur une fois et une fois seulement. Vous devrez donc définir un système de contraintes pour vous assurer que cette condition est respectée. Testez votre implémentation pour plusieurs valeurs de :math:`N`. (Ne choisissez pas des valeurs trop élevées pour :math:`N`, faute de quoi l'algorithme risque de tourner pendant des heures.)

Essayez ensuite d'imaginer d'autres contraintes pour restreindre l'ensemble des solutions et implémentez-les. Par exemple, on peut exiger que deux valeurs consécutives ne soient jamais toutes deux paires ou toutes deux impaires.
