3.2.3 — самый дешёвый способ связать всё со всем3.2.3 — the cheapest way to connect everything to everything

Минимальное остовное дерево (MST)Minimum spanning tree (MST)

Если 3.2.1 отвечает на вопрос "как дешевле добраться из A в B", то MST отвечает на другой вопрос: "как связать все вершины графа рёбрами минимальной суммарной стоимости, без единого лишнего ребра". Три алгоритма здесь — три разных жадных стратегии, доказуемо дающие один и тот же оптимальный результат.If 3.2.1 answers "what's the cheapest way from A to B," MST answers a different question: "how do you connect every vertex in the graph with edges of minimum total cost, with no edge left over." The three algorithms here are three different greedy strategies, provably yielding the same optimal result.

частый вопрос на собеседованииcommon interview question

Что такое остовное деревоWhat a spanning tree is

Остовное дерево (spanning tree) связного неориентированного графа — это подмножество его рёбер, которое связывает все V вершин, не образуя ни одного цикла. У любого связного графа с V вершинами остовное дерево всегда содержит ровно V-1 ребро (не больше — иначе появится цикл; не меньше — иначе граф распадётся на несколько частей). Минимальное остовное дерево — то из всех возможных остовных деревьев, у которого сумма весов рёбер наименьшая.A connected undirected graph's spanning tree is a subset of its edges connecting all V vertices with no cycle formed. Any connected graph with V vertices has a spanning tree with exactly V-1 edges (no more — that would create a cycle; no fewer — the graph would split into several pieces). The minimum spanning tree is, among all possible spanning trees, the one with the smallest total edge weight.

A B C D 1 2 9 7 3 сплошные — MST (вес 1+2+3=6)solid — MST (weight 1+2+3=6) пунктир — отброшены (образовали бы цикл)dashed — dropped (would form a cycle)

5 рёбер в исходном графе, но MST использует только 3 (V-1 для V=4) с минимальной суммой5 edges in the source graph, but the MST uses only 3 (V-1 for V=4) with minimum total weight

Алгоритм Крускала и DSUKruskal's algorithm and DSU

Стратегия: отсортировать все рёбра по весу по возрастанию, затем идти по ним и добавлять в MST каждое ребро, которое не создаёт цикл с уже добавленными — то есть соединяет две ещё не связанные между собой части. "Не создаёт цикл" проверяется структурой данных Disjoint Set Union (DSU, он же Union-Find): каждая компонента — это множество с одним представителем; `find(v)` находит представителя компоненты v, `union(u, v)` сливает две компоненты. Ребро добавляется в MST, только если `find(u) != find(v)` — иначе обе вершины уже в одной компоненте, и ребро создало бы цикл.The strategy: sort all edges by weight ascending, then walk through them and add to the MST every edge that doesn't create a cycle with what's already added — meaning it connects two parts not yet linked to each other. "Doesn't create a cycle" is checked with the Disjoint Set Union (DSU, aka Union-Find) data structure: each component is a set with one representative; `find(v)` finds v's component representative; `union(u, v)` merges two components. An edge is added to the MST only if `find(u) != find(v)` — otherwise both vertices are already in the same component, and the edge would create a cycle.

struct DSU {
    std::vector<int> parent, rank_;
    DSU(int n) : parent(n), rank_(n, 0) { std::iota(parent.begin(), parent.end(), 0); }
    int find(int x) { return parent[x] == x ? x : parent[x] = find(parent[x]); } // сжатие пути// path compression
    bool unite(int a, int b) {
        a = find(a); b = find(b);
        if (a == b) return false;                 // уже связаны — было бы циклом// already connected — would cycle
        if (rank_[a] < rank_[b]) std::swap(a, b);   // union by rank// union by rank
        parent[b] = a;
        if (rank_[a] == rank_[b]) rank_[a]++;
        return true;
    }
};

long long kruskal(int n, std::vector<std::array<int,3>> edges) { // {weight, u, v}// {weight, u, v}
    std::sort(edges.begin(), edges.end());
    DSU dsu(n);
    long long total = 0;
    for (auto& [w, u, v] : edges)
        if (dsu.unite(u, v)) total += w;   // добавлено в MST// added to the MST
    return total;
}

Сжатие пути (path compression) в `find` и объединение по рангу (union by rank) в `unite` вместе дают почти константную амортизированную сложность операций — O(α(V)), где α — обратная функция Аккермана, растущая настолько медленно, что для любого практического V она не превышает 4-5. Сложность Крускала в целом определяется сортировкой рёбер — O(E log E).Path compression in `find` combined with union by rank in `unite` together give near-constant amortized complexity per operation — O(α(V)), where α is the inverse Ackermann function, growing so slowly that for any practical V it never exceeds 4-5. Kruskal's overall complexity is dominated by sorting the edges — O(E log E).

Алгоритм Прима и алгоритм БорувкиPrim's algorithm and Borůvka's algorithm

Прим растит MST из одной стартовой вершины, как Дейкстра растит дерево кратчайших путей: поддерживает множество уже включённых в MST вершин и на каждом шаге добавляет минимальное по весу ребро, ведущее из этого множества наружу (используя приоритетную очередь — почти буквально код Дейкстры с заменой "расстояние от старта" на "вес ребра до множества"). Оправдан на плотных графах, где Крускалу пришлось бы сортировать огромное количество рёбер.Prim's algorithm grows the MST from one starting vertex, the way Dijkstra grows a shortest-path tree: it maintains the set of vertices already in the MST, and at each step adds the minimum-weight edge leading from that set outward (using a priority queue — almost literally Dijkstra's code with "distance from source" replaced by "edge weight into the set"). It's justified on dense graphs, where Kruskal would have to sort an enormous number of edges.

Борувка — исторически первый алгоритм MST (1926 год) и единственный из трёх, естественно параллелизуемый: на каждом раунде КАЖДАЯ компонента одновременно находит своё минимальное исходящее ребро и добавляет его — число компонент минимум вдвое уменьшается за раунд, значит всего нужно O(log V) раундов. Именно эта параллелизуемость делает Борувку основой современных распределённых реализаций MST на графах, не помещающихся в память одной машины.Borůvka's algorithm is historically the first MST algorithm (1926) and the only one of the three that's naturally parallelizable: in each round, EVERY component simultaneously finds its minimum outgoing edge and adds it — the number of components at least halves per round, so only O(log V) rounds are needed total. This parallelizability is exactly why Borůvka's algorithm underlies modern distributed MST implementations on graphs too large to fit in one machine's memory.

5 примеров из реальной жизни5 real-world examples

01

Крускал — проектирование кабельной/оптоволоконной сети между зданиями кампусаKruskal — designing a cable/fiber network between campus buildings

Классическая мотивирующая задача из учебников — не выдумка: сеть, соединяющая N зданий минимальной суммарной длиной кабеля (стоимость прокладки), — это буквально MST, а разреженность графа (не каждое здание физически можно напрямую соединить с каждым) делает Крускала естественным выбором.The classic textbook motivating example isn't made up: a network connecting N buildings with minimum total cable length (laying cost) is literally an MST, and the graph's sparsity (not every building can physically connect directly to every other) makes Kruskal's algorithm the natural choice.

02

Прим — проектирование электросетей и трубопроводовPrim's algorithm — designing power grids and pipelines

Энергетические компании при планировании распределительной сети от одной подстанции ко всем потребителям решают ровно задачу MST от фиксированного корня — естественная постановка для Прима, растущего дерево именно из одной точки.Power companies planning a distribution network from one substation to all consumers solve exactly the MST problem from a fixed root — a natural fit for Prim's algorithm, which grows a tree from exactly one point.

03

MST — приближённый алгоритм для задачи коммивояжёра (TSP)MST — an approximation algorithm for the Traveling Salesman Problem (TSP)

TSP NP-труден, но для метрических графов (выполняется неравенство треугольника) существует 2-приближённый алгоритм: построить MST, обойти его в порядке DFS (удваивая каждое ребро), затем "срезать углы" — результат гарантированно не более чем вдвое хуже оптимального маршрута, и вся конструкция начинается именно с MST.TSP is NP-hard, but for metric graphs (the triangle inequality holds), a 2-approximation algorithm exists: build the MST, walk it in DFS order (doubling every edge), then "shortcut" repeated vertices — the result is guaranteed to be at most twice the optimal tour, and the whole construction starts with an MST.

04

MST — кластеризация в машинном обучении (single-linkage clustering)MST — clustering in machine learning (single-linkage clustering)

Построить MST на графе "расстояние между точками данных", затем удалить K-1 самых длинных рёбер MST — оставшиеся K компонент связности и есть K кластеров. Это точный алгоритм иерархической кластеризации по методу одиночной связи, а не эвристика.Build an MST on a graph of "distance between data points," then remove the K-1 longest MST edges — the remaining K connected components are exactly K clusters. This is the exact algorithm for single-linkage hierarchical clustering, not a heuristic.

05

DSU (без остального Крускала) — обнаружение циклов при онлайн-добавлении дружеских связейDSU (Kruskal's core structure alone) — cycle detection when friendships are added online

Задача "определить, находятся ли два пользователя уже в одной компоненте связности" при потоке новых связей (без пересчёта всего графа с нуля) — типичное отдельное применение DSU, вне контекста MST: LeetCode "Number of Provinces", "Redundant Connection".The task "determine whether two users are already in the same connected component" under a stream of new connections (without recomputing the whole graph from scratch) — a typical standalone use of DSU, outside the MST context: LeetCode's "Number of Provinces," "Redundant Connection."

ГраблиGotchas

DSU без сжатия пути и объединения по рангу деградирует до O(V) на операциюDSU with no path compression and no union by rank degrades to O(V) per operation

Наивная реализация `find` без сжатия пути на неудачно построенной цепочке union'ов вырождается в связный список — каждый `find` становится O(V) вместо почти-константного O(α(V)). Обе оптимизации обязательны для заявленной сложности Крускала, а не факультативны.A naive `find` implementation with no path compression, under an unluckily built chain of unions, degenerates into a linked list — each `find` becomes O(V) instead of near-constant O(α(V)). Both optimizations are mandatory for Kruskal's claimed complexity, not optional.

MST определён только для связного графаAn MST is only defined for a connected graph

Если граф несвязен, единого остовного дерева не существует в принципе — алгоритмы без явной проверки молча вернут "минимальный остовной лес" (по дереву на каждую компоненту), что может быть корректным намерением, а может маскировать баг во входных данных, где граф должен был быть связным, но не является.If the graph is disconnected, a single spanning tree fundamentally doesn't exist — algorithms with no explicit check silently return a "minimum spanning forest" (one tree per component), which may be the intended behavior or may mask a bug in input data that was supposed to be connected but isn't.

MST не единственно при равных весах рёберThe MST isn't unique when edge weights tie

Если два ребра имеют одинаковый вес и оба являются кандидатами на включение, разные корректные реализации Крускала/Прима (отличающиеся порядком перебора рёбер при равенстве) могут дать разные по составу, но одинаковые по суммарному весу деревья — тесты, сравнивающие набор рёбер вместо суммарного веса, будут ложно падать на корректной реализации.If two edges tie in weight and both are candidates for inclusion, different correct implementations of Kruskal's/Prim's algorithm (differing in tie-breaking order) can produce trees with different edge sets but identical total weight — tests comparing the edge set instead of the total weight will falsely fail on a correct implementation.