List order maintenance — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
м (minor)
(Better structure)
Строка 1: Строка 1:
 
[[Файл:ListABCD.jpg|250px|thumb|right|Для такого списка команда <tex>\mathrm{order(D,B)}</tex> выдаст <tex>\mathrm{false}</tex>.]]
 
[[Файл:ListABCD.jpg|250px|thumb|right|Для такого списка команда <tex>\mathrm{order(D,B)}</tex> выдаст <tex>\mathrm{false}</tex>.]]
'''List order maintance''' {{---}} проблема поддержки списка со следующими командами:
+
'''List order maintance''' (русск. <i>поддержка порядка в списке</i>) {{---}} проблема поддержки списка со следующими операциями:
 
* <tex>\mathrm{insert(p, q)}</tex> {{---}} вставка нового элемента <tex>p</tex> в список сразу после <tex>q</tex>;
 
* <tex>\mathrm{insert(p, q)}</tex> {{---}} вставка нового элемента <tex>p</tex> в список сразу после <tex>q</tex>;
 
* <tex>\mathrm{remove(p)}</tex> {{---}} удаление элемента <tex>p</tex> из списка;
 
* <tex>\mathrm{remove(p)}</tex> {{---}} удаление элемента <tex>p</tex> из списка;
 
* <tex>\mathrm{order(p, q)}</tex> {{---}} команда, возвращающая <tex>\mathrm{true}</tex> , если <tex>p</tex> в списке находится до <tex>q</tex> и <tex>\mathrm{false}</tex> иначе.
 
* <tex>\mathrm{order(p, q)}</tex> {{---}} команда, возвращающая <tex>\mathrm{true}</tex> , если <tex>p</tex> в списке находится до <tex>q</tex> и <tex>\mathrm{false}</tex> иначе.
  
Структура данных подходит и для односвязных, и для двусвязных списков.
+
Существует реализация структуры, где все операции выполняются за амортизационную <tex>O(1)</tex>.
 +
Список с командой порядка используется в разных алгоритмах и структурах, к примеру, в реализации полностью персистентного дерева поиска.
  
== Алгоритм ==
+
== Алгоритм за O(logn) ==
 
[[Файл:ListABCDwithMarks.jpg|250px|thumb|right|Пример расставления меток для списка, <tex>u=3</tex>.]]
 
[[Файл:ListABCDwithMarks.jpg|250px|thumb|right|Пример расставления меток для списка, <tex>u=3</tex>.]]
Рассмотрим реализацию списка с командой порядка, где все операции выполняются за амортизационную <tex>O(1)</tex>.  
+
Рассмотрим реализацию списка с командой порядка, где все операции выполняются за амортизационную <tex>O(logn)</tex>.  
 
Дадим каждому элементу списка метки длины <tex>u</tex> из битов. Метки будем хранить в виде [[Сверхбыстрый цифровой бор | цифрового бора]]. Пусть <tex>u:\dfrac{n}{2}<2^u \leqslant 2n</tex>, где <tex>n</tex> {{---}} количество элементов в списке. Если после добавления или удаления элементов <tex>u</tex> перестанет удовлетворять неравенству, пересчитаем все метки заново. Пересчет меток занимает амортизационно <tex>O(1)</tex> по аналогии с саморасширяющимся массивом. Пусть метки идут по возрастанию от начала к концу списка, тогда операцию <tex>\mathrm{order(p,q)}</tex> можно сделать, сравнив метки за <tex>O(1)</tex>. Теперь опишем взаимодействие с метками при выполнении других команд.
 
Дадим каждому элементу списка метки длины <tex>u</tex> из битов. Метки будем хранить в виде [[Сверхбыстрый цифровой бор | цифрового бора]]. Пусть <tex>u:\dfrac{n}{2}<2^u \leqslant 2n</tex>, где <tex>n</tex> {{---}} количество элементов в списке. Если после добавления или удаления элементов <tex>u</tex> перестанет удовлетворять неравенству, пересчитаем все метки заново. Пересчет меток занимает амортизационно <tex>O(1)</tex> по аналогии с саморасширяющимся массивом. Пусть метки идут по возрастанию от начала к концу списка, тогда операцию <tex>\mathrm{order(p,q)}</tex> можно сделать, сравнив метки за <tex>O(1)</tex>. Теперь опишем взаимодействие с метками при выполнении других команд.
  
Для выполнения <tex>\mathrm{remove(p)}</tex> просто удалим элемент <tex>p</tex> вместе с его меткой, проверим, удовлетворяет ли <tex>u</tex> неравенству, если нет {{---}} пересчитаем. Для <tex>\mathrm{insert(p,q)}</tex> существуют два возможных случая:
+
* Для выполнения <tex>\mathrm{remove(p)}</tex> просто удалим элемент <tex>p</tex> вместе с его меткой, проверим, удовлетворяет ли <tex>u</tex> неравенству, если нет {{---}} пересчитаем.  
 +
* Для <tex>\mathrm{insert(p,q)}</tex> существуют два возможных случая:
 +
** Метка <tex>r</tex>: <tex>\mathrm{r.label}>\mathrm{p.label} + 1</tex>, где <tex>r</tex> {{---}} следующий за <tex>p</tex> элемент в списке. Тогда между метками <tex>p</tex> и <tex>r</tex> есть свободная метка, которую мы дадим <tex>q:\mathrm{q.label} = \dfrac{\mathrm{p.label}+\mathrm{r.label}}{2}</tex>. После этого опять проверим <tex>u</tex> на соответствие неравенству. [[Файл:UBitTreeListABCD.jpg|350px|thumb|right|Точка {{---}} метка в листе используется.]]
 +
** В случае, если между метками <tex>p</tex> и <tex>q</tex> свободной метки нет, нам придется пересчитать метки следующим образом:
  
Метка <tex>r</tex>: <tex>\mathrm{r.label}>\mathrm{p.label} + 1</tex>, где <tex>r</tex> {{---}} следующий за <tex>p</tex> элемент в списке. Тогда между метками <tex>p</tex> и <tex>r</tex> есть свободная метка, которую мы дадим <tex>q:\mathrm{q.label} = \dfrac{\mathrm{p.label}+\mathrm{r.label}}{2}</tex>. После этого опять проверим <tex>u</tex> на соответствие неравенству.
+
Все метки хранятся в цифровом боре высоты <tex>u</tex> (там хранятся не только используемые метки, а вообще все возможные заданной длины). В листьях будем хранить, используется ли уже эта метка. Пусть <tex>\mathrm{weight(x)}</tex> {{---}} это количество помеченных (используемых) листьев (меток) в поддереве <tex>x</tex>, а <tex>\mathrm{size(x)}</tex> {{---}} это количество всех листьев в поддереве <tex>x</tex>. Для любой <tex>1<\alpha<2</tex> будем считать, что узел дерева переполнен, если <tex>\dfrac{\mathrm{weight(x)}}{\mathrm{size(x)}}>\dfrac{1}{\alpha^{\mathrm{height(x)}}}</tex>. Наличие переполненности мы будем хранить во всех нелистовых узлах. Стоит заметить, что все листья всегда непереполнены. В худшем случае: <tex> \dfrac{\mathrm{weight(leave)}}{\mathrm{size(leave)}} = 1 = \dfrac{1}{\alpha^{\mathrm{height(x)}}} = \dfrac{1}{\alpha^{0}}</tex>. Получается, что, чем выше, тем более разреженными должны быть поддеревья непереполненных узлов.
[[Файл:UBitTreeListABCD.jpg|350px|thumb|right|Точка {{---}} метка в листе используется.]]
 
 
 
В случае, если между метками <tex>p</tex> и <tex>q</tex> свободной метки нет, нам придется пересчитать метки следующим образом. Все метки хранятся в цифровом боре высоты <tex>u</tex> (там хранятся не только используемые метки, а вообще все возможные заданной длины). В листьях будем хранить, используется ли уже эта метка. Пусть <tex>\mathrm{weight(x)}</tex> {{---}} это количество помеченных (используемых) листьев (меток) в поддереве <tex>x</tex>, а <tex>\mathrm{size(x)}</tex> {{---}} это количество всех листьев в поддереве <tex>x</tex>. Для любой <tex>1<\alpha<2</tex> будем считать, что узел дерева переполнен, если <tex>\dfrac{\mathrm{weight(x)}}{\mathrm{size(x)}}>\dfrac{1}{\alpha^{\mathrm{height(x)}}}</tex>. Наличие переполненности мы будем хранить во всех нелистовых узлах. Стоит заметить, что все листья всегда непереполнены. В худшем случае: <tex> \dfrac{\mathrm{weight(leave)}}{\mathrm{size(leave)}} = 1 = \dfrac{1}{\alpha^{\mathrm{height(x)}}} = \dfrac{1}{\alpha^{0}}</tex>. Получается, что чем выше, тем более разреженными должны быть поддеревья непереполненных узлов.
 
 
[[Файл:UBitTreeExample.jpg|350px|thumb|right|Пример цифрового бора для меток, где узел с крестиком {{---}} переполненный узел, а с галочкой {{---}} непереполненный для <tex>\alpha=1,5</tex>.]]
 
[[Файл:UBitTreeExample.jpg|350px|thumb|right|Пример цифрового бора для меток, где узел с крестиком {{---}} переполненный узел, а с галочкой {{---}} непереполненный для <tex>\alpha=1,5</tex>.]]
  
Тогда, как только мы получаем команду вставить элемент, которому не хватает метки, мы поднимаемся вверх от метки элемента <tex>p</tex>, пока не найдем первый непереполненный узел. Может случиться такое, что на всем пути до корня мы не найдем ни одного непереполненного узла. Чтобы этого избежать, изменим требования к <tex>u</tex> позже. Как только мы нашли первый непереполненный узел, переназначим метки в его поддереве так, чтобы они находились друг от друга на одинаковых расстояниях (места точно хватит, так как <tex>\dfrac{\mathrm{weight(x)}}{size(x)}\leqslant\dfrac{1}{\alpha^{\mathrm{height(x)}}}</tex>, если узел непереполненный). После этого плотность распределения всех занятых листьев составит примерно <tex>\dfrac{1}{\alpha^{\mathrm{height(x)}}}</tex>.  
+
Тогда, как только мы получаем команду вставить элемент, которому не хватает метки, мы поднимаемся вверх от метки элемента <tex>p</tex>, пока не найдем первый непереполненный узел. Может случиться такое, что на всем пути до корня мы не найдем ни одного непереполненного узла. Чтобы этого избежать, изменим требования к <tex>u</tex> позже. Как только мы нашли первый непереполненный узел, переназначим метки в его поддереве так, чтобы они находились друг от друга на одинаковых расстояниях (места точно хватит, так как <tex>\dfrac{\mathrm{weight(x)}}{size(x)}\leqslant\dfrac{1}{\alpha^{\mathrm{height(x)}}}</tex>, если узел непереполненный). После этого плотность распределения всех занятых листьев составит примерно <tex>\dfrac{1}{\alpha^{\mathrm{height(x)}}}</tex>.
 +
 +
* Тогда, чтобы после перераспределения меток в поддереве узла <tex>x</tex>, мы заново пришли к этому же узлу за перераспределением, нужно, чтобы его сын снова переполнился. Если <tex>y</tex> {{---}} сын <tex>x</tex>, то он переполнится, когда <tex>\dfrac{\mathrm{weight(y)}}{\mathrm{size(y)}}>\dfrac{1}{\alpha^{\mathrm{height(x) - 1}}}</tex>. Чтобы это произошло, требуется, чтобы было сделано еще <tex>\mathrm{size(y)}*(\dfrac{1}{\alpha^{\mathrm{height(x) - 1}}} - \dfrac{1}{\alpha^{\mathrm{height(x)}}}) = \mathrm{size(y)}*\dfrac{\alpha - 1}{\alpha^{\mathrm{height(x)}}}</tex> добавлений.
  
Тогда чтобы после перераспределения меток в поддереве узла <tex>x</tex>, мы заново пришли к этому же узлу за перераспределением, нужно, чтобы его сын снова переполнился. Если <tex>y</tex> {{---}} сын <tex>x</tex>, то он переполнится, когда <tex>\dfrac{\mathrm{weight(y)}}{\mathrm{size(y)}}>\dfrac{1}{\alpha^{\mathrm{height(x) - 1}}}</tex>. Чтобы это произошло, требуется, чтобы было сделано еще <tex>\mathrm{size(y)}*(\dfrac{1}{\alpha^{\mathrm{height(x) - 1}}} - \dfrac{1}{\alpha^{\mathrm{height(x)}}}) = \mathrm{size(y)}*\dfrac{\alpha - 1}{\alpha^{\mathrm{height(x)}}}</tex> добавлений.
+
* С другой стороны, следующее перераспределение меток произойдет, когда <tex>\mathrm{weight(x)} = \dfrac{\mathrm{size(x)}}{\alpha^{\mathrm{height(x)}}} = \dfrac{2\mathrm{size(y)}}{\alpha^{\mathrm{height(x)}}} </tex>. Получается, что за <tex>\dfrac{2\mathrm{size(y)}}{\alpha^{\mathrm{height(x)}}} </tex> операций перераспределения меток требуется сделать <tex>\mathrm{size(y)}*\dfrac{\alpha - 1}{\alpha^{\mathrm{height(x)}}}</tex> операций добавления.  
  
 +
Тогда если за каждую операцию добавления брать <tex>\dfrac{2}{\alpha-1}</tex> монет, то за добавления накопится столько монет, чтобы расплатиться за следующую  операцию перераспределения в узле <tex>x</tex>. Проблема в том, что таким образом надо платить за каждый уровень, а количество уровней (бит) равно <tex>u</tex>. Тогда амортизированная стоимость добавления <tex>O(u)</tex>.
  
С другой стороны, следующее перераспределение меток произойдет, когда <tex>\mathrm{weight(x)} = \dfrac{\mathrm{size(x)}}{\alpha^{\mathrm{height(x)}}} = \dfrac{2\mathrm{size(y)}}{\alpha^{\mathrm{height(x)}}} </tex>. Получается, что за <tex>\dfrac{2\mathrm{size(y)}}{\alpha^{\mathrm{height(x)}}} </tex> операций перераспределения меток требуется сделать <tex>\mathrm{size(y)}*\dfrac{\alpha - 1}{\alpha^{\mathrm{height(x)}}}</tex> операций добавления. Тогда если за каждую операцию добавления брать <tex>\dfrac{2}{\alpha-1}</tex> монет, то за добавления накопится столько монет, чтобы расплатиться за следующую  операцию перераспределения в узле <tex>x</tex>. Проблема в том, что таким образом надо платить за каждый уровень, а количество уровней (бит) равно <tex>u</tex>. Тогда амортизированная стоимость добавления <tex>O(u)</tex>.
+
Теперь выберем такое <tex>u</tex>, чтобы корень никогда не переполнялся: <tex>\dfrac{\mathrm{weight(root)}}{\mathrm{size(root)}} < \dfrac{1}{\alpha^{\mathrm{height(root)}}} \Rightarrow \dfrac{n}{2^u} < \dfrac{1}{\alpha ^u} \Rightarrow u \geqslant \log_{\dfrac{ 2}{\alpha}} n</tex>. Тогда операция добавления работает за логарифмическое время от <tex>n</tex>.
 
 
Теперь выберем такое <tex>u</tex>, чтобы корень никогда не переполнялся: <tex>\dfrac{\mathrm{weight(root)}}{\mathrm{size(root)}} < \dfrac{1}{\alpha^{\mathrm{height(root)}}} \Rightarrow \dfrac{n}{2^u} < \dfrac{1}{\alpha ^u} \Rightarrow u \geqslant \log_{\dfrac{ 2}{\alpha}} n</tex>. Тогда операция добавления работает за логарифмическое время от <tex>n</tex>, а не за константное.
 
  
 +
== Алгоритм за O(1) ==
 
[[Файл:GlobalandLocalLabelstoConst.jpg|350px|thumb|right|y-fast-tree для меток.]]
 
[[Файл:GlobalandLocalLabelstoConst.jpg|350px|thumb|right|y-fast-tree для меток.]]
 
Улучшим время работы операции добавления до <tex>O(1)</tex>, для этого будем использовать <tex>\mathrm{y-fast-tree}</tex> (улучшенный цифровой бор). Внутри каждого кусочка будем тоже присваивать каждому элементу метку, от <tex>0</tex> до <tex>2^{2u-1}</tex> жадно, тогда у каждого элемента списка будет две метки: глобальная и локальная. Глобальная задает кусочек, локальная {{---}} положение элемента внутри кусочка. Стоит заметить, что внутри кусочка никогда не будет проблемы, что кому-то не хватит метки или придется сделать перераспределение меток, так как, если мы каждый раз в качестве метки будем брать среднее значение, то для того, чтобы был конфликт из-за меток, нужно больше, чем <tex>2u</tex> ключей (противоречит условию). А глобальные метки будут организованы в структуру, описанную выше. Глобальные метки для кусочков нам придется менять, когда один из кусочков переполнился, тогда разделим кусочек на два, присвоив метку второму методом, описанным выше (поднимемся до первого непереполненного). Каждый кусочек будет иметь <tex>u</tex> занятых меток. Аналогично, когда в каком-то кусочке слишком мало ключей становится, мы его сливаем с соседним. Внутри кусочков мы ключи присваиваем за <tex>O(1)</tex>, а, аналогичный приведенному выше, анализ показывает, что, чтобы потребовалось перераспределение глобальных меток, требуется <tex>\Omega(u)</tex> изменений локальных меток. За эти изменения накопим <tex>O(u)</tex> монет для изменения глобальных меток, тогда операция добавления работает за константное время.
 
Улучшим время работы операции добавления до <tex>O(1)</tex>, для этого будем использовать <tex>\mathrm{y-fast-tree}</tex> (улучшенный цифровой бор). Внутри каждого кусочка будем тоже присваивать каждому элементу метку, от <tex>0</tex> до <tex>2^{2u-1}</tex> жадно, тогда у каждого элемента списка будет две метки: глобальная и локальная. Глобальная задает кусочек, локальная {{---}} положение элемента внутри кусочка. Стоит заметить, что внутри кусочка никогда не будет проблемы, что кому-то не хватит метки или придется сделать перераспределение меток, так как, если мы каждый раз в качестве метки будем брать среднее значение, то для того, чтобы был конфликт из-за меток, нужно больше, чем <tex>2u</tex> ключей (противоречит условию). А глобальные метки будут организованы в структуру, описанную выше. Глобальные метки для кусочков нам придется менять, когда один из кусочков переполнился, тогда разделим кусочек на два, присвоив метку второму методом, описанным выше (поднимемся до первого непереполненного). Каждый кусочек будет иметь <tex>u</tex> занятых меток. Аналогично, когда в каком-то кусочке слишком мало ключей становится, мы его сливаем с соседним. Внутри кусочков мы ключи присваиваем за <tex>O(1)</tex>, а, аналогичный приведенному выше, анализ показывает, что, чтобы потребовалось перераспределение глобальных меток, требуется <tex>\Omega(u)</tex> изменений локальных меток. За эти изменения накопим <tex>O(u)</tex> монет для изменения глобальных меток, тогда операция добавления работает за константное время.
Строка 34: Строка 37:
 
== Использование памяти ==
 
== Использование памяти ==
 
Из-за того, что <tex>u</tex> зависит от выбранной <tex>\alpha</tex>, <tex>\alpha</tex> сильно влияет на реализацию. Увеличивая <tex>\alpha</tex>, мы уменьшаем стоимость операции добавления (количество монет, которые надо брать: <tex>\dfrac{2}{\alpha-1}</tex>), но увеличиваем <tex>u</tex>, значит, больше памяти нужно, а, уменьшая <tex>\alpha</tex>, мы выигрываем в памяти, но проигрываем во времени операции добавления. Так как для реализации структуры мы используем <tex>\mathrm{y-fast-tree}</tex>, требуется <tex>O(n)</tex> памяти.
 
Из-за того, что <tex>u</tex> зависит от выбранной <tex>\alpha</tex>, <tex>\alpha</tex> сильно влияет на реализацию. Увеличивая <tex>\alpha</tex>, мы уменьшаем стоимость операции добавления (количество монет, которые надо брать: <tex>\dfrac{2}{\alpha-1}</tex>), но увеличиваем <tex>u</tex>, значит, больше памяти нужно, а, уменьшая <tex>\alpha</tex>, мы выигрываем в памяти, но проигрываем во времени операции добавления. Так как для реализации структуры мы используем <tex>\mathrm{y-fast-tree}</tex>, требуется <tex>O(n)</tex> памяти.
 
== Применение ==
 
Список с командой порядка используется в разных областях, включая персистентные структуры данных, алгоритмы для графов, отказоустойчивые структуры данных.
 
  
 
== Послесловие ==  
 
== Послесловие ==  
Впервые реализацию такой структуры данных со всеми операциями за константное время амортизационно предложили ''Dietz'' и ''Sleator'', однако их доказательство времени работы было намного сложнее вышеизложенного анализа. Поэтому позже группа ученых во главе с ''Michael A. Bender'' разработала более простое доказательство, изложенное выше, впервые описанное в их статье ''Two simlified algorithms for maintaining order in a list''. Послесловие их статьи таково:
+
Впервые реализацию такой структуры данных со всеми операциями за константное время амортизационно предложили<ref>[http://www.cs.cmu.edu/~sleator/papers/maintaining-order.html Статья Dietz and Sleator "Two algorithms for maintaining order in a list"]</ref> ''Dietz'' и ''Sleator'', однако их доказательство времени работы было намного сложнее вышеизложенного анализа. Поэтому позже группа ученых во главе с ''Michael A. Bender'' разработала<ref>[http://link.springer.com/chapter/10.1007%2F3-540-45749-6_17 Статья "Two Simplified Algorithms for Maintaining Order in a List"]</ref> более простое доказательство, изложенное выше, впервые описанное в их статье ''Two simlified algorithms for maintaining order in a list''. Послесловие их статьи таково:
 
 
 
 
Dietz and Sleator is quite influential
 
 
 
With its tags and its proofs by potential
 
  
But to teach it in class
+
  Dietz and Sleator is quite influential
 
+
  With its tags and its proofs by potential
Is a pain in the ass
+
  But to teach it in class
 
+
  Is a pain in the ass
So our new result is preferential.
+
  So our new result is preferential.
 
== См. также==
 
== См. также==
  
Строка 56: Строка 51:
 
* [[Персистентный стек]]
 
* [[Персистентный стек]]
 
* [[Сверхбыстрый цифровой бор]]
 
* [[Сверхбыстрый цифровой бор]]
 +
* [[Персистентные структуры данных]]
 +
 +
== Примечания ==
 +
 +
<references />
  
 
== Источники информации ==
 
== Источники информации ==
Строка 61: Строка 61:
 
* [https://www.lektorium.tv/lecture/14321 Lectorium {{---}} лекция А.С. Станкевича]
 
* [https://www.lektorium.tv/lecture/14321 Lectorium {{---}} лекция А.С. Станкевича]
 
* [https://en.wikipedia.org/wiki/Order-maintenance_problem  Wikipedia {{---}} order maintance problem]
 
* [https://en.wikipedia.org/wiki/Order-maintenance_problem  Wikipedia {{---}} order maintance problem]
* [http://link.springer.com/chapter/10.1007%2F3-540-45749-6_17 Two Simplified Algorithms for Maintaining Order in a List {{---}} Статья ''Michael A. Bender'' с описанием доказательства.]
 
  
 
[[Категория:Дискретная математика и алгоритмы]]
 
[[Категория:Дискретная математика и алгоритмы]]

Версия 23:18, 27 марта 2016

Для такого списка команда [math]\mathrm{order(D,B)}[/math] выдаст [math]\mathrm{false}[/math].

List order maintance (русск. поддержка порядка в списке) — проблема поддержки списка со следующими операциями:

  • [math]\mathrm{insert(p, q)}[/math] — вставка нового элемента [math]p[/math] в список сразу после [math]q[/math];
  • [math]\mathrm{remove(p)}[/math] — удаление элемента [math]p[/math] из списка;
  • [math]\mathrm{order(p, q)}[/math] — команда, возвращающая [math]\mathrm{true}[/math] , если [math]p[/math] в списке находится до [math]q[/math] и [math]\mathrm{false}[/math] иначе.

Существует реализация структуры, где все операции выполняются за амортизационную [math]O(1)[/math]. Список с командой порядка используется в разных алгоритмах и структурах, к примеру, в реализации полностью персистентного дерева поиска.

Алгоритм за O(logn)

Пример расставления меток для списка, [math]u=3[/math].

Рассмотрим реализацию списка с командой порядка, где все операции выполняются за амортизационную [math]O(logn)[/math]. Дадим каждому элементу списка метки длины [math]u[/math] из битов. Метки будем хранить в виде цифрового бора. Пусть [math]u:\dfrac{n}{2}\lt 2^u \leqslant 2n[/math], где [math]n[/math] — количество элементов в списке. Если после добавления или удаления элементов [math]u[/math] перестанет удовлетворять неравенству, пересчитаем все метки заново. Пересчет меток занимает амортизационно [math]O(1)[/math] по аналогии с саморасширяющимся массивом. Пусть метки идут по возрастанию от начала к концу списка, тогда операцию [math]\mathrm{order(p,q)}[/math] можно сделать, сравнив метки за [math]O(1)[/math]. Теперь опишем взаимодействие с метками при выполнении других команд.

  • Для выполнения [math]\mathrm{remove(p)}[/math] просто удалим элемент [math]p[/math] вместе с его меткой, проверим, удовлетворяет ли [math]u[/math] неравенству, если нет — пересчитаем.
  • Для [math]\mathrm{insert(p,q)}[/math] существуют два возможных случая:
    • Метка [math]r[/math]: [math]\mathrm{r.label}\gt \mathrm{p.label} + 1[/math], где [math]r[/math] — следующий за [math]p[/math] элемент в списке. Тогда между метками [math]p[/math] и [math]r[/math] есть свободная метка, которую мы дадим [math]q:\mathrm{q.label} = \dfrac{\mathrm{p.label}+\mathrm{r.label}}{2}[/math]. После этого опять проверим [math]u[/math] на соответствие неравенству.
      Точка — метка в листе используется.
    • В случае, если между метками [math]p[/math] и [math]q[/math] свободной метки нет, нам придется пересчитать метки следующим образом:

Все метки хранятся в цифровом боре высоты [math]u[/math] (там хранятся не только используемые метки, а вообще все возможные заданной длины). В листьях будем хранить, используется ли уже эта метка. Пусть [math]\mathrm{weight(x)}[/math] — это количество помеченных (используемых) листьев (меток) в поддереве [math]x[/math], а [math]\mathrm{size(x)}[/math] — это количество всех листьев в поддереве [math]x[/math]. Для любой [math]1\lt \alpha\lt 2[/math] будем считать, что узел дерева переполнен, если [math]\dfrac{\mathrm{weight(x)}}{\mathrm{size(x)}}\gt \dfrac{1}{\alpha^{\mathrm{height(x)}}}[/math]. Наличие переполненности мы будем хранить во всех нелистовых узлах. Стоит заметить, что все листья всегда непереполнены. В худшем случае: [math] \dfrac{\mathrm{weight(leave)}}{\mathrm{size(leave)}} = 1 = \dfrac{1}{\alpha^{\mathrm{height(x)}}} = \dfrac{1}{\alpha^{0}}[/math]. Получается, что, чем выше, тем более разреженными должны быть поддеревья непереполненных узлов.

Пример цифрового бора для меток, где узел с крестиком — переполненный узел, а с галочкой — непереполненный для [math]\alpha=1,5[/math].

Тогда, как только мы получаем команду вставить элемент, которому не хватает метки, мы поднимаемся вверх от метки элемента [math]p[/math], пока не найдем первый непереполненный узел. Может случиться такое, что на всем пути до корня мы не найдем ни одного непереполненного узла. Чтобы этого избежать, изменим требования к [math]u[/math] позже. Как только мы нашли первый непереполненный узел, переназначим метки в его поддереве так, чтобы они находились друг от друга на одинаковых расстояниях (места точно хватит, так как [math]\dfrac{\mathrm{weight(x)}}{size(x)}\leqslant\dfrac{1}{\alpha^{\mathrm{height(x)}}}[/math], если узел непереполненный). После этого плотность распределения всех занятых листьев составит примерно [math]\dfrac{1}{\alpha^{\mathrm{height(x)}}}[/math].

  • Тогда, чтобы после перераспределения меток в поддереве узла [math]x[/math], мы заново пришли к этому же узлу за перераспределением, нужно, чтобы его сын снова переполнился. Если [math]y[/math] — сын [math]x[/math], то он переполнится, когда [math]\dfrac{\mathrm{weight(y)}}{\mathrm{size(y)}}\gt \dfrac{1}{\alpha^{\mathrm{height(x) - 1}}}[/math]. Чтобы это произошло, требуется, чтобы было сделано еще [math]\mathrm{size(y)}*(\dfrac{1}{\alpha^{\mathrm{height(x) - 1}}} - \dfrac{1}{\alpha^{\mathrm{height(x)}}}) = \mathrm{size(y)}*\dfrac{\alpha - 1}{\alpha^{\mathrm{height(x)}}}[/math] добавлений.
  • С другой стороны, следующее перераспределение меток произойдет, когда [math]\mathrm{weight(x)} = \dfrac{\mathrm{size(x)}}{\alpha^{\mathrm{height(x)}}} = \dfrac{2\mathrm{size(y)}}{\alpha^{\mathrm{height(x)}}} [/math]. Получается, что за [math]\dfrac{2\mathrm{size(y)}}{\alpha^{\mathrm{height(x)}}} [/math] операций перераспределения меток требуется сделать [math]\mathrm{size(y)}*\dfrac{\alpha - 1}{\alpha^{\mathrm{height(x)}}}[/math] операций добавления.

Тогда если за каждую операцию добавления брать [math]\dfrac{2}{\alpha-1}[/math] монет, то за добавления накопится столько монет, чтобы расплатиться за следующую операцию перераспределения в узле [math]x[/math]. Проблема в том, что таким образом надо платить за каждый уровень, а количество уровней (бит) равно [math]u[/math]. Тогда амортизированная стоимость добавления [math]O(u)[/math].

Теперь выберем такое [math]u[/math], чтобы корень никогда не переполнялся: [math]\dfrac{\mathrm{weight(root)}}{\mathrm{size(root)}} \lt \dfrac{1}{\alpha^{\mathrm{height(root)}}} \Rightarrow \dfrac{n}{2^u} \lt \dfrac{1}{\alpha ^u} \Rightarrow u \geqslant \log_{\dfrac{ 2}{\alpha}} n[/math]. Тогда операция добавления работает за логарифмическое время от [math]n[/math].

Алгоритм за O(1)

y-fast-tree для меток.

Улучшим время работы операции добавления до [math]O(1)[/math], для этого будем использовать [math]\mathrm{y-fast-tree}[/math] (улучшенный цифровой бор). Внутри каждого кусочка будем тоже присваивать каждому элементу метку, от [math]0[/math] до [math]2^{2u-1}[/math] жадно, тогда у каждого элемента списка будет две метки: глобальная и локальная. Глобальная задает кусочек, локальная — положение элемента внутри кусочка. Стоит заметить, что внутри кусочка никогда не будет проблемы, что кому-то не хватит метки или придется сделать перераспределение меток, так как, если мы каждый раз в качестве метки будем брать среднее значение, то для того, чтобы был конфликт из-за меток, нужно больше, чем [math]2u[/math] ключей (противоречит условию). А глобальные метки будут организованы в структуру, описанную выше. Глобальные метки для кусочков нам придется менять, когда один из кусочков переполнился, тогда разделим кусочек на два, присвоив метку второму методом, описанным выше (поднимемся до первого непереполненного). Каждый кусочек будет иметь [math]u[/math] занятых меток. Аналогично, когда в каком-то кусочке слишком мало ключей становится, мы его сливаем с соседним. Внутри кусочков мы ключи присваиваем за [math]O(1)[/math], а, аналогичный приведенному выше, анализ показывает, что, чтобы потребовалось перераспределение глобальных меток, требуется [math]\Omega(u)[/math] изменений локальных меток. За эти изменения накопим [math]O(u)[/math] монет для изменения глобальных меток, тогда операция добавления работает за константное время.

Использование памяти

Из-за того, что [math]u[/math] зависит от выбранной [math]\alpha[/math], [math]\alpha[/math] сильно влияет на реализацию. Увеличивая [math]\alpha[/math], мы уменьшаем стоимость операции добавления (количество монет, которые надо брать: [math]\dfrac{2}{\alpha-1}[/math]), но увеличиваем [math]u[/math], значит, больше памяти нужно, а, уменьшая [math]\alpha[/math], мы выигрываем в памяти, но проигрываем во времени операции добавления. Так как для реализации структуры мы используем [math]\mathrm{y-fast-tree}[/math], требуется [math]O(n)[/math] памяти.

Послесловие

Впервые реализацию такой структуры данных со всеми операциями за константное время амортизационно предложили[1] Dietz и Sleator, однако их доказательство времени работы было намного сложнее вышеизложенного анализа. Поэтому позже группа ученых во главе с Michael A. Bender разработала[2] более простое доказательство, изложенное выше, впервые описанное в их статье Two simlified algorithms for maintaining order in a list. Послесловие их статьи таково:

 Dietz and Sleator is quite influential
 With its tags and its proofs by potential
 But to teach it in class
 Is a pain in the ass
 So our new result is preferential.

См. также

Примечания

Источники информации