Parcurgerea spațiului: BFS și DFS
Algoritmii de căutare sunt fundamentali în Inteligența Artificială. Ei explorează un spațiu de stări pentru a găsi o cale de la starea inițială la starea scop.
Concepte de bază
Spațiul de stări
Un spațiu de stări este definit de:
- Stare inițială: punctul de plecare
- Acțiuni: operațiile posibile din fiecare stare
- Model de tranziție: rezultatul aplicării unei acțiuni
- Test scop: verifică dacă am ajuns la soluție
- Cost cale (opțional): costul parcurgerii
Reprezentare ca graf
A (start)
/ \
B C
/| |\
D E F G (scop)
Căutarea în lățime (BFS)
Breadth-First Search explorează nivelurile grafului pe rând, de la cel mai apropiat la cel mai îndepărtat.
Caracteristici
- Folosește o coadă (FIFO) pentru frontieră
- Găsește calea cea mai scurtă (în număr de pași)
- Complet: găsește soluția dacă există
- Optim: doar dacă toate costurile sunt egale
Algoritm
from collections import deque
def bfs(graf, start, scop):
"""
Breadth-First Search.
Args:
graf: dicționar {nod: [vecini]}
start: nodul de start
scop: nodul țintă
Returns:
Lista nodurilor pe calea găsită sau None
"""
# Coadă: (nod_curent, cale_până_aici)
coada = deque([(start, [start])])
vizitat = {start}
while coada:
nod, cale = coada.popleft()
# Verifică dacă am ajuns la scop
if nod == scop:
return cale
# Explorează vecinii
for vecin in graf.get(nod, []):
if vecin not in vizitat:
vizitat.add(vecin)
coada.append((vecin, cale + [vecin]))
return None # Nu s-a găsit cale
Exemplu de utilizare
graf = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F', 'G'],
'D': ['B'],
'E': ['B'],
'F': ['C'],
'G': ['C']
}
cale = bfs(graf, 'A', 'G')
print(cale) # ['A', 'C', 'G']
Vizualizarea execuției
Pas 1: Coada = [A] Vizitat = {A}
Pas 2: Coada = [B, C] Vizitat = {A, B, C}
Pas 3: Coada = [C, D, E] Vizitat = {A, B, C, D, E}
Pas 4: Coada = [D, E, F, G] Vizitat = {A, B, C, D, E, F, G}
Pas 5: ... continuă până găsește G
Căutarea în adâncime (DFS)
Depth-First Search explorează cât mai adânc posibil pe o ramură înainte de a reveni.
Caracteristici
- Folosește o stivă (LIFO) sau recursivitate
- Nu garantează calea cea mai scurtă
- Complet: doar pentru grafuri finite (cu detecție de cicluri)
- Folosește mai puțină memorie decât BFS
Algoritm iterativ
def dfs_iterativ(graf, start, scop):
"""
Depth-First Search (iterativ).
Args:
graf: dicționar {nod: [vecini]}
start: nodul de start
scop: nodul țintă
Returns:
Lista nodurilor pe calea găsită sau None
"""
# Stivă: (nod_curent, cale_până_aici)
stiva = [(start, [start])]
vizitat = set()
while stiva:
nod, cale = stiva.pop()
if nod in vizitat:
continue
vizitat.add(nod)
# Verifică dacă am ajuns la scop
if nod == scop:
return cale
# Adaugă vecinii pe stivă (în ordine inversă pentru ordine corectă)
for vecin in reversed(graf.get(nod, [])):
if vecin not in vizitat:
stiva.append((vecin, cale + [vecin]))
return None
Algoritm recursiv
def dfs_recursiv(graf, start, scop, vizitat=None, cale=None):
"""
Depth-First Search (recursiv).
"""
if vizitat is None:
vizitat = set()
if cale is None:
cale = []
vizitat.add(start)
cale = cale + [start]
# Verifică dacă am ajuns la scop
if start == scop:
return cale
# Explorează vecinii
for vecin in graf.get(start, []):
if vecin not in vizitat:
rezultat = dfs_recursiv(graf, vecin, scop, vizitat, cale)
if rezultat is not None:
return rezultat
return None
Exemplu de utilizare
graf = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F', 'G'],
'D': ['B'],
'E': ['B'],
'F': ['C'],
'G': ['C']
}
cale = dfs_iterativ(graf, 'A', 'G')
print(cale) # ['A', 'B', 'D', 'E', 'C', 'F', 'G'] sau altă cale
Vizualizarea execuției
Pas 1: Stiva = [A] Vizitat = {A}
Pas 2: Stiva = [B, C] Vizitat = {A}
Pop C → Stiva = [B] Vizitat = {A, C}
Pas 3: Stiva = [B, F, G] Vizitat = {A, C}
Pop G → GĂSIT!
Comparație BFS vs DFS
| Caracteristică | BFS | DFS |
|---|---|---|
| Structură de date | Coadă (FIFO) | Stivă (LIFO) |
| Găsește calea minimă | Da | Nu |
| Complet | Da | Da (cu detecție cicluri) |
| Memorie | O(b^d) | O(b \cdot d) |
| Timp | O(b^d) | O(b^d) |
Unde b = factor de ramificare, d = adâncimea soluției.
Când să folosim fiecare?
BFS:
- Când căutăm calea cea mai scurtă
- Când soluția este aproape de rădăcină
- Când avem suficientă memorie
DFS:
- Când memoria este limitată
- Când soluția este adâncă în arbore
- Pentru detectarea ciclurilor
- Pentru parcurgerea completă a grafului
Variante și extensii
DFS cu limită de adâncime
def dfs_limitat(graf, start, scop, limita):
"""DFS cu adâncime maximă."""
return dfs_limitat_rec(graf, start, scop, limita, set(), [])
def dfs_limitat_rec(graf, nod, scop, limita, vizitat, cale):
if nod == scop:
return cale + [nod]
if limita <= 0:
return None
vizitat.add(nod)
for vecin in graf.get(nod, []):
if vecin not in vizitat:
rezultat = dfs_limitat_rec(graf, vecin, scop,
limita - 1, vizitat, cale + [nod])
if rezultat is not None:
return rezultat
vizitat.remove(nod)
return None
Iterative Deepening DFS (IDDFS)
Combină avantajele BFS (optimalitate) cu cele ale DFS (memorie redusă).
def iddfs(graf, start, scop, max_adancime=100):
"""Iterative Deepening Depth-First Search."""
for adancime in range(max_adancime):
rezultat = dfs_limitat(graf, start, scop, adancime)
if rezultat is not None:
return rezultat
return None
Aplicații practice
Labirint
def rezolva_labirint(labirint, start, scop):
"""
Rezolvă un labirint folosind BFS.
labirint: matrice 2D (0 = liber, 1 = zid)
start, scop: tuple (linie, coloana)
"""
linii, coloane = len(labirint), len(labirint[0])
directii = [(0, 1), (1, 0), (0, -1), (-1, 0)] # dreapta, jos, stânga, sus
coada = deque([(start, [start])])
vizitat = {start}
while coada:
(l, c), cale = coada.popleft()
if (l, c) == scop:
return cale
for dl, dc in directii:
nl, nc = l + dl, c + dc
if (0 <= nl < linii and 0 <= nc < coloane and
labirint[nl][nc] == 0 and (nl, nc) not in vizitat):
vizitat.add((nl, nc))
coada.append(((nl, nc), cale + [(nl, nc)]))
return None
# Exemplu
labirint = [
[0, 0, 1, 0],
[1, 0, 1, 0],
[0, 0, 0, 0],
[0, 1, 1, 0]
]
cale = rezolva_labirint(labirint, (0, 0), (3, 3))
print(cale) # [(0, 0), (0, 1), (1, 1), (2, 1), (2, 2), (2, 3), (3, 3)]
Verificarea conexității
def este_conex(graf):
"""Verifică dacă graful este conex folosind DFS."""
if not graf:
return True
start = next(iter(graf))
vizitat = set()
def dfs(nod):
vizitat.add(nod)
for vecin in graf.get(nod, []):
if vecin not in vizitat:
dfs(vecin)
dfs(start)
return len(vizitat) == len(graf)
Detectarea ciclurilor
def are_ciclu(graf):
"""Detectează dacă graful are cicluri folosind DFS."""
vizitat = set()
in_stiva = set()
def dfs(nod):
vizitat.add(nod)
in_stiva.add(nod)
for vecin in graf.get(nod, []):
if vecin not in vizitat:
if dfs(vecin):
return True
elif vecin in in_stiva:
return True
in_stiva.remove(nod)
return False
for nod in graf:
if nod not in vizitat:
if dfs(nod):
return True
return False
Sortare topologică
def sortare_topologica(graf):
"""Sortare topologică folosind DFS."""
vizitat = set()
rezultat = []
def dfs(nod):
vizitat.add(nod)
for vecin in graf.get(nod, []):
if vecin not in vizitat:
dfs(vecin)
rezultat.append(nod)
for nod in graf:
if nod not in vizitat:
dfs(nod)
return rezultat[::-1]