Splay-дерево (дерево Тарьяна-Слейтора) - двоичное дерево поиска, созданное Робертом Тарьяном и Даниелем Слейтером специально для ускорения работы другой структуры данных, которую мы рассмотрим позже. Splay-дерево позволяет быстро получать доступ к недавно использованным данным (вершинам) засчёт их "поднятия" к корню и имеет амортизированное время работы
Основная операция в splay-дереве - это операция
-
$split(x, k)$ - так же, как и в декартовом дереве, принимает исходное дерево$x$ и возвращает два дерева$l$ и$r$ , причём все ключи в$l$ меньше$k$ , а все ключи в$r$ - не меньше$k$ . Выполним операцию$find(k)$ в дереве$x$ , после которой найденная вершина$u$ окажется корнем$x$ ; заметим, что тогда ключи всех вершин в поддереве левого сына$u$ (обозначим его за$l$ ) будут меньше$k$ , то есть мы можем "отрезать" левое поддерево от$u$ и вернуть пару${l, u}$ . -
$merge(l, r)$ - принимает два дерева$l$ и$r$ (причём все ключи в$l$ меньше всех ключей в$r$ ) и возвращает новое дерево$x$ , состоящее из всех вершин$l$ и$r$ . При помощи операции$find$ найдём в$r$ вершину с минимальным ключом, после чего она станет корнем$r$ ; заметим, что у неё не будет левого сына, тогда мы просто сделаем этим левым сыном всё дерево$l$ . Остальные операции реализуются через эти три (например,$add(x)$ ,$remove(x)$ и т.д).
Если реализовать
Обозначим за
Операция
Операция
Таким образом, для реализации
-
$v$ - непосредственный сын корня. Тогда выполняем$zig(v)$ . -
$L(v) = L(p(v))$ - тогда выполняем$zig-zig(v)$ . -
$L(v) \neq L(p(v)$ - тогда выполняем$zig-zag(v)$ .
Давайте считать, что splay-дерево хранит некоторый массив, причём все элементы массива, за которые отвечают вершины в левом поддереве
struct node {
int x, sz = 1;
int p = -1, l = -1, r = -1;
bool lft = 0;
node() {}
node(int x) : x(x) {}
};
node v[maxn];
int root = -1, mx = 0;
int gsz(int x) {
return (x == -1 ? 0 : v[x].sz);
}
void upd(int x) {
v[x].sz = 1 + gsz(v[x].l) + gsz(v[x].r);
}
// Отсоединяет вершину от предка, обновляя необходимые параметры
void disconnect(int x) {
if (x == -1 || v[x].p == -1) return;
if (v[v[x].p].l == x) v[v[x].p].l = -1;
else v[v[x].p].r = -1;
upd(v[x].p); v[x].p = -1; v[x].lft = 0;
}
// Делает вершину x левым или правым (в зависимости от lft) сыном y
void connect(int x, int y, bool lft) {
if (x == -1 || y == -1) return;
v[x].lft = lft; v[x].p = y;
if (lft) v[y].l = x;
else v[y].r = x;
upd(y);
}
void rotate(int x) {
int y = v[x].p, z = v[y].p, yl = v[y].lft, xl = v[x].lft;
disconnect(x); disconnect(y);
if (xl) {
int b = v[x].r;
disconnect(b); connect(b, y, 1); connect(y, x, 0);
} else {
int b = v[x].l;
disconnect(b); connect(b, y, 0); connect(y, x, 1);
}
connect(x, z, yl);
}
// Если вместо expose написать splay, то больше людей поймут, что вы написали splay-дерево, посмотрев код посылки на кф
void splay(int x) {
if (x == -1) return;
while (v[x].p != -1) {
int y = v[x].p;
if (v[y].p == -1) {
rotate(x);
break;
}
if (v[x].lft == v[y].lft) {
rotate(y);
rotate(x);
} else {
rotate(x);
rotate(x);
}
}
root = x;
}
// Функция find, которую я зачем-то назвал get
int get(int x, int k) {
if (x == -1) return -1;
while (1) {
if (gsz(v[x].l) == k)
break;
if (k < gsz(v[x].l)) {
x = v[x].l;
} else {
k -= (gsz(v[x].l) + 1);
x = v[x].r;
}
}
splay(x);
return x;
}
pair<int, int> split(int x, int k) {
if (x == -1) return {-1, -1};
if (k == 0) return {-1, x};
int aut = get(x, k - 1);
int gg = v[aut].r;
disconnect(gg);
return {aut, gg};
}
int merge(int l, int r) {
if (l == -1) return r;
if (r == -1) return l;
int bs = gsz(l);
int aut = get(l, bs - 1);
connect(r, aut, 0);
return aut;
}
Это первая и последняя реализация splay-дерева, которую я писал, поэтому код такой длинный. Самое болезненное - дебагать обновление параметров вершин при переподвешивании.
Сперва введём необходимые обозначения:
Рассмотрим следующую задачу:
Дан лес из
$n$ вершин. Приходят запросы трёх типов:
-
$link(u,,v)$ - соединить вершины$u$ и$v$ ребром -
$cut(u,,v)$ - удалить ребро между вершинами$u$ и$v$ -
$get(u,,v)$ - найти длину пути между$u$ и$v$ Нужно ответить на все запросы третьего типа. Гарантируется, что после каждого запроса граф остаётся лесом.
Существует структура данных, которая решает эту задачу, с амортизированным временем работы
Подвесим каждое дерево нашего леса за некоторую вершину и ориентируем рёбра в сторону корня. Введём операцию
-
$link(u, v)$ - в дереве вершины$u$ выполним операцию$expose(u)$ , в дереве вершины$v$ - операцию$expose(v)$ , и затем просто соединим$u$ с$v$ ребром, и вершина, в сторону которой будет ориентировано новое ребро, станет новым корнем дерева. -
$cut(u, v)$ - просто удалим ребро между вершинами$u$ и$v$ ; вершина, в сторону которой было ориентировано ребро, останется в старом дереве, а другая вершина окажется в новом, её и следует назначить корнем нового дерева. -
$get(u,,v)$ - выполним операцию$expose(u)$ и найдём глубину вершины$v$ . Если реализовать операцию$expose$ напрямую, то каждый запрос в худшем случае будет обрабатыватся за$O(n)$ ; однако при помощи структур данных можно добиться амортизированного$O(n,log^2,n)$ или даже$O(n,log,n)$ .
Мы описали в общих чертах алгоритм; теперь нужно понять, как выполнять эти операции быстро. Оказывается, что если мы разобьём каждое дерево на набор вершинно непересекающихся вертикальных путей и будем поддерживать эти пути при помощи декартовых деревьев по неявному ключу, то суммарная асимптотика алгоритма будет равна
TODO