6. Graph Valid Tree
«Всякое дерево держится на строгом балансе: убери одно ребро — распадется связь, добавь одно — родится цикл».
— Леонард Эйлер
Фундаментальные законы сетей: Что делает граф деревом?
В задаче 4. Redundant Connection нам гарантировали, что исходный граф был связным деревом до добавления кабеля.
Что делать, если на вход поступает произвольный массив из $N$ вершин и случайный набор ребер? Как математически строго и алгоритмически быстро определить: является ли данный граф Валидным деревом (Valid Tree)?
Задача Graph Valid Tree (LeetCode №261) проверяет знание фундаментальных дискретных свойств структур данных, используемых в архитектуре систем (топологии серверов, деревья разбора AST в компиляторах, DOM-деревья).
С точки зрения теории графов, Дерево — это связный неориентированный ациклический граф:
- Ацикличность: в графе нет ни одного цикла.
- Связность: граф состоит ровно из одной компоненты связности.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
subgraph Valid["Валидное дерево: связно и ациклично"]
A1["0"]:::success --- B1["1"]:::success
A1 --- C1["2"]:::success
B1 --- D1["3"]:::success
B1 --- E1["4"]:::success
end
subgraph CycleError["Ошибка: обнаружен цикл"]
F["0"]:::error --- G["1"]:::error
G --- H["2"]:::error
H --- F
end
subgraph PartitionError["Ошибка: разрыв связности"]
I["0"]:::error --- J["1"]:::error
K["2"]:::error --- L["3"]:::error
end
Математический Fast Fail: Закон N - 1 ребер
Типичная ошибка новичка — сразу бросаться писать DFS/BFS или строить DSU без предварительного анализа размерностей.
Фундаментальная теорема теории графов гласит: любое дерево из $N$ вершин содержит ровно $N - 1$ ребер.
- Если число ребер $> N - 1$, по принципу Дирихле в графе гарантированно присутствует хотя бы один цикл.
- Если число ребер $< N - 1$, граф гарантированно распадается на несвязные компоненты.
Однако одного правила $E = N - 1$ недостаточно: граф может содержать цикл в одной части и быть изолированным в другой. Нам требуется детектор циклов.
Архитектура решения через DSU
Для проверки ацикличности DSU превосходит DFS по показателям Mechanical Sympathy:
- Не требует построения списков смежности
[][]int. - Память выделяется в два непрерывных среза
parentиsize. - Процессор читает ребра последовательно, обеспечивая максимальную утилизацию кэша L1.
package main
type DSU struct {
parent []int
size []int
}
func NewDSU(n int) *DSU {
dsu := &DSU{
parent: make([]int, n),
size: make([]int, n),
}
for i := 0; i < n; i++ {
dsu.parent[i] = i
dsu.size[i] = 1
}
return dsu
}
func (d *DSU) Find(x int) int {
if d.parent[x] != x {
d.parent[x] = d.Find(d.parent[x])
}
return d.parent[x]
}
func (d *DSU) Union(x, y int) bool {
rootX := d.Find(x)
rootY := d.Find(y)
if rootX == rootY {
return false // Цикл найден
}
if d.size[rootX] < d.size[rootY] {
d.parent[rootX] = rootY
d.size[rootY] += d.size[rootX]
} else {
d.parent[rootY] = rootX
d.size[rootX] += d.size[rootY]
}
return true
}
// validTree определяет, является ли граф корректным связным деревом.
// Временная сложность: O(N * α(N)) ≈ O(N).
// Пространственная сложность: O(N) для DSU.
func validTree(n int, edges [][]int) bool {
// 1. Быстрая математическая проверка инварианта дерева
if len(edges) != n-1 {
return false
}
// 2. Инициализация DSU
dsu := NewDSU(n)
// 3. Проверка на отсутствие циклов
for _, edge := range edges {
u, v := edge[0], edge[1]
if !dsu.Union(u, v) {
return false // Обнаружен цикл
}
}
// Теорема: если в графе из N вершин ровно N - 1 ребер и нет циклов,
// он ГАРАНТИРОВАННО является одним связным деревом.
return true
}
Резюме
- Инвариант дерева: Ровно $N - 1$ ребер для $N$ вершин — обязательное условие для быстрого отсечения некорректных графов за $O(1)$.
- Ацикличность и связность: При $E = N - 1$ отсутствие циклов автоматически гарантирует связность всей системы.
- Эффективность DSU: Проверка занимает линейное время $O(N)$ и исключает накладные расходы на рекурсивный стек вызовов.
Завершим блок систем непересекающихся множеств разбором задачи с матричным представлением графа: 7. Number of Provinces.