Теоретический билет. Ниже: определение → существование → два алгоритма (DFS и Кан) с иллюстрациями → корректность → асимптотика → что говорить на устном.
ОпределениеСуществованиеDFS-алгоритмАлгоритм КанаКак отвечать
Рёбра: 0→1, 0→2, 1→2, 1→3, 2→3
Топологический порядок: 0, 1, 2, 3
Каждая стрелка идёт слева направо. Это и значит σ(u)<σ(v) для всех рёбер.
Порядок не единственный: например 0, 1, 2, 3 — единственный здесь, но в разреженных графах вариантов бывает много.
В одну сторону (есть цикл → сортировки нет). Порядок «меньше» сам по себе ацикличен: нельзя иметь σ(a)<σ(b)<…<σ(a). Если в графе есть цикл a→b→…→a, то по рёбрам цикла требовалось бы σ(a)<σ(b)<…<σ(a) — противоречие. Значит при наличии цикла сортировки не существует.
В обратную сторону (нет цикла → сортировка есть). Доказывается конструктивно — приведением алгоритма, который её строит (см. ниже). Раз алгоритм всегда находит порядок на любом DAG, значит он существует.
Цикл a→b→c→a: куда ни поставь a, найдётся ребро, идущее «назад». Топопорядка нет.
Ключевое наблюдение: при обходе в глубину время выхода t_out[v] всегда больше, чем t_out любой вершины, достижимой из v. Почему — потому что все достижимые из v вершины посещаются либо до вызова DFS(v), либо во время него (внутри его рекурсии), и из них выходят раньше, чем из самого v.
Значит для любого ребра (u, v): из u выходим позже, чем из v, то есть t_out[u] > t_out[v]. Если отсортировать по убыванию t_out — u окажется раньше v. Это ровно условие σ(u)<σ(v). А так как мы добавляем вершины в порядке возрастания t_out, достаточно развернуть массив.
0→1, 0→2, 1→2, 1→3, 2→3Запускаем DFS из 0. Условно идём по соседям в порядке возрастания номера.
| событие | t_out | массив (по выходу) |
|---|---|---|
| выход из 3 | 1 | [3] |
| выход из 2 | 2 | [3, 2] |
| выход из 1 | 3 | [3, 2, 1] |
| выход из 0 | 4 | [3, 2, 1, 0] |
import sys def topo_sort_dfs(n, adj): color = [0] * n # 0 белый, 1 серый, 2 чёрный order = [] has_cycle = [False] def dfs(v): color[v] = 1 for u in adj[v]: if color[u] == 0: dfs(u) elif color[u] == 1: # ребро в серую = цикл has_cycle[0] = True color[v] = 2 order.append(v) # добавляем при ВЫХОДЕ for v in range(n): if color[v] == 0: dfs(v) return order[::-1] if not has_cycle[0] else None
На Python длинные цепочки могут переполнить стек рекурсии — поднять лимит sys.setrecursionlimit(...) или писать итеративно.
indeg[v] — число входящих рёбер (один проход по рёбрам).indeg = 0.indeg -= 1; кто стал 0 — в очередь.Вершину берём, только когда её indeg стал 0, то есть все её предшественники уже обработаны раньше. Значит по каждому ребру u→v вершина u вышла раньше v — ровно условие топосорта.
У вершин цикла indeg никогда не обнулится: они держат друг другу по входящему ребру внутри цикла. Они не попадут в очередь → обработано меньше N вершин ⇔ цикл.
| шаг | очередь | берём | indeg после | порядок |
|---|---|---|---|---|
| старт | [0] | — | 1:1 2:2 3:2 | [] |
| 1 | [1] | 0 | 1:0 2:1 3:2 | [0] |
| 2 | [2] | 1 | 2:0 3:1 | [0,1] |
| 3 | [3] | 2 | 3:0 | [0,1,2] |
| 4 | [] | 3 | — | [0,1,2,3] |
from collections import deque def topo_sort_kahn(n, adj): indeg = [0] * n for v in range(n): for u in adj[v]: indeg[u] += 1 dq = deque(v for v in range(n) if indeg[v] == 0) order = [] while dq: v = dq.popleft() order.append(v) for u in adj[v]: indeg[u] -= 1 if indeg[u] == 0: dq.append(u) return order if len(order) == n else None # None = есть цикл
| DFS-версия | Кан (indeg) | |
|---|---|---|
| Идея | сортировка по убыванию t_out | снимаем вершины с indeg=0 |
| Время / Память | O(V+E) / O(V) | O(V+E) / O(V+E) |
| Рекурсия | да (риск переполнения стека) | нет, итеративно |
| Детекция цикла | ребро в серую вершину | обработано < N |
| Лекс. мин. порядок | неудобно | легко: min-куча вместо deque |