# теория · шаг 2 из 7
Алгоритм Кана: порядок по входящим степеням
Коротко
Второй способ получить топологический порядок, и он не про обход, а про подсчёт. Для каждой вершины
считают входящую степень - сколько рёбер в неё ведёт. Вершины с нулевой входящей степенью ни
от чего не зависят: их можно делать сразу. Берём такую вершину, выписываем в ответ, «снимаем» её
рёбра (у каждого соседа уменьшаем входящую степень на единицу) - и если чья-то степень стала
нулём, он тоже готов. Повторяем, пока есть готовые. Цена та же O(V + E). Два подарка сверх
порядка: если выписаны НЕ все вершины, значит остался цикл (и застрявшие вершины - как раз те, что
завязаны на него), а если обрабатывать готовые вершины целыми ПАЧКАМИ, получаются уровни
параллельного выполнения.
Если ты умеешь писать алгоритм Кана, знаешь его критерий цикла (выписано меньше V вершин) и понимаешь, как из него получаются уровни параллельности, листай до «Теперь сам».
Как думаешь, с чего начать, если обхода нет
Забудь на минуту про обход в глубину. Дан граф задач с зависимостями, и надо выдать порядок выполнения. Прежде чем читать дальше, ответь: какую задачу можно взять ПЕРВОЙ - по какому признаку её видно? И что изменится в графе, когда она сделана?
На что это похоже
Разбор завала на складе. Взять можно только ту коробку, на которой ничего не стоит. Снял её - и коробки, которые она придавливала, могли освободиться; если освободились, их тоже можно брать. Так, слой за слоем, разбирается весь завал. А если в какой-то момент свободных коробок нет, а завал остался - значит там коробки стоят друг на друге по кругу, чего в реальности не бывает, а в графе зависимостей случается сплошь и рядом.
Механизм: считаем входящие степени
from collections import deque
indeg = [0] * V
for u in range(V):
for v in adj[u]:
indeg[v] += 1 # считаем, сколько рёбер ВЕДЁТ в каждую вершину
q = deque(v for v in range(V) if indeg[v] == 0) # готовые: ни от чего не зависят
order = []
while q:
v = q.popleft()
order.append(v)
for u in adj[v]:
indeg[u] -= 1 # сняли ребро v → u
if indeg[u] == 0: # у соседа больше нет зависимостей
q.append(u)
if len(order) < V:
print("в графе цикл; в порядок попали только", len(order), "вершин")
Почему это O(V + E): подсчёт степеней проходит по всем спискам смежности (E), каждая вершина
попадает в очередь не больше одного раза (V), и каждое ребро «снимается» ровно один раз (E).
Две полезные модификации:
уровни параллельности : обрабатывать не по одной вершине, а ВЕСЬ текущий слой очереди
целиком; число слоёв = длина самой длинной цепочки зависимостей
лексикографически
наименьший порядок : из всех допустимых - тот, что раньше прочих уходит в
меньший номер (как слова в словаре). Заменить очередь на
очередь с ПРИОРИТЕТАМИ (кучу); цена станет O(V·log V + E)
Правило
Алгоритм Кана: посчитать входящие степени, положить в очередь все вершины с нулевой степенью;
доставая вершину, выписывать её в ответ и уменьшать степень каждого соседа, добавляя в очередь тех,
у кого степень стала нулём. Цена O(V + E). Если выписано меньше V вершин, в графе есть цикл, и
невыписанное множество содержит циклы и всё, что от них зависит. Обработка очереди слоями даёт
уровни параллельного выполнения; замена очереди кучей даёт лексикографически наименьший порядок
(как слова в словаре) за O(V·log V + E).
Разбор: разбираем граф по шагам и по уровням
Граф: a→c, b→c, c→d, b→e, e→d.
Входящие степени: a: 0, b: 0, c: 2 (из a и b), d: 2 (из c и e), e: 1 (из b).
очередь: [a, b] order = []
взяли a → снимаем a→c: indeg[c] = 1 order = [a]
взяли b → снимаем b→c: indeg[c] = 0 → в очередь
снимаем b→e: indeg[e] = 0 → в очередь order = [a, b], очередь: [c, e]
взяли c → снимаем c→d: indeg[d] = 1 order = [a, b, c]
взяли e → снимаем e→d: indeg[d] = 0 → в очередь order = [a, b, c, e]
взяли d order = [a, b, c, e, d]
Выписаны все пять вершин - циклов нет, порядок годится: проверь, что каждое из пяти рёбер смотрит вперёд.
Теперь то же самое СЛОЯМИ. Первый слой - всё, что было в очереди сначала: {a, b}. Снимаем их
рёбра целиком, смотрим, кто освободился: {c, e} - это второй слой. Снимаем их рёбра: освободилась
d - третий слой. Итого три уровня: {a, b}, {c, e}, {d}. Читается это так: если задачи
внутри слоя выполнять параллельно, всё дело займёт три такта вместо пяти, а число тактов равно
длине самой длинной цепочки зависимостей (a→c→d или b→e→d) - её называют критическим путём.
Именно так планировщики сборки решают, что можно компилировать одновременно.
Разбор: почему застрявшие вершины - это в точности цикл
Пусть алгоритм остановился, а вершины остались. Докажем, что среди них есть цикл.
У каждой оставшейся вершины входящая степень больше нуля - иначе она лежала бы в очереди. Значит в каждую ведёт ребро из другой ОСТАВШЕЙСЯ вершины (рёбра из выписанных уже сняты). Теперь пойдём от любой оставшейся вершины ПРОТИВ стрелок: из неё всегда есть куда шагнуть назад, потому что входящая степень не ноль. Шагаем сколько угодно раз, а вершин конечное число - значит рано или поздно мы попадём в вершину, где уже были. Кусок пути между двумя посещениями и есть цикл.
Обратное тоже верно: вершины цикла никогда не освободятся, потому что каждая ждёт предыдущую по
кругу. Отсюда точный критерий: алгоритм выписал все V вершин тогда и только тогда, когда граф
ацикличен, - и он даёт бонус, которого нет у обхода в глубину: список застрявших вершин,
который удобно показать пользователю как «вот эти зависимости не разрешаются».
Что ломается без этого
Первое: уменьшать входящую степень и класть вершину в очередь без проверки на ноль - вершина
попадёт в ответ несколько раз, и порядок сломается. Второе: считать входящие степени по
неориентированному графу - там у каждого ребра два конца, и «нулевая входящая степень» ничего не
значит; алгоритм работает только на ориентированных. Третье: остановиться, когда очередь опустела,
и не сравнить len(order) с V - цикл останется незамеченным, а на выходе будет частичный
порядок, который выглядит правдоподобно. Четвёртое: ждать от очереди лексикографического порядка -
обычная очередь даёт какой-то допустимый порядок, для наименьшего нужна куча. Пятое: пересчитывать
входящие степени заново после каждого снятия - это превращает O(V + E) в O(V·E); степени
изменяются по месту.
Зачем это в работе
Кан против обхода в глубину - выбор по обстоятельствам, и оба нужны. Алгоритм Кана ИТЕРАТИВЕН по своей природе, то есть не упирается в глубину рекурсии; он даёт понятное сообщение о циклической зависимости (вот застрявшие вершины) и естественно раскладывает задачи по уровням параллельности - именно поэтому его выбирают в системах сборки и планировщиках, где надо загрузить все ядра. Топологическая сортировка обходом в глубину короче и бесплатна, если обход всё равно делается (например, вместе с поиском компонент сильной связности - следующий урок). Практическое правило: нужен просто порядок и обход уже есть - бери обход; нужны уровни, отчёт о циклах или граф большой и глубокий - бери Кана. А уровни из Кана - это ещё и оценка «за сколько тактов это вообще можно сделать», то есть нижняя граница времени при любом числе исполнителей.
Теперь сам
Граф: 1→2, 1→3, 2→4, 3→4, 4→5, 6→3. Посчитай входящие степени, проведи алгоритм Кана
(очередь по возрастанию номера) и разложи вершины по уровням параллельности. Сколько тактов займёт
выполнение, если внутри уровня всё делается одновременно?
Ответ. Входящие степени: 1: 0, 2: 1 (из 1), 3: 2 (из 1 и 6), 4: 2 (из 2 и 3), 5: 1
(из 4), 6: 0. Начальная очередь - вершины с нулём: [1, 6]. Берём 1: снимаем 1→2
(indeg[2]=0 → в очередь), снимаем 1→3 (indeg[3]=1). Берём 6: снимаем 6→3 (indeg[3]=0 → в
очередь). Очередь: [2, 3]. Берём 2: снимаем 2→4 (indeg[4]=1). Берём 3: снимаем 3→4
(indeg[4]=0 → в очередь). Берём 4: снимаем 4→5 (indeg[5]=0 → в очередь). Берём 5. Итог:
порядок 1, 6, 2, 3, 4, 5, выписаны все шесть вершин - циклов нет. Уровни: первый - {1, 6}
(ни от чего не зависят), второй - {2, 3} (освободились после первого), третий - {4}, четвёртый -
{5}. Значит четыре такта, и это длина самой длинной цепочки: 1→3→4→5 (или 6→3→4→5) -
четыре вершины. Обрати внимание, что вершин шесть, а тактов четыре: параллельность сэкономила два
такта, и больше сэкономить нельзя - критический путь не сжимается никаким числом исполнителей.
Главное
Алгоритм Кана строит топологический порядок счётом, а не обходом: посчитать входящие степени,
положить в очередь вершины с нулевой степенью, а дальше доставать вершину, выписывать её и
уменьшать степень соседей, добавляя освободившихся. Цена O(V + E): каждая вершина входит в
очередь раз, каждое ребро снимается раз. Критерий цикла точный и полезный: если выписано меньше
V вершин, в графе есть цикл, а застрявшее множество содержит циклы и всё, что от них зависит
(доказывается ходом против стрелок: из каждой застрявшей вершины есть куда шагнуть назад, а вершин
конечное число, значит рано или поздно попадём в уже посещённую). Обработка очереди целыми СЛОЯМИ даёт уровни параллельного выполнения, и число
слоёв равно длине критического пути - нижней границе времени при любом числе исполнителей. Замена
очереди кучей даёт лексикографически наименьший порядок за O(V·log V + E). Кан итеративен и
потому не упирается в глубину рекурсии; обход в глубину короче, если он всё равно делается.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий