Множества и две операции
Представьте шесть городов 0…5. Город — вершина графа, дорога между двумя городами — ребро. Из 0 в 2 можно попасть по пути 0→1→2, даже без прямой дороги 0—2. Компонента связности — все города, между которыми можно проехать; изолированный город тоже образует компоненту. После появления каждой новой дороги нужно быстро проверять, какие города связаны.
DSU / СНМ (Disjoint-set union) — Структура, поддерживающая разбиение элементов на попарно непересекающиеся множества и операции find и union.
В прошлой лекции мы соединяли списки; теперь нужна другая информация: принадлежат ли элементы одной группе. После дорог 0—1 и 1—2 города 0, 1, 2 образуют одну компоненту. Добавленная 0—2 уже не уменьшит число групп. Проследите эту последовательность в таблице, затем создайте дороги самостоятельно.
| Добавленная дорога | Компоненты на карте | Число компонент |
|---|---|---|
| Пока дорог нет | {0}, {1}, {2}, {3}, {4}, {5} | 6 |
| 0—1, затем 1—2 | {0,1,2}, {3}, {4}, {5} | 4 |
| 3—4 | {0,1,2}, {3,4}, {5} | 3 |
| 0—2 | {0,1,2}, {3,4}, {5}: связь уже была | 3 |
Вначале каждый элемент — отдельное множество. find(x) возвращает его представителя. union(x,y) объединяет классы элементов x и y, не меняя остальные.
Можно использовать DSU для добавляемых рёбер графа: чтобы проверить, соединены ли u и v, достаточно сравнить их представителей.
Кружки — города (вершины), линии — дороги (рёбра). Путь может проходить через несколько дорог. Ссылки parent ниже — служебный лес DSU: они не обязаны быть дорогами.
Представители: 0, 1, 2, 3, 4, 5. Компонент: 6.
Каждая вершина пока является своей компонентой.
| Операция | Смысл |
|---|---|
| makeSet(x) | Создать множество {x} |
| find(x) | Получить представитель множества x |
| union(x,y) | Объединить классы x и y |
Если union(0,1) и union(1,2) уже выполнены, то 0, 1 и 2 должны иметь одного представителя, даже если ребра (0,2) нет. Это транзитивная связь, а не проверка непосредственного соседства. Повторный union внутри одной компоненты ничего не меняет и не должен уменьшать счётчик компонент.
Размер компоненты для каждой вершины
Сначала объединяем концы всех рёбер, затем читаем size только у представителя. Изолированная вершина имеет размер 1. Общая цена O(n + (n+m)α(n)), память O(n).
class DSU:
def __init__(self, n):
self.parent = list(range(n))
self.size = [1] * n
self.components = n
def find(self, x):
root = x
while self.parent[root] != root:
root = self.parent[root]
while self.parent[x] != x:
following = self.parent[x]
self.parent[x] = root
x = following
return root
def union(self, a, b):
a, b = self.find(a), self.find(b)
if a == b:
return False
if self.size[a] < self.size[b]:
a, b = b, a
self.parent[b] = a
self.size[a] += self.size[b]
self.components -= 1
return True
def component_sizes(n, edges):
d = DSU(n)
for a, b in edges:
d.union(a, b)
return [d.size[d.find(v)] for v in range(n)]
print(*component_sizes(6, [(0, 1), (1, 2), (3, 4)]))struct Dsu {parent:Vec<usize>,size:Vec<usize>,components:usize}
impl Dsu {
fn new(n:usize)->Self {Self{parent:(0..n).collect(),size:vec![1;n],components:n}}
fn find(&mut self,mut x:usize)->usize {
let mut root=x;while self.parent[root]!=root {root=self.parent[root];}
while self.parent[x]!=x {let next=self.parent[x];self.parent[x]=root;x=next;}
root
}
fn union(&mut self,a:usize,b:usize)->bool {
let(mut a,mut b)=(self.find(a),self.find(b));if a==b{return false;}
if self.size[a]<self.size[b]{std::mem::swap(&mut a,&mut b);}
self.parent[b]=a;self.size[a]+=self.size[b];self.components-=1;true
}
}
fn component_sizes(n:usize,edges:&[(usize,usize)])->Vec<usize>{let mut d=Dsu::new(n);for &(a,b)in edges{d.union(a,b);} (0..n).map(|v|{let root=d.find(v);d.size[root]}).collect()}
fn main(){let result=component_sizes(6,&[(0,1),(1,2),(3,4)]);println!("{}",result.iter().map(|x|x.to_string()).collect::<Vec<_>>().join(" "));}3 3 3 2 2 1
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Группы распространения новости
В Codeforces 1167C все участники одной группы соединяются с её первым участником. Для группы из k человек достаточно k−1 объединений, а не всех k² пар.
Контракт определён. Теперь выберем представление, которое позволит объединять компоненты без переписывания всех их элементов.
Лес деревьев представителей
Этот раздел откроется после предыдущих проверок.
Продолжить: Множества и две операции →Разбиение на непересекающиеся множества удобно хранить лесом. У каждой вершины ровно один parent, а корень ссылается сам на себя. Сам номер представителя не важен — важно равенство представителей.
Каждый набор элементов хранится деревом указателей parent. В корне parent[root] = root; он и является представителем. Запрос find поднимается по ссылкам до корня.
makeSet(x):
parent[x] = x
rank[x] = 0
find(x):
if parent[x] == x: return x
return find(parent[x])Без эвристик деревья могут стать высокими, и find может стоить Θ(n).
Представители: 0, 1, 2, 3, 4, 5. Компонент: 6.
Каждая вершина пока является своей компонентой.
Вызов union сначала вычисляет find для обоих аргументов, затем меняет parent одного корня. Если переподвесить произвольную внутреннюю вершину, можно оторвать часть её старой компоненты или создать цикл. Массив size относится к корням; размер компоненты x читают как size[find(x)].
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Представитель не обязан быть минимумом
Если ответ должен содержать минимальный id компоненты, храните его отдельным полем у корня и обновляйте при union. Правило выбора корня по размеру само по себе не сохраняет минимальный номер.
Лес хранит ответ корректно, но его форма пока может быть плохой. Ограничим высоту правилом объединения.
Эвристика рангов
Этот раздел откроется после предыдущих проверок.
Продолжить: Множества и две операции →Наивное объединение позволяет каждый раз подвешивать длинное дерево под новый корень. После n−1 таких операций получится цепочка. Сначала освоим объединение по размеру: корень компоненты из 3 вершин подвесим под корень компоненты из 7, а в новом корне запишем размер 10. Номер корня при этом не важен.
Добавляем новые вершины 1…7 к компоненте 0. Наивный вариант подвешивает старый корень под новую вершину; вариант по размеру сохраняет больший корень.
Выполнено объединений: 0. Глубина 0: наивно 0, по размеру 0.
union(a,b):
a, b = find(a), find(b)
if a == b: return false
if size[a] < size[b]: swap(a,b)
parent[b] = a
size[a] += size[b]
return trueРазмер читается только у корня; повторный union ничего не меняет.
Путь вершины становится длиннее только при присоединении её меньшей компоненты к большей. Тогда размер как минимум удваивается: 1, 2, 4, 8… Поэтому без сжатия высота не превосходит O(log n). Ранг — другой способ выбирать направление; его анализ можно прочитать после практики с размером.
Ранг (Rank) — Вспомогательное значение корня, ограничивающее высоту дерева при union по рангу.
При объединении корень меньшего ранга подвешивается под корень большего. Если ранги равны, один корень подвешивается к другому и ранг нового корня увеличивается на единицу.
Углубление: логарифмическая граница ранга
Для ранга 3 нужно не меньше 8 вершин. Два корня ранга 2 дают следующий ранг только при равенстве рангов.
После сжатия ранг остаётся исторической верхней оценкой, а не измеренной текущей высотой.
Углубление: rank(root) ≤ ⌊log₂ |set|⌋
Следовательно, без сжатия путей один find занимает O(log n). Равноправный вариант — union по размеру: меньший набор под больший.
Ранг и высота после сжатия
Для обязательной реализации используйте уже разобранный union по размеру. Ранг — равноправная альтернатива, а строгое исследование оценок одной и двух эвристик вынесено в дополнительные главы. Совместное применение размера и сжатия даёт очень малую амортизированную цену длинной последовательности операций.
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Плохой порядок объединений
Вход может сначала объединить 0 с 1, затем всю компоненту с 2 и так далее. Произвольное направление образует цепочку; эвристика размера не зависит от удачного порядка запросов.
Ранги дали логарифмический путь. Используем сам факт прохождения пути, чтобы ускорить следующие запросы.
Эвристика сжатия путей
Этот раздел откроется после предыдущих проверок.
Продолжить: Множества и две операции →Ранг ограничивает исходную высоту, но повторный find всё ещё может проходить те же рёбра. Запомним найденный корень прямо в посещённых вершинах: это и есть сжатие путей.
Сжатие путей (Path compression) — После find(x) все вершины пройденного пути начинают непосредственно указывать на найденный корень.
Первый запрос может пройти несколько ссылок, но он ускоряет будущие запросы. Сжатие путей не меняет разбиение на множества: меняется только форма дерева.
Представители: 0, 0, 0, 0, 0, 0. Компонент: 1.
Начальное дерево специально вытянуто: сравните ссылки до и после find(5).
find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]Важно изменить parent[x], а не просто вычислить корень.
Сжатие меняет только ссылки внутри прежней компоненты. После него ранг уже не обязан совпадать с текущей высотой: уменьшать ранги после find не нужно. Итеративный вариант сначала находит корень, затем проходит путь второй раз; он не расходует стек вызовов на длинной наивной цепочке.
Объединение по размеру и сжатие путей
Find выполнен двумя проходами без рекурсии. Union возвращает false для уже связанных вершин, поэтому счётчик компонент не уменьшается повторно. Инициализация O(n), m операций — O(m α(n)) амортизированно, память O(n). Индексы должны лежать в [0,n).
class DSU:
def __init__(self, n):
self.parent = list(range(n))
self.size = [1] * n
self.components = n
def find(self, x):
root = x
while self.parent[root] != root:
root = self.parent[root]
while self.parent[x] != x:
following = self.parent[x]
self.parent[x] = root
x = following
return root
def union(self, a, b):
a, b = self.find(a), self.find(b)
if a == b:
return False
if self.size[a] < self.size[b]:
a, b = b, a
self.parent[b] = a
self.size[a] += self.size[b]
self.components -= 1
return True
d = DSU(6)
for a, b in [(0, 1), (1, 2), (3, 4), (0, 2)]:
d.union(a, b)
print(d.components, d.size[d.find(2)])struct Dsu {parent:Vec<usize>,size:Vec<usize>,components:usize}
impl Dsu {
fn new(n:usize)->Self {Self{parent:(0..n).collect(),size:vec![1;n],components:n}}
fn find(&mut self,mut x:usize)->usize {
let mut root=x;while self.parent[root]!=root {root=self.parent[root];}
while self.parent[x]!=x {let next=self.parent[x];self.parent[x]=root;x=next;}
root
}
fn union(&mut self,a:usize,b:usize)->bool {
let(mut a,mut b)=(self.find(a),self.find(b));if a==b{return false;}
if self.size[a]<self.size[b]{std::mem::swap(&mut a,&mut b);}
self.parent[b]=a;self.size[a]+=self.size[b];self.components-=1;true
}
}
fn main(){let mut d=Dsu::new(6);for(a,b)in[(0,1),(1,2),(3,4),(0,2)]{d.union(a,b);}let root=d.find(2);println!("{} {}",d.components,d.size[root]);}3 3
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Много запросов связности
Сначала добавлены дороги, затем часто спрашивается связь тех же городов. Сжатие запоминает представителей для посещённых вершин, поэтому повторные find обходят меньше ссылок.
Реализация с двумя эвристиками готова: перейдём к запросам связности и Краскалу. Строгое исследование стоимости последовательности и обратной функции Аккермана доступно в дополнительных главах.
Практическая работа DSU
Этот раздел откроется после предыдущих проверок.
Продолжить: Множества и две операции →Первый этап — запросы связности и размера: добавьте дороги 0—1, 1—2 и 3—4. Теперь find(0)=find(2), но find(0)≠find(3); size[find(2)]=3. Проверьте эти ответы в модели первой главы. Реализуйте component_sizes в разминке практики, а затем переходите к взвешенным дорогам.
Обычные примеры: отслеживание компонент связности при добавлении рёбер, построение минимального остовного дерева алгоритмом Краскала, объединение эквивалентных сущностей.
Для запросов достаточно массивов parent и size со сжатием путей. Теперь второй этап: у дороги есть вес — цена строительства. Цикл — замкнутый путь, который вернулся в начало. Остов — набор дорог, соединяющий все города без циклов; в связном графе из n вершин у него n−1 ребро. Минимальный остов имеет наименьшую сумму весов.
| Вопрос | Ответ |
|---|---|
| Какая цена одиночного find? | Не обязана быть O(1) в худшем случае |
| Можно ли быстро определить, что u и v связны? | Да, find(u) = find(v) |
| Можно ли делать split множества? | Нет, стандартный DSU не поддерживает |
| Можно ли склеить два уже объединённых класса? | union не меняет состояние |
Пусть дороги 0—1, 1—2, 0—2, 2—3 имеют веса 1, 2, 3, 4. Берём первые две: они соединяют разные компоненты. Дорогу 0—2 веса 3 пропускаем: путь через 1 уже есть. Дорога 2—3 соединяет последний город. Получен остов веса 1+2+4=7. Это шаги Краскала: рассматривать рёбра от лёгких к тяжёлым и брать только соединяющие разные компоненты.
Вес — цена дороги. Остов соединяет все вершины без циклов; на четырёх вершинах ему нужны три ребра. Вершины 0 и 2 после двух первых дорог связаны через 1, поэтому третья дорога замкнёт цикл.
Компонент: 4. Вес выбранных дорог: 0.
В Краскале пропускаем ребро, если его концы уже имеют одного представителя: оно замкнуло бы цикл в выбранном лесу. Иначе добавляем вес и объединяем компоненты. Для несвязного графа получится минимальный остовный лес, а не одно дерево; проверяйте число компонент. Для удаления дороги, восстановления пути или поиска минимального ребра на пути одного DSU недостаточно.
Краскал: минимальный остовный лес
Сортируем рёбра по весу и принимаем только соединяющие разные компоненты. Результат — сумма весов и число компонент: если оно больше 1, одного остовного дерева нет. Сортировка O(m log(m+1)) доминирует над DSU; память O(n+m). В Rust сумма i64: при данных до 2·10⁵ рёбер и весах до 10⁹ она безопасна.
class DSU:
def __init__(self, n):
self.parent = list(range(n))
self.size = [1] * n
self.components = n
def find(self, x):
root = x
while self.parent[root] != root:
root = self.parent[root]
while self.parent[x] != x:
following = self.parent[x]
self.parent[x] = root
x = following
return root
def union(self, a, b):
a, b = self.find(a), self.find(b)
if a == b:
return False
if self.size[a] < self.size[b]:
a, b = b, a
self.parent[b] = a
self.size[a] += self.size[b]
self.components -= 1
return True
def kruskal(n, edges):
d, total = DSU(n), 0
for weight, a, b in sorted(edges):
if d.union(a, b):
total += weight
return total, d.components
print(*kruskal(4, [(4, 0, 1), (1, 1, 2), (2, 0, 2), (3, 2, 3)]))struct Dsu {parent:Vec<usize>,size:Vec<usize>,components:usize}
impl Dsu {
fn new(n:usize)->Self {Self{parent:(0..n).collect(),size:vec![1;n],components:n}}
fn find(&mut self,mut x:usize)->usize {
let mut root=x;while self.parent[root]!=root {root=self.parent[root];}
while self.parent[x]!=x {let next=self.parent[x];self.parent[x]=root;x=next;}
root
}
fn union(&mut self,a:usize,b:usize)->bool {
let(mut a,mut b)=(self.find(a),self.find(b));if a==b{return false;}
if self.size[a]<self.size[b]{std::mem::swap(&mut a,&mut b);}
self.parent[b]=a;self.size[a]+=self.size[b];self.components-=1;true
}
}
fn kruskal(n:usize,edges:&[(i64,usize,usize)])->(i64,usize){let mut edges=edges.to_vec();edges.sort_unstable();let mut d=Dsu::new(n);let mut total=0;for(w,a,b)in edges{if d.union(a,b){total+=w;}}(total,d.components)}
fn main(){let(total,components)=kruskal(4,&[(4,0,1),(1,1,2),(2,0,2),(3,2,3)]);println!("{} {}",total,components);}6 1
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Общие и личные рёбра
В LeetCode 1579 поддерживаются отдельные DSU для двух пользователей. Сначала обрабатываются общие рёбра: полезное общее соединение экономит отдельные. В конце проверяется связность обеих структур.
Теперь вы умеете предсказывать число компонент, обновлять размер у представителя и отклонять ребро, создающее цикл. Дальше в основном маршруте — динамическое программирование: граф переходов поможет упорядочить подзадачи. Сливаемые кучи доступны отдельным продвинутым модулем.
Справочник терминов
Справочник собирает определения из разобранных разделов. Используйте поиск, если нужно восстановить термин; затем переходите к оценке ограничений и задачам для самостоятельного решения.
Общий справочник всех лекций ↗
Найдено: 12 из 12
Ограничения и выбор алгоритма
Этот раздел откроется после предыдущих проверок.
Продолжить: Множества и две операции →Сначала подставьте максимальные размеры из условия в оценку времени и памяти. Оценка числа действий — ориентир: константы, язык и ввод проверяются отдельно.
| Вариант | Гарантия |
|---|---|
| Инициализация parent и size | O(n) времени и памяти |
| Без эвристик | Один find может стоить O(n) |
| Только размер или ранг | O(log n) для одного find |
| Размер / ранг вместе со сжатием | O(n+m·α(n)) на инициализацию и m запросов; очень малая амортизированная цена |
Учитываем сумму размеров групп
Пусть S — сумма численностей всех групп. Объединяйте каждого участника с первым: O(S) вызовов union. Попарные связи потребовали бы сумму kᵢ², которая может быть огромной даже при S ≤ 500 000. Инициализация массивов стоит O(n).
Оценка DSU не отменяет сортировку
Для m рёбер Краскал требует O(m log(m+1)) на сортировку и O(m α(n)) на DSU. Именно сортировка обычно доминирует. При m = 200 000 это несколько миллионов сравнений вместо многократного обхода графа.
Сумма весов может достигнуть (n−1)·maxWeight. При n = 200 000 и весах до 10⁹ нужен 64-битный тип. Отрицательные веса не мешают Краскалу, а несвязный граф требует явно договориться об ответе: лес или ошибка отсутствия остова.
Когда обычный DSU не подходит
Удаление произвольного ребра не выражается обратным union: внутри компоненты может быть много альтернативных путей. Офлайн-удаления иногда обрабатывают в обратном порядке; для откатов нужна другая реализация, обычно без сжатия путей.
В тестах нужны изолированные вершины, повторное ребро, union внутри одной компоненты, длинная последовательность объединений и повторный find. Проверяйте разбиение, а не конкретный номер корня: корректные реализации выбирают разные представители.
Практика: 3 уровня
Этот раздел откроется после предыдущих проверок.
Продолжить: Множества и две операции →Разминка: реализуйте операции, затем размеры
Сначала напишите find со сжатием и union по размеру. Затем используйте тот же инвариант для размеров компонент. Проверьте изолированную вершину, повторное ребро и цикл до перехода к внешним задачам.
Основное применение
Начните с Find if Path Exists in Graph и News Distribution. Accounts Merge добавляет отображение строк в id; задачи о циклах и остове используйте после разбора городов и дорог. Трудное испытание в списке необязательно для первого прохода.
Примените разобранные приёмы самостоятельно. Для каждой задачи сначала сформулируйте требования к ответу и предложите простое решение. Затем подставьте максимальные ограничения, оцените время и память и решите, какое улучшение требуется. Подсказки с идеей и оценкой раскрываются отдельно.
Уровни задают порядок практики в курсе, а не единую шкалу платформ. Если уровень отличается от отметки LeetCode, это указано в карточке. Для Codeforces используется оценка курса.
Лёгкие
Три задачи для закрепления основных приёмов.
- LeetCode · 1971
Find if Path Exists in Graph
Проверить связность по представителям.
Ограничения. До 2·10⁵ вершин и рёбер.
Идея и оценка сложности
Объедините концы каждого ребра, затем сравните find(source) и find(destination). Для source == destination путь уже есть. O(n+mα(n)), память O(n).
- Codeforces · 25D
Roads not only in Berland
Найти лишние рёбра и соединить компоненты.
Ограничения. До 1000 городов, исходно n−1 дорог.
Идея и оценка сложности
Ребро внутри компоненты лишнее: сохраните его. Если компонент c, нужны c−1 новых соединений; столько же лишних рёбер можно заменить. Для курса это первая задача на DSU с выдачей конструкции.
- Codeforces · 1167C
News Distribution
Объединять группу линейным числом операций.
Ограничения. n,m и сумма размеров групп до 5·10⁵.
Идея и оценка сложности
Каждого участника группы объединяйте с первым, а не со всеми остальными. Затем выдавайте размер представителя каждого пользователя. Пустые группы не создают операций.
Средние
Две задачи на сочетание идей и анализ ограничений.
- LeetCode · 684
Redundant Connection
Распознать замыкание цикла очередным ребром.
Ограничения. До 1000 вершин; дерево дополнено одним ребром.
Идея и оценка сложности
Проходите рёбра в исходном порядке. Если концы уже связаны, ребро замыкает цикл; иначе union. Для этой постановки так получается нужное позднее ребро. O(nα(n)).
- LeetCode · 721
Accounts Merge
Совместить DSU и отображение строк в индексы.
Ограничения. До 1000 аккаунтов, до 9 адресов на аккаунт; длина строки до 30.
Идея и оценка сложности
Общий email связывает аккаунты, одинаковое имя — нет. Храните первый аккаунт каждого адреса, объединяйте повторные. В конце соберите уникальные адреса по корням и отсортируйте; включите сортировку и цену строк в оценку.
Тяжёлая
Одна задача для переноса идеи на новую постановку.
- LeetCode · 1579
Remove Max Number of Edges to Keep Graph Fully Traversable
Доказать порядок обработки общих рёбер в двух DSU.
Ограничения. До 10⁵ вершин и 10⁵ рёбер трёх типов.
Идея и оценка сложности
Общие рёбра обрабатывайте раньше личных и применяйте к обоим DSU. Затем добавляйте личные, считая действительно нужные соединения. Ответ существует только если обе структуры связны. O((n+m)α(n)).
Дальше в динамическом программировании граф зависимостей поможет выбирать порядок подзадач. Сливаемые кучи доступны как отдельный продвинутый модуль.
Дополнительные материалыДля углублённого разбора · не влияют на прохождение
Анализ только с одной эвристикой
Мы добавили две эвристики и должны разделить их гарантии. Нельзя объяснять почти постоянное время только словами «дерево стало маленьким»: нужна оценка последовательности операций.
Только объединение по рангу обеспечивает логарифмическую высоту. Только сжатие путей тоже сильно помогает на последовательностях запросов, но количественная оценка отличается и доказывается другим способом.
Совместное применение рангов и сжатия путей даёт существенно более сильную амортизированную границу. В лекции анализ строится через классы рангов и многократное пересоединение ссылок.
Форма оценки для m операций над n элементами при стандартных предположениях о последовательности операций.
Почему не просто O(m)?
Без эвристик отдельный find может стоить Θ(n). Только объединение по рангу или размеру даёт O(log n) в худшем случае. Совместно со сжатием получаем O(n + m·α(n)) на инициализацию n элементов и последовательность m запросов в обычной записи оценки. Это не обещание O(1) для каждого отдельного запроса; исходная двухпараметрическая форма α(m,n) ниже описывает ту же теорию при согласованных обозначениях.
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Выбор по максимальным ограничениям
Для 200 000 вершин и рёбер нельзя после каждого добавления заново обходить весь граф. DSU оплачивает инициализацию один раз и дальше обрабатывает запросы почти линейно суммарно.
В формуле появилась α. Разберём её смысл, не подменяя доказательство красивой таблицей больших чисел.
От быстрых функций к обратной функции Аккермана
Обратная функция Аккермана возникает в строгом учёте повторных подъёмов. Для использования DSU не нужно вычислять огромные значения самой функции; полезнее понять, почему оценка амортизированная и какие предположения в неё входят.
Обратная функция Аккермана (Inverse Ackermann function) — Крайне медленно растущая функция, возникающая в точном амортизированном анализе DSU.
Структура доказательства: ранги ограничивают глубины; сжатие ускоряет многократно повторяющиеся переходы; классы, заданные быстро растущей иерархией функций, помогают посчитать общее число проходов через ребра.
| Иллюстрация роста | Значение |
|---|---|
| x + 1 | 5 |
| 2x | 8 |
| 2ˣ | 16 |
| 2^(2ˣ) | 2^16 |
Это иллюстрация ускорения роста, не определение функции Аккермана и не вычислитель α(n). Обратная функция отвечает, сколько ступеней роста нужно, чтобы превысить заданную границу.
Зачем доказательство сложнее алгоритма
Упрощённая иерархия быстрорастущих функций помогает увидеть идею обращения, но сама по себе не является доказательством оценки DSU. Полное доказательство группирует ранги и считает, сколько раз вершина может переходить к более высокому классу. Для практических размеров α чрезвычайно мала, однако ввод и хранение n элементов всё равно требуют Ω(n) времени и памяти.
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Не вычисляйте α в решении
В реализации нужны только массивы и локальные операции. Обратная функция участвует в доказательстве, а не является подпрограммой для каждого find; считать большие числа Аккермана не требуется.
Теперь достаточно знать интерфейс и область применения. Применим его к компонентам и минимальному остовному лесу.