Partially Persistent List
Оглавление
В этом разделе мы научимся делать список частично персистентным за $O(1)$. Определим типы запросов, которые мы сможем поддерживать:
AddAfter(u, x)- вставить новую вершинуxпосле вершиныuDelete(u)- удалить вершинуuNext/Prev(u_k)- получить следующую/предыдущую вершину в списке от вершиныu(зная, что она версииk)- Конечно в каждой вершине можно хранить дополнительную информацию, но нас это не интересует.
Для простоты мы разберем только запрос добавления, а удаление будет аналогично.
Проблема
Предположим, что мы захотим воспользоваться методом fat node, аналогично персистентному массиву. Тогда в каждой вершине нам нужно будет хранить список указателей на все следующие и предыдущие вершины всех версий.
1
2
3
4
5
6
7
8
9
10
11
struct Neighbor {
int version;
Node *ptr;
}
struct Node {
vector<Neighbor> next;
vector<Neighbor> prev;
// maybe some additional information and its versions
// ...
}
Такой подход сделает структру персистентной, но запрос Next/Prev будет работать за $O(\log n)$ из-за бинпоиска нужной версии.
Решение
Ограничим список из next/prev всего лишь двумя версиями.
1
2
3
4
struct Node {
optional<Neighbor> next[2];
optional<Neighbor> prev[2];
}
Главный вопрос - что делать, если на шаге $k$ у нас уже есть две заполненные версии next у вершины $u$, и мы хотим добавить новую вершину $v$ после $u$? В таком случае мы будем применять технику copy path:
- Если $u$ уже имеет две версии
next, то мы создаем новую вершину $u’$, соединяя ее с $prev(u)$ и $v$. - Если $prev(u)$ тоже имел две версии
next, то придется проделать такую же процедуру копирования, пока не дойдем до вершины, у которой есть свободная версияnext. - Аналогично делаем и для вершины $next(u)$, предком которой теперь будет $v$.
Рассмотрим на примере: пусть есть список из 5 вершин версии $V_0$ (черный):

Теперь персистентно добавим вершину $F$ между $B$ и $C$, поскольку у каждой из них занят только один next/prev, то мы лишь добавим указатели на $F$ в $B$ и $C$:

Теперь добавим вершину $G$ между $C$ и $D$. Опять же у $C$ и $D$ только по одному next, поэтому мы просто добавим указатели на $G$ в $C$ и $D$:

Теперь добавим вершину $H$ между $F$ и $G$. У каждой из них опять же есть один свободный next/prev, поэтому мы просто добавим указатели на $H$ в $F$ и $G$:

Наконец, добавим вершину $K$ между $B$ и $F$, тогда у $B$ и $F$ уже есть по два занятых next/prev, поэтому нам придется создать новую вершину $B’$, аналогично будет для почти всех вершин за исключением $A$ и $E$ (у них по одному занятому next/prev):

Анализ времени работы
Утверждается, что такая структура будет работать за $O(1)$ в среднем. Для доказательства воспользуемся банковским методом:
- В каждой вершине при переходе из $1$ занятого
nextв $2$ занятыхnextмы добавляем $1$ монетку в вершину. В таком случае в алгоритме не происходит копирований, поскольку у вершины хватает свободной версииnext. - Если мы копируем вершину, то мы платим $1$ монетку из вершины, которую ранее мы гарантированно имели, поскольку она обязана была перейти из $1$ занятого
nextв $2$ занятыхnext. А больше вершину копировать из-за переполненияnextне придется. - Аналогично для
prev.
Таким образом баланс всегда неотрицательный и на каждом шаге мы добавляли не больше $2$ монетки в вершину, а значит мы можем гарантировать $O(1)$ в среднем.