Résolution de problèmes par recherche

Dans cette série d’exercices, vous allez vous familiariser avec les trois principaux algorithmes de recherche :

  • En profondeur d’abord (DFS) ;
  • En largeur d’abord (BFS) ;
  • Par A^{\star}.

À titre d’exemple, vous les utiliserez pour découvrir un chemin entre deux villes étant donné leurs positions géographiques et les routes qui les connectent.

Modules squelettes

Les modules qui suivent constituent le squelette des programmes que vous allez implémenter. Les deux derniers modules, exemple_carte_simple.py et exemple_carte_suisse.py, permettront de les tester.

recherche.zip

Module .../moteurs_recherche/element.py :

class Element:
    """ Un élément dans l'espace de recherche. """

    def __init__(self, nom=''):
        """
            :param str nom: nom de l'élément (optionnel).
        """

        print('à compléter')

    def distance(self, element):
        """ Retourne la distance entre l'éléments courant et un autre.

            (A implémenter différemment dans les sous-classes, si nécessaire.)
        """

        return 1

    def __eq__(self, autre):
        print('à compléter')

    def __hash__(self):
        return hash(str(self))

    def __repr__(self):
        return '{}'.format(self.nom)

Module .../moteurs_recherche/ville.py :

from math import sqrt
from .element import Element

class Ville(Element):
    """ Un élément représentant une ville.
    """

    def __init__(self, x, y, nom=''):
        """
            :param x: coordonnée en x de la ville.
            :param y: coordonnée en y de la ville.
            :param str nom: nom de la ville.
        """

        Element.__init__(self, nom)
        print('à compléter')

    def distance(self, ville):
        """ Distance euclidienne entre la ville courante et la ville ``ville``. """

        print('à compléter')

    def __eq__(self, autre):
        print('à compléter')

    def __hash__(self):
        return hash(str(self))

    def __repr__(self):
        return '{}({}, {})'.format(self.nom, self.x, self.y)

Module .../moteurs_recherche/espace.py :

from copy import copy

class Espace:
    """ Un espace de recherche dans lequel les éléments sont connectés par des arcs. """

    def __init__(self, elements=None, arcs=None):
        """
            :param list elements: une liste d'éléments.
            :param list arcs: une liste d'arcs sous forme de tuples\
            ``(element_1, element_2)``.
        """

        self.elements = []
        if elements is not None:
            self.elements = sorted(self.elements, key=lambda e: e.nom)

        self.arcs = []
        if arcs is not None:
            self.ajoute_arcs(arcs)

    def ajoute_arcs(self, arcs):
        """
            Ajoute des arcs à l'espace.

            Les éventuels éléments nouveaux mentionnés dans\
            les arcs seront aussi ajoutés à la liste d'éléments
            de l'espace.

            :param list arcs: une liste d'arcs.
        """

        print('à compléter')

    def trouve_voisins(self, element):
        """
            :return: les voisins d'un élément.
        """

        print('à compléter')

    def __repr__(self):
        rep = ''
        for element in self.elements:
            rep += '{}, '.format(element)
            rep += 'avec voisins: '
            voisins = self.trouve_voisins(element)
            rep += ', '.join(map(str, voisins))
            rep +='\n'

        return rep

Module .../moteurs_recherche/noeud.py :

from math import sqrt

class Noeud:
    """ Noeud généré de façon dynamique par un algorithme de recherche,\
        encapsulant un élément de l'espace de recherche.
    """

    def __init__(self, element, parent=None, cout=0, cout_f=0):
        """
            :param Element element: l'élément encapsulé.
            :param Noeud parent: le noeud parent du noeud courant, c'est-à-dire\
            le noeud qui a conduit à la découverte du noeud courant au cours de la\
            recherche.
            :param cout: coût du noeud courant (utile uniquement pour A\*).
            :param cout_f: coût heuristique du noeud courant (utile uniquement\
            pour A\*).
        """

        print('à compléter')

    def __repr__(self):
        rep = '<{}, {}, {}>'.format(self.element,
                                   round(self.cout),
                                   round(self.cout_f))
        return rep

Module .../moteurs_recherche/recherche.py :

from .noeud import Noeud

class Recherche:
    """ Classe générique pour la recherche. """

    echec = 'échec'

    def __init__(self, espace, optimisee=False):
        """
            :param espace: l'espace de recherche.
            :param optimiser: indique si la recherche doit être optimisée pour éviter\
            les cycles.
        """

        self.espace = espace
        self.optimisee = optimisee

    def recherche(self, depart, but):
        """ Recherche un chemin allant de ``depart`` jusqu'à ``but``.

            :param depart: l'élément de départ.
            :param but: le l'élément but.

            :return: le chemin de ``depart `` à ``but``, ou ``'échec'``\
            s'il n'existe pas de chemin.
        """

        # L'heuristique à utiliser (utile uniquement pour A\*).
        self.h = lambda e: e.distance(but)

        noeud_depart = Noeud(depart, None, 0, self.h(depart))
        noeud_but = Noeud(but)

        return self.recherche_chemin(noeud_depart, noeud_but)

    def recherche_chemin(self, noeud_depart, noeud_but):
        """ Recherche un chemin allant de ``depart`` jusqu'à ``but``.

            :param noeud_depart: le noeud de départ.
            :param noeud_but: le noeud but.

            :return: les éléments encapsulés au long du chemin,\
            ou ``'échec'`` s'il n'existe pas de chemin.
        """

        print('à compléter')

    def trouve_chemin(self, noeud):
        chemin = []
        while noeud is not None:
            chemin.insert(0, noeud.element)
            noeud = noeud.parent

        return chemin

    def detecte_cycle(self, trace, noeud):
        """ Vérifie si le noeud courant a déjà été visité par un autre chemin,\
            et donc si l'on vient de parcourir un cycle dans l'espace de recherche.

            :param dict trace: un dictionnaire contenant les noeuds déjà visités ;\
            le dictionnaire associe à chaque élément le noeud qui l'encapsule.\
            Cela permet de tester rapidement si l'élément du noeud courant est\
            présent dans un autre noeud déjà visité.
            :return: True si le noeud courant a déjà été visité.
        """

        return noeud.element in trace

    def trouve_successeurs(self, noeud):
        """ Trouve les successeurs du noeud courant.

            :return: une liste contenant les noeuds successeurs du noeud courant.
        """

        print('à compléter')

    def ajoute_successeurs(self, queue, successeurs):
        """ Ajoute les successeurs ``successeurs`` à la queue ``queue``\
            (à implémenter différemment pour DFS, BFS et A\*).

            La queue peut éventuellement être modifiée par la méthode.

            :param list queue: la queue des noeuds à explorer.
            :param list successeurs: les successeurs à ajouter.
            :return: la queue contenant tous les successeurs dans le\
            bon ordre.
        """

        # Nous retournons une liste vide pour éviter de déclencher une exception,
        # mais cette méthode doit être surchargée dans les sous-classes.
        return []

Module .../moteurs_recherche/bfs.py :

from .recherche import Recherche
from .noeud import Noeud

class RechercheBFS(Recherche):
    """ Recherche de chemin avec BFS. """

    def ajoute_successeurs(self, queue, successeurs):
        print('à compléter')

Module .../moteurs_recherche/dfs.py :

from .recherche import Recherche
from .noeud import Noeud

class RechercheDFS(Recherche):
    """ Recherche de chemin avec DFS. """

    def ajoute_successeurs(self, queue, successeurs):
        print('à compléter')

Module .../moteurs_recherche/astar.py :

from moteurs_recherche.recherche import Recherche
from moteurs_recherche.noeud import Noeud

class RechercheAStar(Recherche):
    """ Recherche de chemin avec A\*. """

    def detecte_cycle(self, trace, noeud):
        print('à compléter')

    def ajoute_successeurs(self, queue, successeurs):
        print('à compléter')

Module .../exemple_carte_simple.py :

from moteurs_recherche.element import Element
from moteurs_recherche.ville import Ville
from moteurs_recherche.espace import Espace
from moteurs_recherche.dfs import RechercheDFS
from moteurs_recherche.bfs import RechercheBFS
from moteurs_recherche.astar import RechercheAStar

# Les éléments dans l'espace de recherche (les villes).
elements = {
    'A': Ville(0, 16, 'A'),
    'B': Ville(5, 13, 'B'),
    'C': Ville(0, 10, 'C'),
    'D': Ville(5, 8, 'D'),
    'E': Ville(11, 18, 'E'),
    'F': Ville(15, 13, 'F'),
    'G': Ville(29, 18, 'G'),
    'H': Ville(26, 0, 'H'),
    'I': Ville(12, 10, 'I'),
    'J': Ville(17, 7, 'J'),
    'K': Ville(11, 3, 'K'),
    'L': Ville(22, 16, 'L'),
    'M': Ville(25, 12, 'M'),
    'N': Ville(24, 6, 'N'),
    'O': Ville(20, 0, 'O'),
    'P': Ville(5, 0, 'P'),
}

# Les arcs liant les éléments (les routes).
arcs = [
    (elements['A'], elements['B']),
    (elements['A'], elements['E']),
    (elements['B'], elements['C']),
    (elements['B'], elements['E']),
    (elements['B'], elements['D']),
    (elements['C'], elements['D']),
    (elements['C'], elements['P']),
    (elements['D'], elements['I']),
    (elements['D'], elements['K']),
    (elements['E'], elements['F']),
    (elements['E'], elements['L']),
    (elements['F'], elements['I']),
    (elements['F'], elements['L']),
    (elements['F'], elements['M']),
    (elements['G'], elements['H']),
    (elements['G'], elements['L']),
    (elements['G'], elements['M']),
    (elements['I'], elements['J']),
    (elements['J'], elements['K']),
    (elements['J'], elements['N']),
    (elements['K'], elements['O']),
    (elements['K'], elements['P']),
    (elements['M'], elements['N']),
    (elements['N'], elements['O']),
    (elements['B'], elements['I']),
    (elements['B'], elements['F']),
    (elements['P'], elements['O'])
]

# L'espace de recherche.
espace = Espace(elements.values(), arcs)
print('L\'espace de recherche:\n{}'.format(espace))

print('Recherche DFS:')
dfs = RechercheDFS(espace, False)
chemin = dfs.recherche(elements['A'], elements['P'])

print('Chemin: {}'.format(chemin))

#################################################

print('\nRecherche BFS:')
bfs = RechercheBFS(espace, False)

chemin = bfs.recherche(elements['A'], elements['P'])
print('Chemin: {}'.format(chemin))

#################################################

print('\nRecherche A*:')
astar = RechercheAStar(espace, False)
chemin = astar.recherche(elements['A'], elements['P'])

print('Chemin: {}'.format(chemin))

Module .../exemple_carte_suisse.py :

from moteurs_recherche.element import Element
from moteurs_recherche.ville import Ville
from moteurs_recherche.espace import Espace
from moteurs_recherche.dfs import RechercheDFS
from moteurs_recherche.bfs import RechercheBFS
from moteurs_recherche.astar import RechercheAStar

# Les éléments dans l'espace de recherche (les villes).
elements = {
    'Lausanne': Ville(110, 260, 'Lausanne'),
    'Genève': Ville(40, 300, 'Genève'),
    'Sion': Ville(200, 300, 'Sion'),
    'Neuchâtel': Ville(150, 170, 'Neuchâtel'),
    'Bern': Ville(210, 280, 'Bern'),
    'Basel': Ville(230, 65, 'Basel'),
    'Fribourg': Ville(175, 200, 'Fribourg'),
    'Zürich': Ville(340, 90, 'Zürich'),
    'Aarau': Ville(290, 95, 'Aarau'),
    'Luzern': Ville(320, 155, 'Luzern'),
    'St-Gallen': Ville(85, 455,  'St-Gallen'),
    'Thun': Ville(235, 210, 'Thun'),
}

# Les arcs liant les éléments (les routes).
arcs = [
    (elements['Lausanne'], elements['Genève']),
    (elements['Sion'], elements['Lausanne']),
    (elements['Neuchâtel'], elements['Lausanne']),
    (elements['Fribourg'], elements['Lausanne']),
    (elements['Fribourg'], elements['Bern']),
    (elements['Sion'], elements['Thun']),
    (elements['Neuchâtel'], elements['Bern']),
    (elements['Basel'], elements['Bern']),
    (elements['Zürich'], elements['Aarau']),
    (elements['Zürich'], elements['Luzern']),
    (elements['Bern'], elements['Aarau']),
    (elements['Bern'], elements['Luzern']),
    (elements['Luzern'], elements['Aarau']),
    (elements['St-Gallen'], elements['Zürich']),
    (elements['Thun'], elements['Bern']),
    (elements['Basel'], elements['Zürich']),
]

# L'espace de recherche.
espace = Espace(elements.values(), arcs)
print('L\'espace de recherche:\n{}'.format(espace))

print('Recherche DFS:')
dfs = RechercheDFS(espace, False)
chemin = dfs.recherche(elements['Lausanne'], elements['Zürich'])

print('Chemin: {}'.format(chemin))

#################################################

print('\nRecherche BFS:')
bfs = RechercheBFS(espace, False)
chemin = bfs.recherche(elements['Lausanne'], elements['Zürich'])

print('Chemin: {}'.format(chemin))

#################################################

print('\nRecherche A*:')
astar = RechercheAStar(espace, False)
chemin = astar.recherche(elements['Lausanne'], elements['Zürich'])

print('Chemin: {}'.format(chemin))

Exercice 1 : Classes de base

La classe Element

Dans cet exercice, vous devrez manipuler des éléments situés dans un espace de recherche — plus précisément, des villes dans un espace à deux dimensions. L’espace de recherche est muni d’une notion de distance appliquable à chaque paire d’éléments. L’objectif des algorithmes de recherche que nous allons programmer sera de trouver le chemin le plus court entre deux éléments.

La classe Element du module element.py représentera donc un élément générique placé dans un espace de recherche. Cet élément sera caractérisé par un seul attribut, self.nom. Commencez par compléter le constructeur de la classe, qui doit initialiser cet attribut au moyen d’une valeur passée en paramètre. Ensuite, complétez la méthode __eq__, qui doit vérifier l’égalité entre l’élément courant et un autre. Deux éléments seront réputés égaux (__eq__ retourne True) lorsqu’ils possèdent des noms égaux. Notez que Element contient aussi une fonction qui retourne la distance entre l’élément courant et un autre élément. Cette fonction n’a ici qu’une implémentation triviale et devra être surchargée de manière appropriée dans les sous-classes.

La classe Ville

La classe Element nous fournit un modèle que les éléments de recherche doivent spécialiser. Notre but dans cet exercice est de trouver des chemins entre des villes, qui sont des éléments dans un espace à deux dimensions. Nous définissons donc une sous-classe Ville, qui étend Element en lui ajoutant deux attributs :

  • x: la position de l’élément sur l’axe des x ;
  • y: la position de l’élément sur l’axe des y.

Commencez par coder le constructeur de la classe, qui doit initialiser ces attributs à partir des valeurs passées en paramètre. Ensuite, surchargez la méthode d’égalité __eq__, afin qu’elle compare les noms et les coordonnées. Finalement, surchargez la fonction de distance, afin de retourner la distance euclidienne entre deux villes. Pour cette dernière opération, vous pouvez utiliser l’opérateur de mise à la puissance de Python, qui est ** (par exemple : 3**2 == 9). En outre, from math import sqrt permet d’importer uniquement la fonction racine carrée du module math.

La classe Espace

Dans l’espace de recherche, chaque élément sera lié à d’autre éléments placés à proximité — ses voisins. Par exemple, si l’espace de recherche représente une carte de la Suisse, Lausanne sera parmi les éléments voisins de Genève. Nous indiquerons la proximité entre deux éléments par un tuple (element_1, element_2). Du point de vue formel, un tuple représente ainsi un arc dans le graphe constitué par les éléments et leurs relations de vicinité.

Nous utiliserons la classe Espace pour représenter un espace de recherche. Espace stockera tous ses éléments dans une liste self.elements et tous ses arcs dans self.arcs. Le constructeur initialise les listes d’éléments et d’arcs, soit avec des collections passées en paramètre, soit comme des listes vides lorsqu’il est appelé sans arguments.

La classe contient aussi les méthodes suivantes, que vous devez implémenter :

  • ajoute_arcs(self, arcs) : prend en argument une collection de tuples (a, b) représentant des arcs, qu’elle ajoute aux collections de l’objet courant. (Cette méthode n’est pas absolument indispensable, car les listes peuvent aussi bien être remplies lors de la construction, mais il est parfois plus pratique et plus lisible d’ajouter des arcs par le biais d’une méthode.);
  • trouve_voisins(self, element) : retourne la liste de tous les voisins d’un élément (par exemple, si l’arc (a, b) est le seul de l’espace e : e.voisins(a) doit retourner [b].

La classe Noeud

Lors de la recherche, chaque Element sera modélisé par un noeud dans un arbre de recherche. Chaque noeud contiendra une référence sur un élément. Les noeuds seront crées de façon dynamique au cours de l’exploration de l’espace de recherche par l’algorithme.

Nous avons donc besoin d’une classe Noeud (squelette disponible dans noeud.py), permettant de modéliser un noeud. Cette classe contiendra quatre attributs :

  • element : une référence sur un objet de type Element ;
  • cout_c : contient le coût c(n), c’est-à-dire le coût depuis le noeud de départ jusqu’au noeud en question. Il s’agit de la somme minimale des longueurs des arcs entre l’élément référencé par le noeud de départ et l’élément référencé par le noeud courant. Dans notre cas, la longueur d’un arc est donnée par la distance entre ses deux éléments ;
  • cout_f : contient le coût f(n), c’est-à-dire le coût heuristique utilisé par l’algorithme A^{\star}, qui est égal à c(n)+h(n). Dans notre cas, la fonction heuristique h(n) calcule la distance euclidienne entre l’élément contenu par le noeud courant et l’élément but ; le coût heuristique modélise ainsi la distance au but, de manière à privilégier l’exploration du noeud le plus prometteur ;
  • parent : le noeud parent du noeud courant, c’est-à-dire le noeud qui a conduit au noeud courant durant la recherche.

Écrivez donc un constructeur qui initialise ces quatre attributs à partir de valeurs passées en paramètres.

Exercice 2 : Algorithmes de recherche

Nous allons maintenant développer un outil de recherche qui implémente les trois principaux algorithmes : DFS (Depth-first search : Recherche en profondeur d’abord), BFS (Breadth-first search: Recherche en largeur d’abord) et A^{\star} (Recherche par A^{\star}).

Le principe de tout algorithme de recherche est de trouver un élément but (dans notre exemple, la ville de destination) à partir d’un élément initial (la ville de départ). Lors de l’exploration de l’espace des éléments, la recherche traverse un arbre constitué de noeuds qui sont liés à leurs noeuds successeurs. Chaque noeud de recherche correspond à une étape dans la recherche. L’exploration d’un noeud de recherche permet, s’il en a, de trouver ses successeurs — qui deviennent de nouveaux noeuds de recherche.

Les algorithmes que nous allons étudier ne se différencient que par la gestion de la liste des noeuds ouverts Q :

  • DFS (en profondeur d’abord) : place les nouveaux noeuds en tête de Q ;
  • BFS (en largeur d’abord) : place les nouveaux noeuds en queue de Q ;
  • A^{\star} : insère les nouveaux noeuds de telle sorte que Q soit toujours ordonnée selon le coût heuristique croissant de ces noeuds.

Le pseudocode de l’algorithme de recherche vous est donné ci-dessous :

Recherche(noeud_depart, noeud_but, methode)
 1. Q <- [noeud_depart]
 2. WHILE Q n'est pas vide DO
 3.     n <- premier(Q)
 4.     Q <- reste(Q)
 5.     IF n == noeud_but THEN
 6.         RETURN chemin de noeud_depart à n
 7.     ELSE
 8.         S <- successeurs de n
 9.         Q <- AjouterSuccesseurs(Q, S, methode)
10.     END IF
11. END WHILE
12. RETURN échec
END Recherche

Comme vous pouvez le constater, l’algorithme de base est le même quelle que soit la méthode utilisée (DFS, BFS et A^{\star}). La seule différence réside dans la façon d’ajouter les successeurs à la liste Q. Plus concrètement, la fonction qui ajoute les successeurs à Q est définie comme suit :

AjouteSuccesseurs(Q, S, methode)
 1. IF methode == DFS THEN
 2.     RETURN S + Q
 3. ELSE IF methode == BFS THEN
 4.     RETURN Q + S
 5. ELSE IF methode == A* THEN
 6.     Q <- Q + S
 7.     Q <- Q trié par coût heuristique
 8.     RETURN Q
 9. ELSE
10.     RETURN échec
11. END IF
END AjouteSuccesseurs

Les classes de recherche

L’algorithme de recherche doit être implémenté dans la classe Recherche de recherche.py. Un constructeur de la classe, qui initialise le graphe de recherche, vous est déjà donné. Également donnée est une fonction d’interface recherche, qui prend en paramètres deux éléments, crée deux noeuds de recherche contenant ces éléments, et les passe en arguments à la méthode recherche_chemin. Cest dans celle-ci que vous devez implémenter l’algorithme. Appelez les méthodes trouve_successeurs et ajoute_successeurs aux étapes 8 et 9 de l’algorithme. Afin de retourner le chemin depuis le noeud de départ, appelez la méthode trouve_chemin, qui vous est déjà donnée.

Ensuite, complétez la méthode trouve_successeurs. Cette méthode doit retourner une liste contenant tous les successeurs d’un noeud passé en paramètre. La méthode doit retourner tous les éléments voisins de l’élément encapsulé par le noeud courant, eux-mêmes contenus dans de nouveaux noeuds de recherche (sauf le parent du noeud courant, pour éviter les cycles...). Notez qu’en créant ces nouveaux noeuds, vous devez aussi initialiser leurs coûts, à partir du coût du noeud courant et en utilisant la fonction heuristique self.h. Cette dernière est initialisée à l’aide d’une fonction lambda dans la méthode d’interface recherche.

Ensuite, vous pouvez passer à la méthode ajoute_successeurs. Il convient d’implémenter cette méthode différemment pour chacun des trois algorithmes, dans les sous-classes de Recherche : RechercheDFS (dfs.py), RechercheBFS (bfs.py) et RechercheAStar (astar.py)

Test du programme

Testez vos algorithmes sur les fichiers exemple_carte_simple.py et exemple_carte_suisse.py.

python3 exemple_carte_simple.py
python3 exemple_carte_suisse.py

Qu’en concluez-vous ? Quelle est l’importance de l’heuristique utilisée par A^{\star} ? Regardez surtout le nombre de noeuds de recherche examinés. Que pouvez-vous conclure sur les algorithmes en regardant la longueur des chemins (en nombre de villes traversées) ?

Pourquoi l’algorithme DFS boucle-t-il à l’infini sur les deux cartes ? Testez maintenant en commentant le code appelant l’algorithme DFS. Quel résultat obtenez-vous ?

Exercice 3: Algorithmes de recherche avec détection de cycles

Comme vous avez pu le constater, l’algorithme n’est pas très efficace car, dans un espace de recherche cyclique, certains noeuds peuvent être visités à plusieurs reprises. Pire, l’algorithme peut se retrouver prisonnier d’une boucle infinie.

Détecter et éviter les cycles nécessite de maintenir une liste des noeuds déjà explorés. Dans le cas du DFS et du BFS, l’algorithme devra contrôler si ce noeud est déjà présent dans la liste avant de chercher ses successeurs. Lorsque c’est le cas, nous savons que les successeurs ont déjà été construits, et il n’est pas utile de recommencer. L’algorithme A^{\star} est un peu plus compliqué de ce point de vue : on doit revisiter un noeud si et seulement si le coût f(n) est inférieur au coût f(n) du noeud la dernière fois qu’il a été exploré

En résumé, l’algorithme de recherche optimisé est le suivant :

RechercheOptimisee(noeud_depart, noeud_but, methode)
 1. Q <- [noeud_depart]
 2. C <- []
 3. WHILE Q non vide DO
 4.     n <- premier(Q), Q <- reste(Q)
 5.     IF n == noeud_but THEN
 6.         RETURN chemin de noeud_depart à n
 7.     ELSE
 8.         IF n not in C or
               (n=n' in C and f(n)<f(n') and methode is "A*")  THEN
 9.             S <- successeurs de n
10.             Q <- AjouterSuccesseurs(Q, S, methode)
11.             ajoute n dans C
12.         END IF
13.     END IF
14. END WHILE
15. RETURN échec
END RechercheOptimisee

En vous basant sur le pseudocode ci-dessus, implémentez l’algorithme de recherche optimisé. Vous devrez d’abord implémenter la méthode detecte_cycle, qui teste si un noeud a déjà été exploré, étant donné le noeud courant et la collection trace des noeuds déjà explorés. Notez que vous devez surcharger cette méthode dans la classe RechercheAStar, afin de tolérer les cycles qui permettent de trouver un chemin plus court.

Nous vous recommandons de ne pas implémenter une nouvelle méthode recherche_chemin, mais de modifier la version existante afin de traiter le cas où l’attribut de la classe self.optimisee prend la valeur True. En outre, afin d’optimiser la détection des cycles, nous vous suggérons d’implémenter trace comme un dictionnaire qui associe chaque élément au noeud de recherche qui le contient.

Lorsque vous avez terminé, vous pouvez tester à nouveau votre programme. Qu’observez-vous cette fois ?

Exercice 4 : Labyrinthe

À titre d’exercice supplémentaire, vous pouvez tester votre implémentation en lui faisant découvrir un chemin entre deux points d’un labyrinthe. Pour simplifier, vous coderez ce dernier sous la forme d’une matrice de caractères L, telle que L[i][j] prenne la valeur 'x' si la cellule (i,j) est occupée, et ne peut pas être explorée.

Vous pouvez ensuite implémenter une méthode qui traduira cette matrice en un objet Espace, qui contiendra les déplacements possibles depuis chaque cellule (par exemple, en haut, en bas, à gauche et à droite si les cellules voisines sont libres). Appliquez les trois algorithmes de recherche pour construire le chemin conduisant d’un point quelconque à un autre et comparez les résultats.