3.1.3 — порядок выполнения задач и структура ориентированного графа3.1.3 — task ordering and the structure of a directed graph
Две связанные задачи: как упорядочить вершины DAG так, чтобы каждое ребро указывало "вперёд" (топологическая сортировка), и как найти группы вершин, взаимно достижимые друг из друга в ориентированном графе (компоненты сильной связности). Обе опираются на DFS и его tin/tout из 3.1.2.Two related problems: how to order a DAG's vertices so every edge points "forward" (topological sort), and how to find groups of vertices mutually reachable from each other in a directed graph (strongly connected components). Both build on DFS and its tin/tout from 3.1.2.
Топологический порядок вершин DAG — это такое линейное упорядочивание, что для каждого ребра u→v вершина u стоит раньше v. Он существует тогда и только тогда, когда граф ациклический (в графе с циклом такое упорядочивание в принципе невозможно — если A→B→A, то и A должна быть раньше B, и B раньше A одновременно).A DAG's topological order is a linear ordering of vertices such that for every edge u→v, u comes before v. It exists if and only if the graph is acyclic (in a graph with a cycle, such an ordering is fundamentally impossible — if A→B→A, then A must come before B and B before A at the same time).
Алгоритм Кана строит порядок итеративно, без рекурсии: посчитать in-degree каждой вершины, положить в очередь все вершины с in-degree = 0 (у них нет невыполненных зависимостей), затем повторно извлекать вершину из очереди, добавлять в результат и уменьшать in-degree всех её соседей — если чей-то in-degree стал 0, он тоже добавляется в очередь. Если в конце обработаны не все вершины — в графе есть цикл (побочный эффект: Кан заодно детектирует циклы).Kahn's algorithm builds the order iteratively, without recursion: count each vertex's in-degree, enqueue every vertex with in-degree 0 (no unmet dependencies), then repeatedly dequeue a vertex, append it to the result, and decrement the in-degree of all its neighbors — if any neighbor's in-degree hits 0, enqueue it too. If not all vertices are processed by the end, the graph has a cycle (a side effect: Kahn's algorithm detects cycles for free).
build и lint оба зависят только от deps — оба валидны для второго места, порядок между ними не единствененboth build and lint depend only on deps — both are valid for second place, the order between them isn't unique
std::vector<int> topoSort(int n, const std::vector<std::vector<int>>& adj) {
std::vector<int> indeg(n, 0), order;
for (int u = 0; u < n; ++u)
for (int v : adj[u]) indeg[v]++;
std::queue<int> q;
for (int u = 0; u < n; ++u) if (indeg[u] == 0) q.push(u);
while (!q.empty()) {
int u = q.front(); q.pop();
order.push_back(u);
for (int v : adj[u]) if (--indeg[v] == 0) q.push(v);
}
if (order.size() != (size_t)n) throw std::runtime_error("cycle detected");
return order;
}
Компонента сильной связности (SCC) — максимальное подмножество вершин ориентированного графа, где из любой вершины достижима любая другая внутри этого подмножества. Свёртка каждой SCC в одну "суперверщину" всегда превращает исходный ориентированный граф в DAG — это ключевой факт, который делает SCC полезными: сложная структура с циклами разбивается на простую ациклическую структуру связей между блоками.A strongly connected component (SCC) is a maximal subset of a directed graph's vertices where every vertex can reach every other vertex within that subset. Collapsing each SCC into a single "super-vertex" always turns the original directed graph into a DAG — this is the key fact that makes SCCs useful: a complex structure full of cycles decomposes into a simple acyclic structure of connections between blocks.
Алгоритм Косарайю — концептуально простой: DFS на исходном графе, запоминая порядок выхода из вершин (tout); затем DFS на транспонированном графе (все рёбра развёрнуты), запускаемый от вершин в порядке убывания tout — каждое дерево этого второго обхода и есть одна SCC. Алгоритм Тарьяна находит SCC за один проход DFS (без построения транспонированного графа), используя low-link значения (то же понятие, что и для мостов в 3.1.2) и явный стек — вершина является "корнем" своей SCC, если её `low` равен её собственному `tin`.Kosaraju's algorithm is conceptually simple: DFS on the original graph, recording vertex exit order (tout); then DFS on the transposed graph (every edge reversed), launched from vertices in decreasing tout order — each tree of this second traversal is one SCC. Tarjan's algorithm finds SCCs in a single DFS pass (no transposed graph needed), using low-link values (the same concept as for bridges in 3.1.2) and an explicit stack — a vertex is the "root" of its SCC if its `low` equals its own `tin`.
A,B,C взаимно достижимы — одна SCC; связь SCC1→SCC2 однонаправленная, как ребро в DAGA,B,C are mutually reachable — one SCC; the SCC1→SCC2 link is one-directional, like a DAG edge
Цель зависит от других целей — build system строит DAG зависимостей и выполняет их в топологическом порядке, попутно выявляя параллельно исполнимые независимые ветки (build и lint из диаграммы выше можно запускать одновременно).A target depends on other targets — a build system constructs the dependency DAG and runs them in topological order, incidentally revealing independent branches that can run in parallel (build and lint in the diagram above can run concurrently).
Классическая формулировка задачи в учебниках (LeetCode "Course Schedule") не абстрактна — университетские деканаты буквально решают эту задачу при построении расписания, чтобы гарантировать, что пререквизит стоит раньше зависимого курса.The textbook framing of this problem (LeetCode's "Course Schedule") isn't abstract — university registrars literally solve this problem when building a schedule, to guarantee a prerequisite is placed before its dependent course.
Из примера в 3.1.1: цикл в графе зависимостей — ошибка конфигурации. Кан не только строит порядок установки, но и естественно обнаруживает цикл (не все вершины обработаны) — именно так пакетные менеджеры выдают ошибку "circular dependency detected" вместо зависания.From the 3.1.1 example: a cycle in the dependency graph is a configuration error. Kahn's algorithm doesn't just build an install order — it naturally detects a cycle (not all vertices got processed) — this is exactly how package managers report "circular dependency detected" instead of hanging.
Модуль A импортирует B, B импортирует C, C импортирует A — SCC {A,B,C} сигнализирует архитектурную проблему (циклическая зависимость модулей), которую линтеры архитектуры (ArchUnit, deptrac) находят именно через поиск SCC размера > 1 в графе импортов.Module A imports B, B imports C, C imports A — the SCC {A,B,C} signals an architectural problem (a circular module dependency), which architecture linters (ArchUnit, deptrac) find precisely by searching for SCCs of size > 1 in the import graph.
Исследование структуры интернета (Broder et al., 2000) показало, что веб-граф раскладывается на гигантскую SCC ("ядро", куда можно попасть отовсюду и откуда можно попасть везде внутри неё) плюс "входящие" и "исходящие" компоненты — модель, до сих пор используемая при анализе связности крупных ориентированных графов.A study of the internet's structure (Broder et al., 2000) showed the web graph decomposes into one giant SCC (the "core," reachable from everywhere and able to reach everywhere within it) plus "IN" and "OUT" components — a model still used when analyzing large directed graphs' connectivity.
Взаимодействие микросервисов — тоже ориентированный граф (сервис A вызывает сервис B по сети — ребро A→B), и на нём применимы обе идеи этой страницы сразу, но для разных задач. Топологическая сортировка нужна при развёртывании: если сервис B при старте обращается к сервису A (например, для миграции схемы или прогрева кэша), поднимать сервисы нужно строго в порядке "от зависимости к зависимому" — то же самое рассуждение, что и для сборки проекта в 3.1.1, только вершины — не задачи Make, а поды в Kubernetes. Компоненты сильной связности здесь ищут не ради оптимизации, а как сигнал тревоги: цикл вызовов A→B→A в рантайме (в отличие от статического импорта в примере 04) означает, что при пиковой нагрузке или частичном отказе один из сервисов может дождаться ответа от самого себя через цепочку — классический сценарий каскадного таймаута и распределённого дедлока, который системы трейсинга (Jaeger, Zipkin) обнаруживают, строя граф вызовов из спанов и явно проверяя его на циклы.Microservice interaction is also a directed graph (service A calls service B over the network — an edge A→B), and both of this page's ideas apply to it at once, for different purposes. Topological sort is needed at deploy time: if service B, on startup, reaches out to service A (say, for a schema migration or cache warmup), services must come up strictly in "dependency before dependent" order — the same reasoning as building a project in 3.1.1, except the vertices are Kubernetes pods rather than Make targets. Strongly connected components here aren't sought for optimization but as a warning sign: a runtime call cycle A→B→A (unlike the static import in example 04) means that under peak load or a partial outage, one service can end up waiting on itself through the chain — a classic cascading-timeout and distributed-deadlock scenario, which tracing systems (Jaeger, Zipkin) detect by building a call graph from spans and explicitly checking it for cycles.
Если несколько вершин одновременно имеют in-degree 0 (как build и lint выше), их относительный порядок в результате произволен и зависит от реализации очереди — код, полагающийся на конкретный порядок среди таких вершин (а не только на "все зависимости идут раньше зависимых"), содержит скрытое предположение, которое сломается при смене реализации.If several vertices simultaneously have in-degree 0 (like build and lint above), their relative order in the result is arbitrary and depends on the queue implementation — code relying on one specific order among such vertices (rather than just "dependencies come before dependents") carries a hidden assumption that breaks if the implementation changes.
В неориентированном графе "связность" достаточно проверить одним BFS/DFS на компоненту. В ориентированном графе достижимость несимметрична (A может достигать B, но не наоборот) — применение простого BFS/DFS вместо Тарьяна/Косарайю даст неверный ответ, объединяя вершины, которые на самом деле не образуют SCC.In an undirected graph, checking "connectivity" is just one BFS/DFS per component. In a directed graph, reachability is asymmetric (A can reach B but not vice versa) — using plain BFS/DFS instead of Tarjan's/Kosaraju's algorithm gives a wrong answer, merging vertices that don't actually form an SCC.
Второй проход Косарайю обязан идти по транспонированному графу (рёбра развёрнуты) — запуск второго DFS на исходном графе в порядке убывания tout не даёт корректных SCC, это одна из самых частых ошибок при реализации алгоритма по памяти.Kosaraju's second pass must run on the transposed graph (edges reversed) — running the second DFS on the original graph in decreasing tout order does not produce correct SCCs; this is one of the most common mistakes when implementing the algorithm from memory.