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

Материал из Викиконспекты
Перейти к: навигация, поиск
(Structure)
м
Строка 5: Строка 5:
 
* <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>.
+
Существует реализация такой структуры, где <tex>\mathrm{order(p, q)}</tex> выполняется за истинную, а команды добавления и удаления за амортизационную <tex>O(1)</tex>.
Потребность в списке с командой порядка возникает, к примеру, в реализации [[Персистентные структуры данных|персистентного дерева поиска]].
+
Проблема поддержки порядка в списке возникает, к примеру, при реализации [[Персистентные структуры данных|персистентного дерева поиска]].
  
 
== Алгоритм ==
 
== Алгоритм ==
 
=== Идея ===
 
=== Идея ===
 
[[Файл:ListABCDwithMarks.jpg|250px|thumb|right|Пример расставления меток для списка, <tex>u=3</tex>.]]
 
[[Файл:ListABCDwithMarks.jpg|250px|thumb|right|Пример расставления меток для списка, <tex>u=3</tex>.]]
Все операции кроме <tex>\mathrm{order(p,q)}</tex> за <tex>O(1)</tex> может выполнить обычный [[Список|двусвязный список]]. Проблема возникает в тот момент, когда мы хотим получить порядок элементов, так как связный список не хранит информацию о нём. Пусть каждому узлу сопоставлено некоторое число, при этом все числа строго возрастают от начала списка к его концу. Таким образом, эти числа, которые в дальнейшим будут называться метками, задают порядок на элементах списка. И, зная ссылки на два элемента <tex>p</tex> и <tex>q</tex>, можно за <tex>O(1)</tex> ответить на запрос <tex>\mathrm{order(p,q)}</tex>, просто сравнив их метки. Заметим, что добавление меток никак не влияет на реализацию операции <tex>\mathrm{remove(p)}</tex>. Однако с реализацией <tex>\mathrm{insert(p,q)}</tex> возникают проблемы: при добавлении нового элемента <tex>q</tex> после узла <tex>p</tex>, узлу <tex>q</tex> надо присвоить метку, которая строго больше предыдущего элемента и строго меньше следующего. Исходя из предположения, что метки имеют конечную длину, в какой-то момент возникнет ситуация, что новой метки не найдётся. Тогда будем выдавать элементам списка новые метки. Теперь рассмотрим алгоритм, который позволяет реализовать эту идею.
+
Все операции кроме <tex>\mathrm{order(p,q)}</tex> за <tex>O(1)</tex> может выполнить обычный [[Список|двусвязный список]]. Однако с его помощью невозможно получить информацию о порядке объектов, поэтому сопоставим каждому узлу некоторое число так, чтобы все числа строго возрастали от начала к концу списка. Таким образом, эти числа, которые в дальнейшим будут называться метками, задают порядок на элементах списка.
 +
 
 +
Ответить на запрос <tex>\mathrm{order(p,q)}</tex> можно за <tex>O(1)</tex>, просто сравнив метки <tex>p</tex> и <tex>q</tex>. Заметим, что добавление меток никак не влияет на реализацию операции <tex>\mathrm{remove(p)}</tex>. Однако реализацию <tex>\mathrm{insert(p,q)}</tex> потребуется изменить: при добавлении нового элемента <tex>q</tex> после узла <tex>p</tex>, узлу <tex>q</tex> надо присвоить метку, которая строго больше предыдущего элемента и строго меньше следующего. Исходя из предположения, что метки имеют конечную длину, в какой-то момент возникнет ситуация, что новой метки не найдётся, тогда перераспределим среди элементов списка метки так, чтобы <tex>q</tex> хватило метки. Далее будет рассмотрен алгоритм, который позволяет эффективно реализовать эту идею.
  
 
=== Алгоритм за O(logn) ===
 
=== Алгоритм за O(logn) ===
 +
==== Способ хранения меток ====
 
Метки будем хранить в виде чисел в двоичной системе счисления. Посмотрим, какой длины должны быть метки, чтобы перераспределения не случались слишком часто. Если <tex>u</tex> {{---}} длина каждой метки, то для начала пусть <tex>u:\dfrac{n}{2}<2^u \leqslant 2n</tex>, где <tex>n</tex> {{---}} количество элементов в списке. Если после добавления или удаления элементов <tex>u</tex> перестанет удовлетворять неравенству, пересчитаем все метки заново. Пересчет меток занимает амортизационно <tex>O(1)</tex> по аналогии с саморасширяющимся массивом. Позже, в доказательстве времени работы, мы несколько уточним значение <tex>u</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>u</tex>.
  
 +
Будем хранить все метки в [[Сверхбыстрый цифровой бор | цифровом боре]] высоты <tex>u</tex> (там хранятся не только используемые метки, а вообще все возможные заданной длины). А также будем хранить:
 +
* <b>в листьях</b> {{---}} используется ли уже эта метка. Пусть <tex>\mathrm{weight(x)}</tex> {{---}} это количество помеченных (используемых) листьев (меток) в поддереве <tex>x</tex>, а <tex>\mathrm{size(x)}</tex> {{---}} это количество всех листьев в поддереве <tex>x</tex>;[[Файл:UBitTreeExample.jpg|350px|thumb|right|Пример цифрового бора для меток, где узел с крестиком {{---}} переполненный узел, а с галочкой {{---}} непереполненный для <tex>\alpha=1,5</tex>.]]
 +
* <b>в нелистовых узлах</b> {{---}} является ли узел переполненным. Для любой <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>. Получается, что, чем выше, тем более разреженными должны быть поддеревья непереполненных узлов.
  
<b>Способ хранения меток.</b> Будем хранить все метки в [[Сверхбыстрый цифровой бор | цифровом боре]] высоты <tex>u</tex> (там хранятся не только используемые метки, а вообще все возможные заданной длины). В узлах будем хранить:
+
==== Перераспределение меток ====
* <b>в листьях</b> будем хранить, используется ли уже эта метка. Пусть <tex>\mathrm{weight(x)}</tex> {{---}} это количество помеченных (используемых) листьев (меток) в поддереве <tex>x</tex>, а <tex>\mathrm{size(x)}</tex> {{---}} это количество всех листьев в поддереве <tex>x</tex>;[[Файл:UBitTreeExample.jpg|350px|thumb|right|Пример цифрового бора для меток, где узел с крестиком {{---}} переполненный узел, а с галочкой {{---}} непереполненный для <tex>\alpha=1,5</tex>.]]
+
Тогда, как только мы получаем команду вставить элемент, которому не хватает метки, мы поднимаемся вверх от метки элемента <tex>p</tex>, пока не найдем первый непереполненный узел. Может случиться такое, что на всем пути до корня мы не найдем ни одного непереполненного узла. Чтобы этого избежать, изменим требования к <tex>u</tex> позже. Как только мы нашли первый непереполненный узел, переназначим метки в его поддереве так, чтобы они находились друг от друга на одинаковых расстояниях (места точно хватит, так как <tex>\dfrac{\mathrm{weight(x)}}{\mathrm{size(x)}}\leqslant\dfrac{1}{\alpha^{\mathrm{height(x)}}}</tex>, если узел непереполненный). После этого между занятыми метками будет не меньше <tex>\alpha^{\mathrm{height(x)}}</tex> свободных меток.
* <b>в нелистовых узлах</b> будем хранить, является ли узел переполненным. Для любой <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>. Получается, что, чем выше, тем более разреженными должны быть поддеревья непереполненных узлов.
 
 
 
 
 
<b>Перераспределение меток.</b> Тогда, как только мы получаем команду вставить элемент, которому не хватает метки, мы поднимаемся вверх от метки элемента <tex>p</tex>, пока не найдем первый непереполненный узел. Может случиться такое, что на всем пути до корня мы не найдем ни одного непереполненного узла. Чтобы этого избежать, изменим требования к <tex>u</tex> позже. Как только мы нашли первый непереполненный узел, переназначим метки в его поддереве так, чтобы они находились друг от друга на одинаковых расстояниях (места точно хватит, так как <tex>\dfrac{\mathrm{weight(x)}}{\mathrm{size(x)}}\leqslant\dfrac{1}{\alpha^{\mathrm{height(x)}}}</tex>, если узел непереполненный). После этого плотность распределения всех занятых листьев составит примерно <tex>\dfrac{1}{\alpha^{\mathrm{height(x)}}}</tex>.
 
 
 
  
<b>Докажем амортизационную стоимость операции добавления.</b>
+
==== Доказательство времени работы ====
 
* С одной стороны, повторное перераспределение меток в поддереве узла <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)} \cdot (\dfrac{1}{\alpha^{\mathrm{height(x) - 1}}} - \dfrac{1}{\alpha^{\mathrm{height(x)}}}) = \mathrm{size(y)} \cdot \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)} \cdot (\dfrac{1}{\alpha^{\mathrm{height(x) - 1}}} - \dfrac{1}{\alpha^{\mathrm{height(x)}}}) = \mathrm{size(y)} \cdot \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>\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> операций добавления.  

Версия 01:54, 11 апреля 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]\mathrm{order(p, q)}[/math] выполняется за истинную, а команды добавления и удаления за амортизационную [math]O(1)[/math]. Проблема поддержки порядка в списке возникает, к примеру, при реализации персистентного дерева поиска.

Алгоритм

Идея

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

Все операции кроме [math]\mathrm{order(p,q)}[/math] за [math]O(1)[/math] может выполнить обычный двусвязный список. Однако с его помощью невозможно получить информацию о порядке объектов, поэтому сопоставим каждому узлу некоторое число так, чтобы все числа строго возрастали от начала к концу списка. Таким образом, эти числа, которые в дальнейшим будут называться метками, задают порядок на элементах списка.

Ответить на запрос [math]\mathrm{order(p,q)}[/math] можно за [math]O(1)[/math], просто сравнив метки [math]p[/math] и [math]q[/math]. Заметим, что добавление меток никак не влияет на реализацию операции [math]\mathrm{remove(p)}[/math]. Однако реализацию [math]\mathrm{insert(p,q)}[/math] потребуется изменить: при добавлении нового элемента [math]q[/math] после узла [math]p[/math], узлу [math]q[/math] надо присвоить метку, которая строго больше предыдущего элемента и строго меньше следующего. Исходя из предположения, что метки имеют конечную длину, в какой-то момент возникнет ситуация, что новой метки не найдётся, тогда перераспределим среди элементов списка метки так, чтобы [math]q[/math] хватило метки. Далее будет рассмотрен алгоритм, который позволяет эффективно реализовать эту идею.

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

Способ хранения меток

Метки будем хранить в виде чисел в двоичной системе счисления. Посмотрим, какой длины должны быть метки, чтобы перераспределения не случались слишком часто. Если [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]u[/math].

Будем хранить все метки в цифровом боре высоты [math]u[/math] (там хранятся не только используемые метки, а вообще все возможные заданной длины). А также будем хранить:

  • в листьях — используется ли уже эта метка. Пусть [math]\mathrm{weight(x)}[/math] — это количество помеченных (используемых) листьев (меток) в поддереве [math]x[/math], а [math]\mathrm{size(x)}[/math] — это количество всех листьев в поддереве [math]x[/math];
    Пример цифрового бора для меток, где узел с крестиком — переполненный узел, а с галочкой — непереполненный для [math]\alpha=1,5[/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]p[/math], пока не найдем первый непереполненный узел. Может случиться такое, что на всем пути до корня мы не найдем ни одного непереполненного узла. Чтобы этого избежать, изменим требования к [math]u[/math] позже. Как только мы нашли первый непереполненный узел, переназначим метки в его поддереве так, чтобы они находились друг от друга на одинаковых расстояниях (места точно хватит, так как [math]\dfrac{\mathrm{weight(x)}}{\mathrm{size(x)}}\leqslant\dfrac{1}{\alpha^{\mathrm{height(x)}}}[/math], если узел непереполненный). После этого между занятыми метками будет не меньше [math]\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)} \cdot (\dfrac{1}{\alpha^{\mathrm{height(x) - 1}}} - \dfrac{1}{\alpha^{\mathrm{height(x)}}}) = \mathrm{size(y)} \cdot \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_{\frac{ 2}{\alpha}} n[/math]. Тогда операция добавления работает за [math]O(\log n)[/math].

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

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

Предыдущий алгоритм работает за логарифм из-за того, что слишком часто приходится делать перераспределение меток. Используем [math]\mathrm{y}{-}\mathrm{fast}{-}\mathrm{trie}[/math] (модифицированный цифровой бор), чтобы улучшить время работы операции добавления до [math]O(1)[/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}{-}\mathrm{fast}{-}\mathrm{trie}[/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.

См. также

Примечания

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