Обход в глубину (DFS)
https://t.me/solidityset
Пример 1
def dfs(graph, start_node, visited=None):
"""Обход графа в глубину (рекурсивный)."""
# При первом вызове создаём пустое множество посещённых узлов
# Почему не visited=set() в аргументах? Это ловушка Python:
# изменяемые объекты по умолчанию создаются ОДИН РАЗ для всех вызовов!
if visited is None:
visited = set()
visited.add(start_node) # Помечаем текущий узел как посещённый
result = [start_node] # Добавляем его в результат
for neighbor in graph.get(start_node, []): # Для каждого соседа
if neighbor not in visited: # Если ещё не были там
result.extend(dfs(graph, neighbor, visited)) # Уходим глубже!
return result
# Наш граф
graph = {
"A": ["B", "C"],
"B": ["A", "D", "E"],
"C": ["A", "F"],
"D": ["B"],
"E": ["B", "F"],
"F": ["C", "E"]
}
print(dfs(graph, "A"))
# Вывод: ['A', 'B', 'D', 'E', 'F', 'C']
Пример 2
dfs(A)
├─ visited = {A}, result = [A]
├─ сосед B → не посещён
│ dfs(B)
│ ├─ visited = {A,B}, result = [B]
│ ├─ сосед A → уже посещён, пропуск
│ ├─ сосед D → не посещён
│ │ dfs(D)
│ │ ├─ visited = {A,B,D}, result = [D]
│ │ ├─ сосед B → уже посещён, пропуск
│ │ └─ return [D] ← ТУПИК, возврат
│ ├─ сосед E → не посещён
│ │ dfs(E)
│ │ ├─ visited = {A,B,D,E}, result = [E]
│ │ ├─ сосед B → уже посещён, пропуск
│ │ ├─ сосед F → не посещён
│ │ │ dfs(F)
│ │ │ ├─ visited = {A,B,D,E,F}, result = [F]
│ │ │ ├─ сосед C → не посещён
│ │ │ │ dfs(C)
│ │ │ │ ├─ visited = {A,B,D,E,F,C}, result = [C]
│ │ │ │ ├─ сосед A → посещён, пропуск
│ │ │ │ ├─ сосед F → посещён, пропуск
│ │ │ │ └─ return [C]
│ │ │ ├─ сосед E → посещён, пропуск
│ │ │ └─ return [F, C]
│ │ └─ return [E, F, C]
│ └─ return [B, D, E, F, C]
└─ return [A, B, D, E, F, C]
Пример 3
def dfs_iterative(graph, start_node):
"""Обход графа в глубину (итеративный, с явным стеком)."""
visited = set() # Множество посещённых узлов
stack = [start_node] # Стек: начинаем с начального узла
result = []
while stack: # Пока стек не пуст
node = stack.pop() # Берём верхний элемент стека
if node in visited: # Если уже посещали — пропускаем
continue
visited.add(node) # Помечаем как посещённый
result.append(node) # Добавляем в результат
# Добавляем соседей в стек (в обратном порядке, чтобы сохранить
# тот же порядок обхода, что и в рекурсивной версии)
for neighbor in reversed(graph.get(node, [])):
if neighbor not in visited:
stack.append(neighbor)
return result
print(dfs_iterative(graph, "A"))
# Вывод: ['A', 'B', 'D', 'E', 'F', 'C']
Пример 4
def has_cycle(graph):
"""Определяет наличие цикла в ориентированном графе."""
visited = set() # Все посещённые узлы
rec_stack = set() # Узлы в текущем пути рекурсии
def dfs_cycle(node):
visited.add(node)
rec_stack.add(node) # Вошли в узел — добавляем в путь
for neighbor in graph.get(node, []):
if neighbor not in visited:
if dfs_cycle(neighbor): # Рекурсивно проверяем
return True
elif neighbor in rec_stack: # Сосед уже в нашем пути — ЦИКЛ!
return True
rec_stack.remove(node) # Покидаем узел — убираем из пути
return False
# Запускаем DFS из каждого непосещённого узла
# (граф может быть несвязным)
for node in graph:
if node not in visited:
if dfs_cycle(node):
return True
return False
# Тест 1: граф без цикла
graph_no_cycle = {
"A": ["B", "C"],
"B": ["D"],
"C": [],
"D": []
}
print(has_cycle(graph_no_cycle)) # False
# Тест 2: граф с циклом (B → D → B)
graph_with_cycle = {
"A": ["B"],
"B": ["D"],
"D": ["B"] # ← вот он, цикл!
}
print(has_cycle(graph_with_cycle)) # True
Пример 5
Обход graph_with_cycle:
dfs(A):
rec_stack = {A}
→ сосед B, не посещён
dfs(B):
rec_stack = {A, B}
→ сосед D, не посещён
dfs(D):
rec_stack = {A, B, D}
→ сосед B, уже посещён И находится в rec_stack!
← ЦИКЛ НАЙДЕН! return True
Пример 6
# DFS для поиска любого пути в лабиринте
def find_exit_dfs(maze, start, exit_point):
"""
maze: словарь {узел: [соседи]}
Возвращает путь от start до exit_point, если он существует.
"""
visited = set()
path = []
def dfs(node):
if node in visited:
return False
visited.add(node)
path.append(node)
if node == exit_point: # Нашли выход!
return True
for neighbor in maze.get(node, []):
if dfs(neighbor): # Если этот путь ведёт к выходу
return True
path.pop() # Тупик — убираем из пути (backtracking)
return False
if dfs(start):
return path
return None # Выхода нет
# Простой лабиринт в виде графа
maze = {
"S": ["A", "B"],
"A": ["S", "C"],
"B": ["S", "D"],
"C": ["A", "E"],
"D": ["B", "E"],
"E": ["C", "D", "EXIT"],
"EXIT": ["E"]
}
print(find_exit_dfs(maze, "S", "EXIT"))
# Один из возможных путей: ['S', 'A', 'C', 'E', 'EXIT']