Перейти к содержанию
Educora
Университет25 мин50 / 59

Графы и алгоритмы на графах

Пошагово изучи способы представления графов, поиск в ширину (BFS) и в глубину (DFS), алгоритм Дейкстры, топологическую сортировку и минимальное остовное дерево (Краскал).

Проверь себя
В этом уроке ты узнаешь
  • Представлять граф матрицей или списками смежности и обосновывать выбор
  • Вручную трассировать BFS, DFS и алгоритм Дейкстры
  • Находить топологический порядок и минимальное остовное дерево

Навигатор за секунду находит кратчайший путь из Баку в Шеки, социальная сеть предлагает «людей, которых вы можете знать», а университет выстраивает порядок курсов так, чтобы никто не записался на «Алгоритмы» раньше «Программирования». Три разные задачи — одна модель: граф. Граф описывает объекты (вершины) и связи между ними (рёбра) и является одной из самых употребительных моделей информатики.

Графы и их представление

Определение
Граф G = (V, E)

Множество вершин V и множество рёбер E (пар вершин). Рёбра бывают неориентированными (дружба) и ориентированными (A → B, «подписан на»); если у каждого ребра есть вес (расстояние, цена, время), граф называется взвешенным. Степень вершины — число инцидентных ей рёбер.

∑ deg(v) = 2|E|, |E| ≤ |V| · (|V| − 1) / 2∑ deg(v) = 2|E|, |E| ≤ |V| · (|V| − 1) / 2
где:
  • deg(v)степень вершины v
  • |V|, |E|число вершин и рёбер

«Лемма о рукопожатиях»: каждое неориентированное ребро добавляет к сумме степеней 2 — по одному на каждом конце. Второе неравенство — максимальное число рёбер в простом неориентированном графе.

СвойствоМатрица смежностиСписки смежности
памятьO(V²)O(V + E)
есть ли ребро (u, v)?O(1)O(deg u)
обойти всех соседей uO(V)O(deg u)
когда выбиратьплотный граф (E ≈ V²)разреженный граф (дороги, соцсети)

Весь урок мы будем пользоваться одним графом-примером. Шесть вершин A–F и взвешенные неориентированные рёбра: A–B 4, A–C 2, B–C 1, B–D 5, C–D 8, C–E 10, D–E 2, D–F 6, E–F 3. Представь, что веса — это километры.

Поиск в ширину (BFS) и в глубину (DFS)

BFS расходится от стартовой вершины «волной»: сначала все соседи, потом соседи соседей. Он использует очередь и в невзвешенном графе даёт кратчайшие пути по числу рёбер. DFS идёт по одному пути как можно глубже и возвращается назад в тупиках; он работает со стеком (или рекурсией) и применяется для проверки связности, поиска циклов и топологической сортировки. Оба просматривают каждую вершину и каждое ребро один раз: O(V + E).

Пример 1: BFS и DFS из F

Не учитывая веса, выполни BFS и DFS из F на графе-примере. Соседей бери по алфавиту: A: B, C; B: A, C, D; C: A, B, D, E; D: B, C, E, F; E: C, D, F; F: D, E.

Показать решение
BFS (очередь): [F] → берём F, добавляем D и E (расстояние 1) → берём D, добавляем B и C (расстояние 2) → у E новых соседей нет → берём B, добавляем A (расстояние 3).
Уровни: F | D, E | B, C | A.
DFS (рекурсия): F → D (первый сосед F) → B (первый непосещённый сосед D) → A → C (сосед A) → E (непосещённый сосед C).
Порядок: F, D, B, A, C, E — DFS добрался до A за 4 шага, а BFS нашёл кратчайший путь (3 ребра).
Python
from collections import deque

graph = {'A': ['B', 'C'], 'B': ['A', 'C', 'D'], 'C': ['A', 'B', 'D', 'E'],
         'D': ['B', 'C', 'E', 'F'], 'E': ['C', 'D', 'F'], 'F': ['D', 'E']}

def bfs(start):
    dist = {start: 0}
    queue = deque([start])
    while queue:
        v = queue.popleft()
        for w in graph[v]:
            if w not in dist:
                dist[w] = dist[v] + 1
                queue.append(w)
    return dist

def dfs(v, seen):
    seen.append(v)
    for w in graph[v]:
        if w not in seen:
            dfs(w, seen)
    return seen

print('BFS from F:', bfs('F'))
print('DFS from F:', dfs('F', []))
▸ Ожидаемый результат
BFS from F: {'F': 0, 'D': 1, 'E': 1, 'B': 2, 'C': 2, 'A': 3}
DFS from F: ['F', 'D', 'B', 'A', 'C', 'E']
Списки смежности хранятся в словаре. BFS возвращает расстояния в порядке обнаружения вершин; порядок DFS совпадает с ручной трассировкой.

Алгоритм Дейкстры: кратчайшие пути во взвешенном графе

Если у рёбер есть веса, BFS не подходит: три коротких ребра могут оказаться дешевле одного длинного. Алгоритм Дейкстры (для неотрицательных весов) на каждом шаге выбирает среди ещё не окончательных вершин ту, у которой расстояние наименьшее (очередь с приоритетом), объявляет её окончательной и обновляет расстояния соседей релаксацией.

dist[v] ← min(dist[v], dist[u] + w(u, v)); T = O((V + E) · log V)
где:
  • dist[v]найденное на данный момент кратчайшее расстояние от источника до v
  • w(u, v)вес ребра u–v (≥ 0)
  • Tвремя работы с двоичной кучей
Шаг: зафиксированаABCDEF
0: начало0∞∞∞∞∞
1: A (0)042∞∞∞
2: C (2)0321012∞
3: B (3)032812∞
4: D (8)03281014
5: E (10)03281013
6: F (13)03281013
Трассировка Дейкстры из A (значения dist после каждого шага). На шаге 2 B уменьшается с 4 до 3 (A → C → B), на шагах 4 и 5 улучшаются E и F.
Пример 2: восстановление пути

По таблице найди кратчайший путь из A в F и его длину.

Показать решение
При каждом обновлении запоминаем, «откуда пришли» (prev): F последний раз обновлена из E (10 + 3), E — из D (8 + 2), D — из B (3 + 5), B — из C (2 + 1), C — из A (0 + 2).
С конца назад: F ← E ← D ← B ← C ← A.
Путь: A → C → B → D → E → F, длина 2 + 1 + 5 + 2 + 3 = 13.
Заметь: очевидный на вид путь A → B → D → F равен 4 + 5 + 6 = 15.
Python
import heapq

edges = [('A', 'B', 4), ('A', 'C', 2), ('B', 'C', 1), ('B', 'D', 5), ('C', 'D', 8),
         ('C', 'E', 10), ('D', 'E', 2), ('D', 'F', 6), ('E', 'F', 3)]
adj = {}
for u, v, w in edges:
    adj.setdefault(u, []).append((v, w))
    adj.setdefault(v, []).append((u, w))

def dijkstra(src):
    dist, prev, pq = {src: 0}, {}, [(0, src)]
    while pq:
        d, u = heapq.heappop(pq)
        if d > dist[u]:
            continue
        for v, w in adj[u]:
            if d + w < dist.get(v, float('inf')):
                dist[v], prev[v] = d + w, u
                heapq.heappush(pq, (d + w, v))
    return dist, prev

dist, prev = dijkstra('A')
path = ['F']
while path[-1] != 'A':
    path.append(prev[path[-1]])
print(dict(sorted(dist.items())))
print(' -> '.join(reversed(path)), '=', dist['F'])
▸ Ожидаемый результат
{'A': 0, 'B': 3, 'C': 2, 'D': 8, 'E': 10, 'F': 13}
A -> C -> B -> D -> E -> F = 13
Роль очереди с приоритетом играет heapq. Вершина может попасть в очередь несколько раз; устаревшие записи отбрасывает условие d > dist[u] («ленивое удаление»).

Топологическая сортировка и минимальное остовное дерево

Топологическая сортировка упорядочивает вершины ориентированного ациклического графа (DAG) так, чтобы для каждого ребра u → v вершина u шла раньше v: пререквизиты курсов, порядок сборки модулей программы, порядок пересчёта ячеек в Excel. Алгоритм Кана: помести вершины с входящей степенью 0 в очередь, извлеки одну, удали её рёбра и добавь в очередь вершины, чья входящая степень упала до 0. Если вышло меньше V вершин, в графе есть цикл. Время: O(V + E).

Python
from collections import deque

courses = {'Programming': ['DataStructures', 'Databases'], 'Math': ['Algorithms'],
           'DataStructures': ['Algorithms'], 'Databases': ['AI'],
           'Algorithms': ['AI'], 'AI': []}

def topo_sort(g):
    indeg = {v: 0 for v in g}
    for v in g:
        for w in g[v]:
            indeg[w] += 1
    queue = deque(v for v in g if indeg[v] == 0)
    order = []
    while queue:
        v = queue.popleft()
        order.append(v)
        for w in g[v]:
            indeg[w] -= 1
            if indeg[w] == 0:
                queue.append(w)
    return order if len(order) == len(g) else None

print(topo_sort(courses))
print(topo_sort({'X': ['Y'], 'Y': ['Z'], 'Z': ['X']}))
▸ Ожидаемый результат
['Programming', 'Math', 'DataStructures', 'Databases', 'Algorithms', 'AI']
None
Для графа курсов получаем допустимый порядок; для графа с циклом X → Y → Z → X возвращается None — при таких требованиях ни один курс начать нельзя.
Определение
Минимальное остовное дерево (MST)

Множество рёбер, которое соединяет все вершины связного взвешенного графа, не содержит циклов и имеет наименьший возможный суммарный вес. В нём всегда ровно V − 1 рёбер. Применения: соединить сёла минимальным количеством кабеля, проектирование электро- и водопроводных сетей, кластеризация.

Пример 3: алгоритм Краскала

Найди минимальное остовное дерево графа-примера алгоритмом Краскала: бери рёбра по возрастанию веса и оставляй те, что не образуют цикла.

Показать решение
Порядок: B–C 1, A–C 2, D–E 2, E–F 3, A–B 4, B–D 5, D–F 6, C–D 8, C–E 10.
B–C 1 ✓ → {B, C}
A–C 2 ✓ → {A, B, C}
D–E 2 ✓ → {D, E}
E–F 3 ✓ → {D, E, F}
A–B 4 ✗ — A и B уже в одной компоненте (замкнулся бы цикл A–C–B)
B–D 5 ✓ → все вершины соединены; 5 = V − 1 рёбер, стоп.
MST: {B–C, A–C, D–E, E–F, B–D}, суммарный вес 1 + 2 + 2 + 3 + 5 = 13.
Python
edges = [('A', 'B', 4), ('A', 'C', 2), ('B', 'C', 1), ('B', 'D', 5), ('C', 'D', 8),
         ('C', 'E', 10), ('D', 'E', 2), ('D', 'F', 6), ('E', 'F', 3)]
parent = {v: v for v in 'ABCDEF'}

def find(v):
    while parent[v] != v:
        v = parent[v]
    return v

mst = []
for u, v, w in sorted(edges, key=lambda e: e[2]):
    ru, rv = find(u), find(v)
    if ru != rv:
        parent[ru] = rv
        mst.append((u, v, w))
print(mst)
print('total weight:', sum(w for _, _, w in mst))
▸ Ожидаемый результат
[('B', 'C', 1), ('A', 'C', 2), ('D', 'E', 2), ('E', 'F', 3), ('B', 'D', 5)]
total weight: 13
На вопрос «образует ли ребро цикл?» отвечает структура система непересекающихся множеств (union–find): find находит «представителя» компоненты, и если у двух вершин он общий, ребро замкнуло бы цикл. Время Краскала определяется сортировкой: O(E log E). Алгоритм Прима же выращивает дерево из одной вершины (похоже на Дейкстру, с очередью с приоритетом).

Главное

  • Граф G = (V, E); для разреженных графов — списки смежности (O(V + E)), для плотных — матрица (O(V²)).
  • BFS идёт по уровням с очередью и даёт кратчайшие пути без весов; DFS идёт вглубь со стеком; оба — O(V + E).
  • Дейкстра работает с неотрицательными весами, очередью с приоритетом и релаксацией за O((V + E) log V).
  • Топологический порядок существует только в DAG; алгоритм Кана заодно обнаруживает циклы.
  • В MST V − 1 рёбер; Краскал сортирует рёбра по весу и избегает циклов с помощью union–find.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
В дорожной сети V = 10⁴ перекрёстков и E = 3 · 10⁴ дорог. Сколько ячеек нужно матрице смежности?