3.2.3 — самый дешёвый способ связать всё со всем3.2.3 — the cheapest way to connect everything to everything
Если 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.
Остовное дерево (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.
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
Стратегия: отсортировать все рёбра по весу по возрастанию, затем идти по ним и добавлять в 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).
Прим растит 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.
Классическая мотивирующая задача из учебников — не выдумка: сеть, соединяющая 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.
Энергетические компании при планировании распределительной сети от одной подстанции ко всем потребителям решают ровно задачу 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.
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.
Построить 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.
Задача "определить, находятся ли два пользователя уже в одной компоненте связности" при потоке новых связей (без пересчёта всего графа с нуля) — типичное отдельное применение 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."
Наивная реализация `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.
Если граф несвязен, единого остовного дерева не существует в принципе — алгоритмы без явной проверки молча вернут "минимальный остовной лес" (по дереву на каждую компоненту), что может быть корректным намерением, а может маскировать баг во входных данных, где граф должен был быть связным, но не является.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.
Если два ребра имеют одинаковый вес и оба являются кандидатами на включение, разные корректные реализации Крускала/Прима (отличающиеся порядком перебора рёбер при равенстве) могут дать разные по составу, но одинаковые по суммарному весу деревья — тесты, сравнивающие набор рёбер вместо суммарного веса, будут ложно падать на корректной реализации.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.