Recherche informée

CSI 4106 - Automne 2025

Marcel Turcotte

Version: nov. 7, 2025 08h58

Préambule

Message du jour

Objectifs d’apprentissage

  • Comprendre les stratégies de recherche informée et le rôle des fonctions heuristiques dans l’efficacité de la recherche.

  • Implémenter et comparer BFS, DFS, et la recherche par meilleur choix (Best-First Search) en utilisant le problème du puzzle à 8 cases.

  • Analyser la performance et l’optimalité de divers algorithmes de recherche.

Résumé

Problème de recherche

  • Un ensemble d’états, appelé espace d’états.

  • Un état initial où l’agent commence.

  • Un ou plusieurs états objectifs qui définissent des résultats réussis.

  • Un ensemble d’actions disponibles dans un état donné \(s\).

  • Un modèle de transition qui détermine l’état suivant en fonction de l’état actuel et de l’action sélectionnée.

  • Une fonction de coût d’action qui spécifie le coût de l’exécution de l’action \(a\) dans l’état \(s\) pour atteindre l’état \(s'\).

Définitions

  • Un chemin est défini comme une séquence d’actions.

  • Une solution est un chemin qui relie l’état initial à l’état objectif.

  • Une solution optimale est le chemin avec le coût le plus bas parmi toutes les solutions possibles.

Exemple : 8-Puzzle

Code
import random
import matplotlib.pyplot as plt
import numpy as np

random.seed(58)

def is_solvable(tiles):
    # Compter les inversions dans la liste à plat des tuiles (en excluant l'espace vide)
    inversions = 0
    for i in range(len(tiles)):
        for j in range(i + 1, len(tiles)):
            if tiles[i] != 0 and tiles[j] != 0 and tiles[i] > tiles[j]:
                inversions += 1
    return inversions % 2 == 0

def generate_solvable_board():
    # Générer une configuration de plateau aléatoire qui est garantie d'être résoluble
    tiles = list(range(9))
    random.shuffle(tiles)
    while not is_solvable(tiles):
        random.shuffle(tiles)

    return tiles

def plot_board(board, title, num_pos, position):
    ax = plt.subplot(1, num_pos, position)
    ax.set_title(title)
    ax.set_xticks([])
    ax.set_yticks([])

    board = np.array(board).reshape(3, 3).tolist() # Reconfigurer en une grille 3x3

    # Utiliser une carte de couleurs pour afficher les numéros
    cmap = plt.cm.plasma
    norm = plt.Normalize(vmin=-1, vmax=8)

    for i in range(3):
        for j in range(3):
            tile_value = board[i][j]
            color = cmap(norm(tile_value))
            ax.add_patch(plt.Rectangle((j, 2 - i), 1, 1, facecolor=color, edgecolor='black'))
            if tile_value == 0:
                ax.add_patch(plt.Rectangle((j, 2 - i), 1, 1, facecolor='white', edgecolor='black'))
            else:
                ax.text(j + 0.5, 2 - i + 0.5, str(tile_value),
                        fontsize=16, ha='center', va='center', color='black')

    ax.set_xlim(0, 3)
    ax.set_ylim(0, 3)

Exemple : 8-Puzzle

Code
def main():
    # Générer un plateau initial résoluble
    initial_board = generate_solvable_board()

    # Définir l'état objectif
    goal_board = [
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 0]
    ]

    # Tracer les deux plateaux
    plt.figure(figsize=(8, 4))
    plot_board(initial_board, "Plateau Initial", 2, 1)
    plot_board(goal_board, "État Objectif", 2, 2)
    plt.tight_layout()
    plt.show()

main()

Arbre de recherche

Arbre de recherche

Frontière

Frontière

Frontière

is_empty

def is_empty(frontier):
    """Vérifie si la frontière est vide."""
    return len(frontier) == 0

is_goal

def is_goal(state, goal_state):
    """Détermine si un état donné correspond à l'état objectif."""
    return state == goal_state

expand

def expand(state):
    """Génère les états successeurs en déplaçant la tuile vide dans toutes les directions possibles."""
    size = int(len(state) ** 0.5) # Déterminer la taille du puzzle (3 pour le 8-Puzzle, 4 pour le 15-Puzzle)
    idx = state.index(0) # Trouver l'index de la tuile vide représentée par 0
    x, y = idx % size, idx // size # Convertir l'index en coordonnées (x, y)
    neighbors = []

    # Définir les mouvements possibles : Gauche, Droite, Haut, Bas
    moves = [(-1, 0), (1, 0), (0, -1), (0, 1)]
    for dx, dy in moves:
        nx, ny = x + dx, y + dy
        # Vérifier si la nouvelle position est dans les limites du puzzle
        if 0 <= nx < size and 0 <= ny < size:
            n_idx = ny * size + nx
            new_state = state.copy()
            # Échanger la tuile vide avec la tuile adjacente
            new_state[idx], new_state[n_idx] = new_state[n_idx], new_state[idx]
            neighbors.append(new_state)
    return neighbors

Recherche en largeur

from collections import deque

La recherche en largeur (BFS) utilise une file pour gérer les nœuds de la frontière, également connus sous le nom de liste ouverte.

Recherche en largeur

def bfs(initial_state, goal_state):
    frontier = deque() # Initialiser la file pour BFS
    frontier.append((initial_state, [])) # Chaque élément est un tuple : (état, chemin)

    explored = set()
    explored.add(tuple(initial_state))

    iterations = 0 # utilisé simplement pour comparer les algorithmes

    while not is_empty(frontier):
        current_state, path = frontier.popleft()

        if is_goal(current_state, goal_state):
            print(f"Nombre d'itérations : {iterations}")
            return path + [current_state] # Retourner le chemin réussi

        iterations = iterations + 1

        for neighbor in expand(current_state):
            neighbor_tuple = tuple(neighbor)
            if neighbor_tuple not in explored:
                explored.add(neighbor_tuple)
                frontier.append((neighbor, path + [current_state]))

    return None # Aucune solution trouvée

Recherche en profondeur

def dfs(initial_state, goal_state):
    frontier = [(initial_state, [])] # Chaque élément est un tuple : (état, chemin)

    explored = set()
    explored.add(tuple(initial_state))

    iterations = 0

    while not is_empty(frontier):
        current_state, path = frontier.pop()

        if is_goal(current_state, goal_state):
            print(f"Nombre d'itérations : {iterations}")
            return path + [current_state] # Retourner le chemin réussi

        iterations = iterations + 1

        for neighbor in expand(current_state):
            neighbor_tuple = tuple(neighbor)
            if neighbor_tuple not in explored:
                explored.add(neighbor_tuple)
                frontier.append((neighbor, path + [current_state]))

    return None # Aucune solution trouvée

Remarques

  • La recherche en largeur (BFS) identifie la solution optimale, 25 mouvements, en 145 605 itérations.

  • La recherche en profondeur (DFS) découvre une solution impliquant 1 157 mouvements en 1 187 itérations.

Recherche informée

Recherche heuristique

Les algorithmes de recherche informée utilisent des connaissances spécifiques au domaine concernant l’emplacement de l’état objectif.

Recherche heuristique

Soit \(f(n)\) une fonction heuristique qui estime le coût du chemin de moindre coût de l’état ou du nœud actuel \(n\) jusqu’à l’objectif.

Recherche heuristique

Dans les problèmes de recherche de route, on pourrait utiliser la distance en ligne droite de la position actuel à la destination comme heuristique.

Bien qu’un chemin réel puisse ne pas exister le long de cette ligne droite, l’algorithme donnera la priorité à l’expansion du nœud le plus proche de la destination (objectif) en se basant sur cette mesure en ligne droite.

Exemple du livre

Exemple du livre

Exemple du livre

Exemple du livre

Exemple du livre

Exemple du livre

Exemple du livre

Exemple du livre

Implémentation

  • Comment peut-on modifier les algorithmes existants de recherche en largeur et de recherche en profondeur pour implémenter la recherche du meilleur d’abord ?

    • Cela peut être réalisé en utilisant une file de priorité, qui est triée selon les valeurs de la fonction heuristique \(f(n)\).
import heapq

Observation

La recherche en largeur peut être interprétée comme une forme de recherche du meilleur d’abord, où la fonction heuristique \(f(n)\) est définie comme la profondeur du nœud dans l’arbre de recherche, correspondant à la longueur du chemin.

A-star

\(A^\star\) (a-star, a-étoile) est la recherche informée la plus courante.

\[ f(n) = g(n) + h(n) \]

  • \(g(n)\) est le coût du chemin depuis l’état initial jusqu’à \(n\).
  • \(h(n)\) est une estimation du coût du plus court chemin de \(n\) à l’état final.

Admissibilité

Une heuristique est admissible si elle ne surestime jamais le coût réel pour atteindre l’objectif à partir de n’importe quel nœud dans l’espace de recherche.

Cela garantit que l’algorithme \(A^\star\) trouve une solution optimale, puisque le coût estimé est toujours une borne inférieure du coût réel.

Admissibilité

Formellement, une heuristique \(h(n)\) est admissible si : \[ h(n) \leq h^*(n) \] où :

  • \(h(n)\) est l’estimation heuristique du coût depuis le nœud \(n\) jusqu’à l’objectif.
  • \(h^*(n)\) est le coût réel du chemin optimal depuis le nœud \(n\) jusqu’à l’objectif.

Optimalité de coût

L’optimalité de coût se réfère à la capacité d’un algorithme à trouver la solution de moindre coût parmi toutes les solutions possibles.

Dans le contexte des algorithmes de recherche comme \(A^\star\), l’optimalité de coût signifie que l’algorithme identifiera le chemin avec le coût total le plus bas depuis le départ jusqu’à l’objectif, à condition qu’une heuristique admissible soit utilisée.

Théorème

Soit \(h\) admissible, c’est-à-dire \(0\le h(n)\le h^\star(n)\) pour tous les nœuds \(n\), où \(h^\star(n)\) est le coût réel de \(n\) à un objectif.

Supposons des coûts d’action non négatifs et que \(A^\star\) se termine lorsqu’un objectif est sélectionné pour expansion (c’est-à-dire, retiré de la frontière).

Alors \(A^\star\) renvoie une solution optimale.

Preuve

  1. Supposons pour contradiction

Supposons que \(A^\star\) renvoie un objectif sous-optimal \(G\) avec un coût \(C > C^\star\), où \(C^*\) est le coût de la solution optimale.

Quand \(A^\star\) s’arrête, \(G\) vient d’être sélectionné de la frontière avec \(f(G)=g(G)=C\).

Preuve

  1. Borne inférieure le long de tout chemin optimal

Considérons un nœud \(n\) sur un chemin vers un objectif optimal \(G^\star\) (coût \(C^\star\)).

Par admissibilité,

\[ f(n) = g(n) + h(n)\ \le\ g(n) + h^\star(n) = C^\star \]

Ainsi, chaque nœud sur un chemin optimal a \(f(n) \le C^\star\).

Preuve

  1. Le premier nœud non-expansé est dans la frontière

Quand \(G\) est choisi, marchez depuis le début le long d’un chemin optimal vers \(G^\star\) jusqu’à atteindre le premier nœud \(n\) qui n’a pas encore été expansé.

Son parent sur ce chemin a été expansé (par définition de “premier”), donc \(n\) a été généré et est donc en frontière.

D’après l’étape 2, \(f(n)\le C^\star\). Puisque \(C^\star < C = f(G)\), la règle de priorité de \(A^\star\) devrait choisir \(n\) (ou un autre nœud avec \(f \le C^\star\)) avant \(G\), une contradiction.

Preuve

  1. Conclure l’optimalité

La contradiction montre que \(A^\star\) ne peut pas renvoyer un objectif avec un coût \(>C^\star\); donc la solution renvoyée est optimale.

C.Q.F.D.

8-Puzzle

Pouvez-vous penser à une fonction heuristique pour le casse-tête de 8 tuiles ?

Distance du nombre d’inversions

Peut-on utiliser le nombre d’inversions comme fonction heuristique, \(h(n)\), pour le 8-Puzzle?

Code
# Définir l'état initial
initial_board = [
    [1, 2, 3],
    [4, 5, 0],
    [7, 8, 6]
]

# Définir l'état objectif
goal_board = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 0]
]

# Tracer les deux plateaux
plt.figure(figsize=(6, 3))
plot_board(initial_board, "État Initial", 2, 1)
plot_board(goal_board, "État Objectif", 2, 2)
plt.tight_layout()
plt.show()

  • Calculez \(h(s)\) et \(h^\star(s)\).
  • Qu’en concluez-vous?

Distance des tuiles mal placées

def misplaced_tiles_distance(state, goal_state):
    # Comptez le nombre de tuiles mal placées
    misplaced_tiles = sum(1 for s, g in zip(state, goal_state) if s != g and s != 0)
    return misplaced_tiles

8-Puzzle

Code
plt.figure(figsize=(8, 2))

initial_state_8a = [1, 2, 3,
                    4, 0, 6,
                    7, 5, 8]

initial_state_8b = [6, 4, 5,
                    8, 2, 7,
                    1, 0, 3]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

distance_a = misplaced_tiles_distance(initial_state_8a, goal_state_8)

distance_b = misplaced_tiles_distance(initial_state_8b, goal_state_8)

plot_board(initial_state_8a, f"h(n) = {distance_a}", 3, 1)

plot_board(goal_state_8, "État Objectif", 3, 2)

plot_board(initial_state_8b, f"h(n) = {distance_b}", 3, 3)

plt.tight_layout()
plt.show()

Recherche du meilleur d’abord

def best_first_search(initial_state, goal_state):

    frontier = []  # Initialisez la file de priorité
    initial_h = misplaced_tiles_distance(initial_state, goal_state)

    # Ajoutez l'état initial avec sa valeur heuristique dans la file
    heapq.heappush(frontier, (initial_h, 0, initial_state, []))  # (f(n), g(n), état, chemin)

    explored = set()
    explored.add(tuple(initial_state))
    iterations = 0

    while not is_empty(frontier):
        f, g, current_state, path = heapq.heappop(frontier)

        if is_goal(current_state, goal_state):
            print(f"Nombre d'itérations : {iterations}")
            return path + [current_state]  # Retournez le chemin réussi

        iterations += 1

        for neighbor in expand(current_state):
            if tuple(neighbor) not in explored:
                new_g = g + 1  # Incrémentez le coût du chemin
                h = misplaced_tiles_distance(neighbor, goal_state)
                new_f = new_g + h  # Calculez le nouveau coût total
                # Ajoutez l'état voisin dans la file de priorité
                heapq.heappush(frontier, (new_f, new_g, neighbor, path + [current_state]))
                explored.add(tuple(neighbor))  # Marquez le voisin comme exploré

    return None  # Aucune solution trouvée

Cas simple

Code
plt.figure(figsize=(8, 2))

initial_state_8 = [1, 2, 3,
                   4, 0, 6,
                   7, 5, 8]
goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

solutions = best_first_search(initial_state_8, goal_state_8)

for i, solution in enumerate(solutions):
    plot_board(solution, f"Étape : {i}", 3, i+1)

plt.tight_layout()
plt.show()
Nombre d'itérations : 2

initial_state_8 = [1, 2, 3,
                   4, 0, 6,
                   7, 5, 8]
goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

best_first_search(initial_state_8, goal_state_8)
Nombre d'itérations : 2
[[1, 2, 3, 4, 0, 6, 7, 5, 8],
 [1, 2, 3, 4, 5, 6, 7, 0, 8],
 [1, 2, 3, 4, 5, 6, 7, 8, 0]]

Cas difficile

initial_state_8 = [6, 4, 5,
                   8, 2, 7,
                   1, 0, 3]
goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

print("Résolution du casse-tête de 8 tuiles avec best_first_search...")

solution_8_bfs = best_first_search(initial_state_8, goal_state_8)

if solution_8_bfs:
    print(f"Solution trouvée par best_first_search en {len(solution_8_bfs) - 1} mouvements :")
    print_solution(solution_8_bfs)
else:
    print("Aucune solution trouvée pour le casse-tête de 8 tuiles avec best_first_search.")
Résolution du casse-tête de 8 tuiles avec best_first_search...
Nombre d'itérations : 29005
Solution trouvée par best_first_search en 25 mouvements :
Étape 0 :
6 4 5
8 2 7
1   3

Étape 1 :
6 4 5
8 2 7
  1 3

Étape 2 :
6 4 5
  2 7
8 1 3

Étape 3 :
6 4 5
2   7
8 1 3

Étape 4 :
6   5
2 4 7
8 1 3

Étape 5 :
  6 5
2 4 7
8 1 3

Étape 6 :
2 6 5
  4 7
8 1 3

Étape 7 :
2 6 5
4   7
8 1 3

Étape 8 :
2 6 5
4 1 7
8   3

Étape 9 :
2 6 5
4 1 7
  8 3

Étape 10 :
2 6 5
  1 7
4 8 3

Étape 11 :
2 6 5
1   7
4 8 3

Étape 12 :
2 6 5
1 7  
4 8 3

Étape 13 :
2 6 5
1 7 3
4 8  

Étape 14 :
2 6 5
1 7 3
4   8

Étape 15 :
2 6 5
1   3
4 7 8

Étape 16 :
2   5
1 6 3
4 7 8

Étape 17 :
2 5  
1 6 3
4 7 8

Étape 18 :
2 5 3
1 6  
4 7 8

Étape 19 :
2 5 3
1   6
4 7 8

Étape 20 :
2   3
1 5 6
4 7 8

Étape 21 :
  2 3
1 5 6
4 7 8

Étape 22 :
1 2 3
  5 6
4 7 8

Étape 23 :
1 2 3
4 5 6
  7 8

Étape 24 :
1 2 3
4 5 6
7   8

Étape 25 :
1 2 3
4 5 6
7 8  

8-Puzzle

def manhattan_distance(state, goal_state):
    distance = 0
    size = int(len(state) ** 0.5)
    for num in range(1, len(state)):
        idx1 = state.index(num)
        idx2 = goal_state.index(num)
        x1, y1 = idx1 % size, idx1 // size
        x2, y2 = idx2 % size, idx2 // size
        distance += abs(x1 - x2) + abs(y1 - y2)
    return distance

\[ h_{\mathrm{Manathan}}(s) = \sum_{t \in \{1,\ldots,8\}} (|x_t - x_t^\star| + |y_t - y_t^\star|) \]

8-Puzzle

Code
plt.figure(figsize=(8, 2))

initial_state_8a = [1, 2, 3,
                    4, 0, 6,
                    7, 5, 8]

initial_state_8b = [6, 4, 5,
                    8, 2, 7,
                    1, 0, 3]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

distance_a = manhattan_distance(initial_state_8a, goal_state_8)

distance_b = manhattan_distance(initial_state_8b, goal_state_8)

plot_board(initial_state_8a, f"h(n) = {distance_a}", 3, 1)

plot_board(goal_state_8, "État objectif", 3, 2)

plot_board(initial_state_8b, f"h(n) = {distance_b}", 3, 3)

plt.tight_layout()
plt.show()

8-Puzzle

  • Comparer les heuristiques Manhattan et Tuiles Mal Placées.
  • Laquelle est la plus efficace ?
  • Différences significatives de temps d’exécution ?

8-Puzzle

Code
plt.figure(figsize=(8, 2))

initial_state_8a = [1, 2, 3,
                    4, 0, 6,
                    7, 5, 8]

initial_state_8b = [6, 4, 5,
                    8, 2, 7,
                    1, 0, 3]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

distance_a_mis = misplaced_tiles_distance(initial_state_8a, goal_state_8)
distance_b_mis = misplaced_tiles_distance(initial_state_8b, goal_state_8)

distance_a_man = manhattan_distance(initial_state_8a, goal_state_8)
distance_b_man = manhattan_distance(initial_state_8b, goal_state_8)

plot_board(initial_state_8a, f"a = {distance_a_mis}, b = {distance_a_man}", 3, 1)

plot_board(goal_state_8, "État objectif", 3, 2)

plot_board(initial_state_8b, f"a = {distance_b_mis}, b = {distance_b_man}", 3, 3)

plt.tight_layout()
plt.show()

  • a = distance des tuiles mal placées
  • b = distance de Manathan

8-Puzzle

Code
plt.figure(figsize=(8, 2))

initial_state_8a = [3, 1, 2,
                    4, 5, 6,
                    7, 8, 0]

initial_state_8b = [8, 2, 3,
                    4, 5, 6,
                    1, 0, 7]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

distance_a_mis = misplaced_tiles_distance(initial_state_8a, goal_state_8)
distance_b_mis = misplaced_tiles_distance(initial_state_8b, goal_state_8)

distance_a_man = manhattan_distance(initial_state_8a, goal_state_8)
distance_b_man = manhattan_distance(initial_state_8b, goal_state_8)

plot_board(initial_state_8a, f"a = {distance_a_mis}, b = {distance_a_man}", 3, 1)

plot_board(goal_state_8, "État objectif", 3, 2)

plot_board(initial_state_8b, f"a = {distance_b_mis}, b = {distance_b_man}", 3, 3)

plt.tight_layout()
plt.show()

  • a = distance des tuiles mal placées
  • b = distance de Manhattan

Recherche du meilleur d’abord

def best_first_search_revised(initial_state, goal_state):
    frontier = [] # Initialiser la file de priorité
    initial_h = manhattan_distance(initial_state, goal_state)
    # Ajouter l'état initial avec sa valeur heuristique dans la file
    heapq.heappush(frontier, (initial_h, 0, initial_state, [])) # (f(n), g(n), état, chemin)

    explored = set()
    explored.add(tuple(initial_state))
    iterations = 0

    while not is_empty(frontier):
        f, g, current_state, path = heapq.heappop(frontier)

        if is_goal(current_state, goal_state):
            print(f"Nombre d'itérations : {iterations}")
            return path + [current_state] # Retourner le chemin réussi

        iterations = iterations + 1

        for neighbor in expand(current_state):
            if tuple(neighbor) not in explored:
                new_g = g + 1 # Incrémenter le coût du chemin
                h = manhattan_distance(neighbor, goal_state)
                new_f = new_g + h # Calculer le nouveau coût total
                # Ajouter l'état voisin dans la file de priorité
                heapq.heappush(frontier, (new_f, new_g, neighbor, path + [current_state]))
                explored.add(tuple(neighbor)) # Marquer le voisin comme exploré

    return None # Aucune solution trouvée

Cas simple

Code
plt.figure(figsize=(8, 2))

initial_state_8 = [1, 2, 3,
                   4, 0, 6,
                   7, 5, 8]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

solutions = best_first_search_revised(initial_state_8, goal_state_8)

for i, solution in enumerate(solutions):
    plot_board(solution, f"Étape : {i}", 3, i+1)

plt.tight_layout()
plt.show()
Nombre d'itérations : 2

initial_state_8 = [1, 2, 3,
                   4, 0, 6,
                   7, 5, 8]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

best_first_search_revised(initial_state_8, goal_state_8)
Nombre d'itérations : 2
[[1, 2, 3, 4, 0, 6, 7, 5, 8],
 [1, 2, 3, 4, 5, 6, 7, 0, 8],
 [1, 2, 3, 4, 5, 6, 7, 8, 0]]

Cas difficile

initial_state_8 = [6, 4, 5,
                   8, 2, 7,
                   1, 0, 3]

goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

print("Résolution du 8-Puzzle avec best_first_search...")

solution_8_bfs = best_first_search_revised(initial_state_8, goal_state_8)

if solution_8_bfs:
    print(f"Solution best_first_search trouvée en {len(solution_8_bfs) - 1} mouvements :")
    print_solution(solution_8_bfs)
else:
    print("Aucune solution trouvée pour le 8-Puzzle en utilisant best_first_search.")
Résolution du 8-Puzzle avec best_first_search...
Nombre d'itérations : 2255
Solution best_first_search trouvée en 25 mouvements :
Étape 0 :
6 4 5
8 2 7
1   3

Étape 1 :
6 4 5
8 2 7
  1 3

Étape 2 :
6 4 5
  2 7
8 1 3

Étape 3 :
6 4 5
2   7
8 1 3

Étape 4 :
6   5
2 4 7
8 1 3

Étape 5 :
  6 5
2 4 7
8 1 3

Étape 6 :
2 6 5
  4 7
8 1 3

Étape 7 :
2 6 5
4   7
8 1 3

Étape 8 :
2 6 5
4 1 7
8   3

Étape 9 :
2 6 5
4 1 7
  8 3

Étape 10 :
2 6 5
  1 7
4 8 3

Étape 11 :
2 6 5
1   7
4 8 3

Étape 12 :
2 6 5
1 7  
4 8 3

Étape 13 :
2 6 5
1 7 3
4 8  

Étape 14 :
2 6 5
1 7 3
4   8

Étape 15 :
2 6 5
1   3
4 7 8

Étape 16 :
2   5
1 6 3
4 7 8

Étape 17 :
2 5  
1 6 3
4 7 8

Étape 18 :
2 5 3
1 6  
4 7 8

Étape 19 :
2 5 3
1   6
4 7 8

Étape 20 :
2   3
1 5 6
4 7 8

Étape 21 :
  2 3
1 5 6
4 7 8

Étape 22 :
1 2 3
  5 6
4 7 8

Étape 23 :
1 2 3
4 5 6
  7 8

Étape 24 :
1 2 3
4 5 6
7   8

Étape 25 :
1 2 3
4 5 6
7 8  

Expérimentation

Code
def best_first_search_count(initial_state, goal_state):
    frontier = [] # Initialiser la file de priorité
    initial_h = misplaced_tiles_distance(initial_state, goal_state)
    # Ajouter l'état initial avec sa valeur heuristique dans la file
    heapq.heappush(frontier, (initial_h, 0, initial_state, [])) # (f(n), g(n), état, chemin)

    explored = set()

    iterations = 0

    while not is_empty(frontier):
        f, g, current_state, path = heapq.heappop(frontier)

        if is_goal(current_state, goal_state):
            return len(path), iterations

        iterations = iterations + 1

        explored.add(tuple(current_state))

        for neighbor in expand(current_state):
            if tuple(neighbor) not in explored:
                new_g = g + 1 # Incrémenter le coût du chemin
                h = misplaced_tiles_distance(neighbor, goal_state)
                new_f = new_g + h # Calculer le nouveau coût total
                # Ajouter l'état voisin dans la file de priorité
                heapq.heappush(frontier, (new_f, new_g, neighbor, path + [current_state]))
                explored.add(tuple(neighbor)) # Marquer le voisin comme exploré

    return None, None # Aucune solution trouvée
Code
def best_first_search_revised_count(initial_state, goal_state):
    frontier = [] # Initialiser la file de priorité
    initial_h = manhattan_distance(initial_state, goal_state)
    # Ajouter l'état initial avec sa valeur heuristique dans la file
    heapq.heappush(frontier, (initial_h, 0, initial_state, [])) # (f(n), g(n), état, chemin)

    explored = set()

    iterations = 0

    while not is_empty(frontier):
        f, g, current_state, path = heapq.heappop(frontier)

        if is_goal(current_state, goal_state):
            return len(path), iterations

        iterations = iterations + 1

        explored.add(tuple(current_state))

        for neighbor in expand(current_state):
            if tuple(neighbor) not in explored:
                new_g = g + 1 # Incrémenter le coût du chemin
                h = manhattan_distance(neighbor, goal_state)
                new_f = new_g + h # Calculer le nouveau coût total
                # Ajouter l'état voisin dans la file de priorité
                heapq.heappush(frontier, (new_f, new_g, neighbor, path + [current_state]))
                explored.add(tuple(neighbor)) # Marquer le voisin comme exploré

    return None, None # Aucune solution trouvée

1000 Expériences

Code
goal_state_8 = [1, 2, 3,
                4, 5, 6,
                7, 8, 0]

moves = []
iterations_a = []
iterations_b = []

for i in range(1000):
    initial_state_8 = generate_solvable_board()
    nb_moves, nb_iterations_a = best_first_search_count(initial_state_8, goal_state_8)
    nb_moves, nb_iterations_b = best_first_search_revised_count(initial_state_8, goal_state_8)
    moves.append(nb_moves)
    iterations_a.append(nb_iterations_a)
    iterations_b.append(nb_iterations_b)

import seaborn as sns
import matplotlib.pyplot as plt

# Configurer la structure des sous-graphiques
fig, axes = plt.subplots(1, 2, figsize=(10, 5), sharey=True)

# Tracer l'histogramme pour `moves`
sns.histplot(moves, bins=20, kde=True, color='green', ax=axes[0], edgecolor='black')
axes[0].set_title("Histogramme des mouvements")
axes[0].set_xlabel("Mouvements")
axes[0].set_ylabel("Fréquence")

# Tracer l'histogramme pour `iterations`
sns.histplot(iterations_a, kde=True, color='red', ax=axes[1], edgecolor='black', label="Tuiles Mal Placées")

# Tracer l'histogramme pour `iterations`
sns.histplot(iterations_b, kde=True, color='blue', ax=axes[1], edgecolor='black', label="Manhattan")
axes[1].set_title("Histogramme des itérations")
axes[1].set_xlabel("Itérations")

# Afficher le graphique
plt.legend()
plt.tight_layout()
plt.show()

Diagramme de dispersion

Code
# Créer un diagramme de dispersion

plt.figure(figsize=(8, 6))
plt.scatter(moves, iterations_b, color='blue', edgecolor='black')

# Ajouter des titres et des étiquettes
plt.title("Diagramme de dispersion : Mouvements vs Itérations")
plt.xlabel("Mouvements")
plt.ylabel("Itérations")

# Afficher le graphique
plt.show()

Exploration

La recherche en largeur (BFS) garantit de trouver le chemin le plus court, ou la solution de coût le plus bas, en supposant que toutes les actions ont un coût unitaire.

Développez un programme qui effectue les tâches suivantes :

  1. Générer une configuration aléatoire du puzzle 8-Puzzle.
  2. Déterminer le chemin le plus court en utilisant la recherche en largeur.
  3. Identifier la solution optimale en utilisant l’algorithme \(A^\star\).
  4. Comparer les coûts des solutions obtenues aux étapes 2 et 3. Il ne devrait pas y avoir de différence si \(A^\star\) identifie des solutions optimales en termes de coût.
  5. Répéter le processus.

Exploration

L’heuristique \(h(n) = 0\) est considérée comme admissible, mais elle entraîne généralement une exploration inefficace de l’espace de recherche. Développez un programme pour explorer ce concept. Montrez que, lorsque toutes les actions sont supposées avoir un coût unitaire, \(A^\star\) et la recherche en largeur (BFS) explorent l’espace de recherche de manière similaire. Plus précisément, ils examinent d’abord tous les chemins de longueur un, puis ceux de longueur deux, et ainsi de suite.

Remarques

  • La recherche en largeur (BFS) identifie la solution optimale, 25 mouvements, en 145 605 itérations.

  • La recherche en profondeur (DFS) découvre une solution impliquant 1 157 mouvements en 1 187 itérations.

  • La recherche du meilleur d’abord identifie la solution optimale, 25 mouvements, en 2 255 itérations.

Évaluation de la performance

  • Complétude : L’algorithme assure-t-il qu’une solution sera trouvée si elle existe et indique-t-il correctement l’absence de solution lorsqu’aucune solution n’existe ?

  • Optimalité du coût : L’algorithme identifie-t-il la solution avec le coût de chemin le plus bas parmi toutes les solutions possibles ?

Évaluation de la performance

  • Complexité temporelle : Comment le temps requis par l’algorithme évolue-t-il par rapport au nombre d’états et d’actions ?

  • Complexité spatiale : Comment l’espace requis par l’algorithme évolue-t-il par rapport au nombre d’états et d’actions ?

Vidéos par Sebastian Lague

Une ressource dédiée à \(A^\star\)

Prologue

Résumé

  • Recherche informée et heuristiques
  • Recherche du meilleur d’abord
  • Implémentations

Prochain cours

  • Nous explorerons plus en détail les fonctions heuristiques et examinerons d’autres algorithmes de recherche.

Références

Hart, Peter E., Nils J. Nilsson, et Bertram Raphael. 1968. « A Formal Basis for the Heuristic Determination of Minimum Cost Paths ». IEEE Transactions on Systems Science and Cybernetics 4 (2): 100‑107. https://doi.org/10.1109/tssc.1968.300136.
Russell, Stuart, et Peter Norvig. 2020. Artificial Intelligence: A Modern Approach. 4ᵉ éd. Pearson. http://aima.cs.berkeley.edu/.

Marcel Turcotte

Marcel.Turcotte@uOttawa.ca

École de science informatique et de génie électrique (SIGE)

Université d’Ottawa