Matrice et file : parcours en largeur (BFS)
- Comprendre la notion d’un k-voisinage
- Utiliser une file pour faire un parcours en largeur dans une matrice
1 Introduction
Dans une matrice \(n\times m\) ou un graphe, on appelle k-voisinage d’une case, toutes les cases qui sont accessibles en k déplacements à partir de la case donnée.
1-voisinage (distance de Manhattan = 1) :
Matrice 5×5 avec case centrale (3,3) :
□ □ □ □ □
□ □ ◉ □ □ ◉ = voisin à distance 1
□ ◉ ● ◉ □ ● = case de référence
□ □ ◉ □ □
□ □ □ □ □
Les 4 voisins directs : haut, bas, gauche, droite
2-voisinage (distance de Manhattan = 2) :
□ ◉ □ ◉ □
◉ □ ◉ □ ◉ ◉ = voisin à distance 2
□ ◉ ● ◉ □ ● = case de référence
◉ □ ◉ □ ◉
□ ◉ □ ◉ □
8 cases accessibles en exactement 2 déplacements
Nous utiliserons ici une file (principe FIFO) afin de faire des parcours en largeur, par niveau. On peut ainsi programmer des recherches de plus court chemin, de remplissage, de propagation.
Nous rappelons que les procédures pour les files sont : Lire(F), Enfiler(F, x), Defiler(F) et EstVide(F).
2 k-voisinage
2.1 Principe
On part de la case de départ, on explore les voisins et pour chaque voisin, on incrémente de 1 la distance au point de départ.
Exemple avec obstacles (cases noires) :
k=0 (départ): k=1: k=2: k=3:
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ 1 □ □ □ □ 1 □ □ □ □ 1 □ □ □
□ □ 0 ■ □ 1 □ 1 ■ □ 2 1 1 ■ 2 3 1 1 ■ 2
□ □ □ □ □ □ 1 □ □ □ 2 1 2 2 2 3 1 2 2 2
□ ■ □ □ □ □ ■ □ □ □ □ ■ 2 □ □ 3 ■ 2 □ □
0 = départ Niveau 1 Niveau 2 Niveau 3
■ = obstacle (4 cases) (8 cases) (11 cases)
2.2 Algorithmes
Pour réaliser l’algorithme principal, il nous faut un autre algorithme : la recherche de voisins.
2.2.1 La recherche de voisins
Pour rechercher les voisins accessibles, on examine les cellules adjacentes à la position courante dans la matrice afin de vérifier si le passage est libre (valeur 0). La case est ajoutée à la file Voisin pour être explorée lors des prochaines étapes.
ALGORITHME ChercheVoisin(M, [l, c]):
DONNEES
M : matrice de taille n × m
[l, c] : coordonnées de la case
VARIABLES
Voisin : file
DEBUT
SI l-1 ≥ 1 ET M[l-1][c] = 0 ALORS
Enfiler(Voisin, [l-1, c]) // Voisin du haut
FSI
SI c-1 ≥ 1 ET M[l][c-1] = 0 ALORS
Enfiler(Voisin, [l, c-1]) // Voisin de gauche
FSI
SI l+1 ≤ n ET M[l+1][c] = 0 ALORS
Enfiler(Voisin, [l+1, c]) // Voisin du bas
FSI
SI c+1 ≤ m ET M[l][c+1] = 0 ALORS
Enfiler(Voisin, [l, c+1]) // Voisin de droite
FSI
RENVOYER Voisin
FIN
Complexité : Il y a 4 opérations possibles (haut, bas, gauche, droite), la complexité est en \(O(1)\).
2.2.2 Recherche du k-voisinage
Pour rechercher le k-voisinage, il va nous falloir enregistrer le nombre de déplacements effectués pour arriver à chaque case. Pour cela, nous allons utiliser une matrice \(V\) (distances).
ALGORITHME Kvoisin(M, [l, c], k):
DONNEES
M : matrice de taille n × m
[l, c] : coordonnées de la case de départ
k : distance maximale
VARIABLES
i, j, vl, vc : entiers
V : matrice de taille n × m
F, Voisin, kvois : files
DEBUT
Enfiler(F, [l, c])
V[l][c] ← 0
TQ EstVide(F) = FAUX FAIRE
i, j ← Defiler(F)
SI V[i][j] = k ALORS
Enfiler(kvois, [i, j]) // Case à distance k
SINON
Voisin ← ChercheVoisin(M, [i, j])
TQ EstVide(Voisin) = FAUX FAIRE
vl, vc ← Defiler(Voisin)
SI V[vl, vc] < V[i][j] ALORS
Enfiler(F, [vl, vc])
V[vl][vc] ← V[i][j] + 1
FSI
FTQ
FSI
FTQ
RENVOYER kvois
FIN
- Initialisation : On enfile la case de départ avec distance 0
- Boucle principale : Tant que la file n’est pas vide :
- On défile une case \((i, j)\)
- Si sa distance est exactement \(k\), on l’ajoute au résultat
- Sinon, on explore ses voisins non visités
- Pour chaque voisin, on lui attribue la distance \(V[i][j] + 1\)
- Résultat : File contenant toutes les cases à distance exactement \(k\)
2.3 Complexité
Pour un k-voisinage, on va potentiellement explorer un carré autour de la case de départ de côté \(2k+1\) (car on peut aller \(k\) fois à droite ou \(k\) fois à gauche en partant de la case). Et pour chaque case, nous allons avoir un maximum de 4 voisins, ainsi la complexité sera en \(O((2k+1)^2)=O(k^2)\).
| Caractéristique | BFS (File) | DFS (Pile) |
|---|---|---|
| Structure | File (FIFO) | Pile (LIFO) |
| Ordre d’exploration | Niveau par niveau | Profondeur d’abord |
| Plus court chemin | ✓ Garanti | ✗ Non garanti |
| Complexité | \(O(k^2)\) pour k-voisinage | \(O(n \times m)\) |
| Application | Distance, BFS | Backtracking, DFS |
3 Chemin le plus court
Afin de résoudre le parcours du labyrinthe mais en choisissant le chemin le plus court possible (s’il existe), nous allons modifier notre matrice de distance (voisinage) afin de ne pas pouvoir passer deux fois au même endroit et d’obtenir pour chaque case la plus petite distance à parcourir en partant de la case départ.
3.1 Pseudo-code
3.1.1 Distance minimale
On va parcourir les voisins et on regarde si la distance pour accéder aux voisins est meilleure, si c’est le cas, on enfile le voisin et on met à jour la matrice de distance. On initialise la matrice de distance avec le chemin le plus long, c’est-à-dire \(m \times n\).
ALGORITHME Distance(M, [l, c]):
DONNEES
M : matrice de taille n × m
[l, c] : coordonnées de la case de départ
VARIABLES
i, j, vl, vc : entiers
D : matrice de taille n × m initialisée à n×m
F, Voisin : files
DEBUT
D[l][c] ← 0
Enfiler(F, [l, c])
TQ EstVide(F) = FAUX FAIRE
i, j ← Defiler(F)
Voisin ← ChercheVoisin(M, [i, j])
TQ EstVide(Voisin) = FAUX FAIRE
vl, vc ← Defiler(Voisin)
SI D[vl, vc] > D[i][j] + 1 ALORS
Enfiler(F, [vl, vc])
D[vl][vc] ← D[i][j] + 1
FSI
FTQ
FTQ
RENVOYER D
FIN
- Initialisation : Toutes les cases ont une distance infinie (\(n \times m\)), sauf le départ (0)
- Relaxation : Pour chaque voisin, si on trouve un chemin plus court, on met à jour sa distance
- Garantie : Le BFS garantit que chaque case est atteinte par le plus court chemin
- Résultat : Matrice \(D\) contenant la distance minimale de chaque case au départ
Exemple :
Labyrinthe (0=libre, 1=mur): Matrice D (distances):
0 0 0 1 0 0 1 2 ∞ 4
1 0 1 1 0 ∞ 1 ∞ ∞ 3
0 0 0 0 0 2 1 2 3 2
Départ en (1,1), les cases inaccessibles restent à ∞
3.1.2 Chemin le plus court
On va maintenant se servir de la matrice distance pour trouver le chemin le plus court en partant de la fin et en cherchant quel voisin possède une distance égale à celle de notre case - 1 et ce, jusqu’à arriver à la case de départ. Pour l’avoir dans le bon ordre, nous utiliserons une pile.
ALGORITHME LePlusCourtChemin(M, D, [l, c]):
DONNEES
D : matrice de taille n × m (distances)
M : matrice de taille n × m (labyrinthe)
[l, c] : coordonnées de la case d'arrivée
VARIABLES
i, j, vl, vc : entiers
P : pile
Voisin : file
DEBUT
SI D[l][c] = m × n ALORS
Afficher("Pas de solution")
SINON
Empiler(P, [l, c])
TQ D[l][c] > 0 FAIRE
Voisin ← ChercheVoisin(M, [l, c])
TQ EstVide(Voisin) = FAUX FAIRE
i, j ← Defiler(Voisin)
SI D[i][j] = D[l][c] - 1 ALORS
Empiler(P, [i, j])
l, c ← i, j
TQ EstVide(Voisin) = FAUX FAIRE
Defiler(Voisin) // Vider le reste
FTQ
FSI
FTQ
FTQ
TQ EstVide(P) = FAUX FAIRE
Afficher(Depiler(P))
FTQ
FSI
FIN
Principe :
- On part de la case d’arrivée
- On cherche un voisin dont la distance est \(D[arrivée] - 1\)
- On empile ce voisin et on recommence depuis lui
- On s’arrête quand on atteint le départ (distance 0)
- On dépile pour afficher le chemin dans le bon ordre
Exemple avec la matrice D précédente :
Arrivée \((1,5)\) avec \(D[1][5] = 4\) : - Chercher voisin avec \(D = 3\) → trouve \((2,5)\) - Chercher voisin avec \(D = 2\) → trouve \((3,5)\) - Chercher voisin avec \(D = 1\) → trouve \((3,4)\) puis \((3,3)\) puis \((2,2)\) puis \((1,2)\) - Chercher voisin avec \(D = 0\) → trouve \((1,1)\) (départ) - Dépiler : \((1,1) \to (1,2) \to (1,3) \to ... \to (1,5)\)
Complexité : Il y a au pire 4 voisins à visiter, et une majoration possible du nombre de cases qui correspond au chemin est \(n \times m\). Ainsi la complexité de l’algorithme est en \(O(n\times m)\).
4 Exemple complet : plus court chemin dans un labyrinthe
Labyrinthe \(6 \times 6\) :
\[ \begin{bmatrix} 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 1 & 0 & 1 & 0 & 1 \\ 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 1 & 0 & 1 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 1 & 1 & 0 & 0 \\ \end{bmatrix} \]
Départ : \((1, 1)\) → Arrivée : \((6, 6)\)
Étape 1 : Calcul de la matrice D (distances) :
\[ D = \begin{bmatrix} 0 & 1 & 2 & \infty & 10 & 11 \\ 1 & \infty & 3 & \infty & 9 & \infty \\ 2 & 3 & 4 & 5 & 8 & 9 \\ \infty & \infty & 5 & \infty & \infty & 10 \\ 6 & 7 & 6 & 7 & 8 & 9 \\ 7 & \infty & \infty & \infty & 9 & 10 \\ \end{bmatrix} \]
Étape 2 : Reconstruction du chemin :
À partir de \((6,6)\) avec \(D[6][6] = 10\) : - Voisin avec \(D=9\) : \((6,5)\) - Voisin avec \(D=8\) : \((5,5)\) - Voisin avec \(D=7\) : \((5,4)\) - … - Voisin avec \(D=0\) : \((1,1)\)
Plus court chemin : 11 cases
\((1,1) \to (1,2) \to (1,3) \to (2,3) \to (3,3) \to (3,4) \to (3,5) \to (3,6) \to (4,6) \to (5,6) \to (6,6)\)
5 Applications et comparaisons
- Plus court chemin : dans un graphe non pondéré ou matrice
- Recherche de distance : k-voisinage, propagation
- Remplissage : algorithme de flood fill (traitement d’images)
- Parcours niveau par niveau : arbre de décision, génération
- Connectivité : trouver toutes les cases accessibles
6 Comparaison BFS vs DFS
| Critère | BFS (File) | DFS (Pile) |
|---|---|---|
| Structure | File FIFO | Pile LIFO |
| Exploration | Largeur (niveaux) | Profondeur (branches) |
| Plus court chemin | ✓ Oui | ✗ Non |
| Mémoire | \(O(k)\) pour niveau k | \(O(h)\) pour hauteur h |
| Chemin trouvé | Plus court | Quelconque |
| Utilisation | Distance, connectivité | Backtracking, cycles |
7 Points clés
- Le k-voisinage regroupe toutes les cases à distance exactement \(k\)
- Le BFS avec file explore niveau par niveau, garantissant le plus court chemin
- Deux matrices sont nécessaires :
- Matrice de distances \(D\) pour calculer les chemins minimaux
- Matrice du labyrinthe \(M\) pour vérifier les passages libres
- La reconstruction du chemin se fait en remontant de l’arrivée au départ
- Utilisation d’une pile pour inverser l’ordre du chemin
- Complexité \(O(n \times m)\) pour l’exploration complète de la matrice
- Le BFS est optimal pour les graphes non pondérés