СНМ с операцией удаления за О(1) — различия между версиями
(→Расширение структуры данных) |
(→Реализация операции Find) |
||
Строка 81: | Строка 81: | ||
=== Реализация операции Find === | === Реализация операции Find === | ||
− | В нашей реализации операции <tex>\mathrm {Find}</tex> вместо уже известного нам метода '''сжатия путей (path compressing)''' мы будем использовать '''разделение путей (path splitting)'''. Он заключается в том, чтобы при выполнении операции <tex>\mathrm {Find}</tex> перевешивать элемент, от которого мы вызвались, не сразу к корню, а к собственному "дедушке". Такой метод сокращения пути приводит к той же амотризационной оценке для функции <tex>\mathrm {Find}</tex><ref>[http://bioinfo.ict.ac.cn/~dbu/AlgorithmCourses/Lectures/Union-Find-Tarjan.pdf|Robert E. Tarjan and Jan van Leeuwen. Worst-case analysis of set union algorithms. Journal of the ACM, 31(2):245–281, 1984.]</ref>, несмотря на то, что интуитивно кажется более медленным. Мы будем использовать именно разделение путей, потому что это серьезно упрощает поддержку списков и инвариантов. | + | В нашей реализации операции <tex>\mathrm {Find}</tex> вместо уже известного нам метода '''сжатия путей (path compressing)''' мы будем использовать '''разделение путей (path splitting)'''. Он заключается в том, чтобы при выполнении операции <tex>\mathrm {Find}</tex> перевешивать элемент, от которого мы вызвались, не сразу к корню, а к собственному "дедушке". Такой метод сокращения пути приводит к той же амотризационной оценке для функции <tex>\mathrm {Find}</tex><ref>[http://bioinfo.ict.ac.cn/~dbu/AlgorithmCourses/Lectures/Union-Find-Tarjan.pdf|Robert E. Tarjan and Jan van Leeuwen. Worst-case analysis of set union algorithms. Journal of the ACM, 31(2):245–281, 1984.]</ref>, несмотря на то, что интуитивно кажется более медленным. Мы будем использовать именно разделение путей, потому что это серьезно упрощает поддержку списков и инвариантов.<br/> |
+ | ==== Операция Relink ==== | ||
+ | Реализуем процедуру <tex> \mathrm { Relink(x) } </tex> {{---}} переподвешивание элемента <tex>x</tex> к его "дедушке" с сохранением инвариантов и структуры списков. | ||
+ | |||
+ | # Удалим <tex> x </tex> из <tex> \mathrm{C_{LIST}} \; p(x)</tex> и вставим его в <tex> \mathrm{C_{LIST}} \; p(p(x))</tex> следующим образом: | ||
+ | #* Если <tex>x</tex> имеет брата справа от себя, вставим его в <tex> \mathrm{C_{LIST}} \; p(p(x))</tex> сразу слева от <tex> p(x) </tex> | ||
+ | #* Иначе (если <tex> x </tex> {{---}} крайний правый сын <tex>p(x)</tex>) {{---}} вставим <tex> x </tex> сразу справа от <tex> p(x) </tex> | ||
+ | # Обновим <tex> \mathrm {DFS_{LIST}} </tex> следующим образом: | ||
+ | #* Если <tex>x</tex> {{---}} крайний правый сын <tex>p(x)</tex>, то на предыдущем шаге мы вставили его в список детей <tex>p(p(x))</tex> сразу после <tex>p(x)</tex>, следовательно {{---}} порядок обхода вершин из <tex> p(p(x)) </tex> в глубину не изменился. Значит, нам не нужно менять <tex> \mathrm {DFS_{LIST}} </tex>. | ||
+ | #* В противном случае нам нужно поместить участок <tex> \mathrm {DFS_{LIST}} </tex>, соответствующий поддереву <tex>x</tex>, перед <tex>p(x) </tex>. Этот участок {{---}} полуинтервал <tex>[x, l)</tex>, где <tex>l</tex> {{---}} сосед <tex>x</tex> справа. Вырежем его и вставим перед <tex>p(x) </tex>. | ||
+ | # Если после этого <tex> p(x) </tex> стал листом, присвоим <tex> rank(p(x)) := 0 </tex> и удалим <tex> p(x) </tex> из <tex>\mathrm{NL_{LIST}}</tex> корня дерева. | ||
+ | # Если после этого <tex>\mathrm{NL_{LIST}}</tex> корня стал пустым, присвоим <tex> rank(root) := 1 </tex> | ||
== Примечания == | == Примечания == |
Версия 23:55, 10 июня 2014
Реализация системы непересекающихся множеств с помощью леса корневых деревьев не поддерживает операцию удаления элемента из множества. Приведенная ниже модификация этой структуры данных вводит поддержку операции удаления за истинную , сохраняя асимптотику для операций и и потребление памяти .
Содержание
Введение
Наша структура данных должна поддерживать следующие операции:
- — создать новое множество из 1 элемента . Время:
- — объединить множества A и B в одно. Время: , без учета времени на операцию , которая используется, если множества A и B заданы своими произвольными представителями.
- — найти множество, в котором содержится элемент . Время; в худшем случае, — в среднем ( — обратная функция Аккермана), где n — размер множества.
- — удалить элемент x из содержащего его множества. Время: O(1)
В дальнейшем мы будем использовать следующие понятия и обозначения:
- — размер множества A (количество элементов в нем).
- — корень дерева
- — высота вершины : если является листом, то , иначе .
- — родитель вершины . Если - корень, то считаем, что
- — ранг вершины, некоторая верхняя оценка на ее высоту.
Как и в обычной реализации, выполнено следующее:
Идея
В реализации СНМ с помощью леса корневых деревьев мы не можем удалить произвольную вершину из множества за разумное время - в таком случае нам придется переподвешивать
Соображение 1: Пусть мы умеем менять произвольные вершины местами за . Тогда для удаления некоторой вершины достаточно поменять ее местами с каким-нибудь листом и удалить этот лист.
Соображение 2: Пусть мы умеем находить какой-нибудь лист (неважно, какой именно) в дереве за . Тогда, по соображению 1, мы уже умеем удалять произвольный элемент из дерева за
Все дальнейшие действия направлены на то, чтобы поддержать эти 2 операции, не испортив при этом асимптотику всех остальных.
Реализация
Расширение структуры данных
Расширим лес корневых деревьев следующим образом:
- Для каждой вершины дерева, не являющейся листом, будем хранить двусвязный список ее детей. Будем считать, что дети упорядочены по направлению списка слева направо.
- Для корня каждого дерева храним двусвязный список его детей, не являющихся листьями.
- Для каждого дерева (включая поддеревья) храним циклический двусвязный список его вершин, располагаемых в порядке обхода в глубину, начиная с левой вершины.
- Разделим понятия вершина дерева и элемент множества:
- вершиной дерева назовем объект, содержащий ссылки , и (где необходимо) для каждого из вышеперечисленных списков, а так же ссылку на соответствующий вершине элемент множества;
- элемент множества - объект, содержащий значение элемента и ссылку на соотв. вершину дерева.
Последнее нововведение, очевидно, позволит нам менять элементы в дереве местами за .
Первые три необходимы для нахождения листа в дереве (как оказывается, это гораздо более нетривиальная задача)
Введем также следующие определения:
Определение: |
Дерево, либо состоящее ровно из одной вершины, либо же из 1 вершины ранга 1 и листьев ранга 0, называется сокращенным (англ. "reduced") |
Определение: |
Дерево называется полным (англ. "full"), если каждый из его узлов либо является листом с рангом 0, либо имеет не менее 3 детей. |
В нашей структуре данных будет поддерживаться следующий инвариант: дерево всегда полное или сокращенное.
Этот инвариант влечет за собой очевидные следствия:
- Все деревья (и поддеревья) размера < 4 - сокращенные, а >= 4 - полные
- Каждая вершина, среди детей которой есть хотя бы 1 нелистовая вершина, имеет не менее 3 детей (это не позволяет дереву вытягиваться в бамбук, например)
Реализация операции Makeset
Тривиально:
- Создадим узел и свяжем его с элементом . Установим:
- Создадим для вершины пустые списки и .
- Создадим с одним элементом - вершина
Очевидно, что операция соблюдает инварианты и выполняется за
Реализация операции Union
Пусть
- деревья, реализующие множества и соответственно. Пусть размер одного из деревьев меньше 4; не умаляя общности - . Тогда действуем следующим образом:- Присоединим и для в конец и для
Теперь рассмотрим случай, когда размеры обоих деревьев больше 4. Примем, не умаляя общности, что
. Тогда:- , и если , увеличим на 1.
- Вставим в начала и для
- Вставим для в для сразу после
- Сделаем для пустым. (Мы работаем в предположении, что очистка списка не подразумевает удаления каждого элемента вручную)
Если в качестве идентификаторов множеств нам переданы произвольные представители этих множеств, нам придется запустить процедуру
для каждого из них, чтобы найти корни деревьев. Без учета вызова процедуры мы сделаем операций.Реализация операции Find
В нашей реализации операции [1], несмотря на то, что интуитивно кажется более медленным. Мы будем использовать именно разделение путей, потому что это серьезно упрощает поддержку списков и инвариантов.
Операция Relink
Реализуем процедуру
— переподвешивание элемента к его "дедушке" с сохранением инвариантов и структуры списков.- Удалим
- Если имеет брата справа от себя, вставим его в сразу слева от
- Иначе (если — крайний правый сын ) — вставим сразу справа от
из и вставим его в следующим образом:
- Обновим
- Если — крайний правый сын , то на предыдущем шаге мы вставили его в список детей сразу после , следовательно — порядок обхода вершин из в глубину не изменился. Значит, нам не нужно менять .
- В противном случае нам нужно поместить участок , соответствующий поддереву , перед . Этот участок — полуинтервал , где — сосед справа. Вырежем его и вставим перед .
следующим образом:
- Если после этого стал листом, присвоим и удалим из корня дерева.
- Если после этого корня стал пустым, присвоим