Загрузил rodzher.zet

Практика по алгоритмам BST, Treap, Персистентность

Реклама
Первый курс, весенний семестр 2020
Практика по алгоритмам #11
BST, Treap, Персистентность
17 апреля
Собрано 22 апреля 2020 г. в 19:28
Содержание
1. BST, Treap, Персистентность
1
2. Разбор задач практики
2
3. Домашнее задание
3.1. Обязательная часть . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2. Дополнительная часть . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
6
7
4. Разбор домашнего задания
4.1. Обязательная часть . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2. Дополнительная часть . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
8
10
Алгоритмы, весна 2020
Практика #11. BST, Treap, Персистентность.
BST, Treap, Персистентность
1. Улучшаем B-tree
a) Merge за 𝒪(log 𝑛) и 𝒪(log𝑘 𝑛) дисковых операций.
b) Split за 𝒪(log 𝑛) и 𝒪(log𝑘 𝑛) дисковых операций.
c) Модификация 𝐵-дерева: в каждой вершине [𝑘, 32 𝑘] ключей.
d) Опишите Split/Merge для RB-Tree.
2. Улучшаем и изучаем Treap
a) Add через один спуск и один split.
b) Del через один спуск и один merge.
c) Нарисуйте все деревья, которые могут получиться в результате операции Merge(бамбук
идущий влево-вниз, вершина) и Merge(бамбук идущий вправо-вниз, вершина).
d) Наивный алгоритм построения дерева от множества пар ⟨𝑥𝑖 , 𝑦𝑖 ⟩:
за линию выбрать корень + рекурсия. Оцените время работы.
e) Пусть пары уже отсортированы по 𝑥𝑖 . Постройте декартово дерево за линейное время.
f) (*) Разрешим хранить равные ключи.
Что будет, если Add = Split + 2Merge? Если Add = GoDown + Split?
Заметим, что GoDown можно делать двумя способами: if(x < root.x) и if(x 6 root.x).
g) (*) Хотим хранить пары ⟨𝑎𝑖 , 𝑏𝑖 ⟩. Умеем по ключу 𝑎𝑖 построить декартово дерево и в каждой вершине поддерживать сумму всех 𝑏𝑖 в поддереве. А можно поддерживать декартово
дерево всех 𝑏𝑖 в поддереве? За сколько будут работать операции с таким деревом?
h) (*) Хотим поддерживать массив, делать ему rotate.
Придумайте, как и не хранить 𝑦, и не вызывать rand().
3. Персистентный поворот
Напишите явно код персистентной версии rotateRight.
4. Детская персистентность
Придумайте персистентный массив, который умеет делать
a) обращение за 𝒪(1), модификацию за 𝒪(𝑛);
b) обращение за 𝒪(𝑚), модификацию за 𝒪(1).
5. Персистентная хеш-таблица
6. Максимальный покрывающий отрезок
Даны отрезки на прямой [𝐿𝑖 , 𝑅𝑖 ], у каждого есть вес 𝑤𝑖 .
В offline/online за 𝒪(log 𝑛) находить отрезок max веса, покрывающий точку 𝑥.
А то же самое, но среди отрезков не длиннее 𝐿?
7. Запросы на плоскости
Дан массив. Online запросы ⟨𝑖, 𝑥⟩: сколько на 𝑎[0..𝑖] чисел 6 𝑥.
8. (*) Антитест для persistent RBST
Предъявите 𝑛 операций с persistent RBST (add, delete, split, merge), которые работают за
Θ(𝑛2 ). Есть ли такая последовательность операций, содержащая только add и delete?
9. (*) Persistent List
Запросы merge, (push/pop)(front/back).
a) Всего не более 𝑁 запросов.
b) А если изначально число запросов неизвестно?
1/11
Алгоритмы, весна 2020
Практика #11. BST, Treap, Персистентность.
Разбор задач практики
1. Улучшаем B-tree
a) Merge за 𝒪(log 𝑛)
Пусть 𝑙 выше 𝑟. Спускаемся в 𝑙 по правой ветви к вершине 𝑣 на нужной высоте.
Удаляем из 𝑟 min 𝑥, то есть самый левый.
Соединяем 𝑣 и 𝑟 через ключ 𝑥: между их крайними детьми должен быть ключ.
Могла получиться слишком большая вершина, исправляем как при добавлении.
Потратили 𝒪(ℎ𝑟 ) на удаление 𝑥, затем 𝒪(ℎ𝑙 − ℎ𝑟 ) на поиск 𝑣 и балансировку полученного
дерева.
b) Split за 𝒪(log 𝑛)
Делим корень по 𝑥 (одна из половин может быть пустой). Получились половины 𝐿 и 𝑅,
между ними ребенок 𝑡. Рекурсивно делим 𝑡 на половины 𝑙 и 𝑟.
Делаем Merge(𝐿, 𝑙). При этом не надо вынимать соединительный ключ из 𝑙. Соединительный ключ уже есть – самый правый в корне 𝐿, ведь идущего за ним ребенка мы отрезали.
То же с 𝑅 и 𝑟.
Мы спустились рекурсивно на 𝒪(log𝑘 𝑛) уровней. При выходе из рекурсии делали Merge
и его результат отдавали в Merge на следующем уровне. Поскольку мы не искали лишние
ключи, каждый Merge работал за разность высот, суммарно выходит высота исходного
дерева.
c) 𝐵 * : у вершин [𝑘, 32 𝑘] ключей
Если вершина переполнилась, пытаемся перекинуть один ключ в брата.
Не получилось ⇒ размер брата ровно 32 𝑘 ⇒ суммарный размер нас с братом 3𝑘 + 1 и ещё
один соединительный ключ в предке между нами.
Делится на 3 вершины по 𝑘 ключей и 2 соединительных ключа.
Если размер вершины 𝑘 − 1, пытаемся отщепить один ключ от двух ближайших братьев
(они могут быть и с разных, и с одной стороны).
Не получилось ⇒ размер братьев ровно по 𝑘 ⇒ суммарный размер 3𝑘 − 1 и 2 соединительных ключа.
Делится на 2 вершины по 23 𝑘 ключей и один соединительный.
Братьев нет только у корня, в нем будет [1, 2𝑘] ключей.
Если стало 2𝑘 + 1, просто делим на две вершины.
Если стало 0, то остался один ребенок, он новый корень.
Если у корня стало два ребенка, один 𝑘 − 1, другой 𝑘, соединяем их с корнем, получаем
новый корень размера 2𝑘.
У 𝐵 * есть два практических преимущества.
У 𝐵 узел заполнен хотя бы наполовину, у 𝐵 * хотя бы на 2/3 от максимума. Это дает в
среднем меньшую глубину.
При переполнении узла в 𝐵 узел сразу делится. Приходится выделять память под новый
узел. А в 𝐵 * мы сначала пытаемся перекидывать ключи, создание новых узлов реже.
d) Split/Merge для RB
RB = 2-3-4-tree
2. Улучшаем и изучаем Treap
a) add через один split
2/11
Алгоритмы, весна 2020
b)
c)
d)
e)
f)
g)
h)
Практика #11. BST, Treap, Персистентность.
Спускаемся по дереву до позиции вставки 𝑣. Ставим вершину с 𝑥 вместо 𝑣, делаем ее
детьми split(𝑣, 𝑥).
delete через один merge
Спускаемся по дереву до удаляемой вершины, заменяем ее на merge ее детей.
Merge(бамбук идущий влево-вниз (вправо-вниз), вершина)
Новая вершина 𝑣 = ⟨𝑥, 𝑦⟩ должна находиться правее всех вершин бамбука. В случае
бамбука влево-вниз возможностей только две: 𝑣 становится либо правым ребенком корня
бамбука, либо корнем дерева, а бамбук – ее левым ребенком.
В случае вправо-вниз бамбук режется на части 𝐴 и 𝐵: у вершин 𝐴 приоритет > 𝑦, а в
𝐵 он < 𝑦. Тогда 𝑣 становится правым ребенком листа 𝐴 и корень 𝐵 становится левым
ребенком 𝑣. Получается шрам Гарри Поттера.
Наивный алгоритм построения дерева
В худшем случае 𝑇 (𝑛) = Θ(𝑛) + 𝑇 (𝑛 − 1) = Θ(𝑛2 ).
В среднем 𝒪(𝑛 log 𝑛), та же рекуррента, что в Qsort.
Декартово дерево за 𝒪(𝑛) по отсортированным 𝑥
Добавляем вершины по одной. У новой вершины самый большой 𝑥, она добавится в правую ветвь. Держим правую ветвь в стеке. Ищем позицию вставки снизу вверх.
while (!right_branch.empty() && new_node->y < right_branch.back()->y) {
new_node->l = right_branch.back();
right_branch.pop_back();
}
if (!right_branch.empty())
right_branch.back()->r = new_node;
right_branch.push_back(new_node);
𝒪(𝑛), по одному добавлению и удалению из стека на вершину.
(*) Проблема с равными ключами
Нужно просто писать одинаковые знаки и в GoDown, и в Split. Например, идти вправо
при t->x <= x в обоих функциях.
Получится, что каждый новый элемент мы как бы считаем больше старых с тем же значением. Так мы ввели строгий порядок на всех добавленных ключах, поэтому с высотой
всё хорошо.
Если же написать разные знаки, при постоянном добавлении одинаковых ключей дерево
выродится в бамбук при любых приоритетах. Например, если в GoDown при равенстве идти влево, а в Split вправо (то есть равные уходят в левое поддерево), то будет растущий
влево бамбук.
(*) Ключ 𝑎𝑖 , декартово дерево 𝑏𝑖 в поддереве
С декартовым деревом 𝑏𝑖 в поддереве есть проблемы – при прикреплении ребенка 𝑢 к 𝑣
пришлось бы сливать декартовы деревья 𝑢 и 𝑣. Это делается быстро, только если все 𝑏𝑖
в 𝑢 больше всех 𝑏𝑖 в 𝑣, что не всегда верно.
С отрезанием ребенка та же проблема.
То есть это возможно, но сложность операций Θ(𝑛).
(*) Не хранить 𝑦, не вызывать rand()
Заменим рандом на псевдорандомную функцию от адреса узла 𝑎. Например, (𝑎2 + 1) mod
109 + 7.
3. Персистентный поворот
3/11
Алгоритмы, весна 2020
Практика #11. BST, Treap, Персистентность.
return new Node(v->l->x, v->l->l, new Node(v->x, v->l->r, v->r))
4. Детская персистентность
Изменение за 𝒪(𝑛) – копировать массив полностью (copy on write).
Изменение за 𝒪(1) – поддерживать дерево версий (всего массива, а не ячеек отдельно!) Для
каждой версии хранить версию-отца и изменение относительно отца.
get(version, i) – пройти по дереву версий от version до корня, найти последнее изменение
𝑖-й ячейки массива.
5. Персистентная хеш-таблица
Online. Хеш-таблица – это массив. Умеем персистентный массив.
Но хеш-таблицу бессмысленно выражать через массив за 𝒪(log 𝑛), проще сразу взять обычное персистентное BST.
Offline: строим дерево версий, обходим его dfs.
Проходя по ребру-изменению, запоминаем старые значения измененных ячеек, вносим изменения.
Возвращаясь назад по ребру, возвращаем запомненные старые значения.
Время – рандомизированное 𝒪(1).
6. Максимальный покрывающий отрезок
Offline.
Сортируем события. Идем сканирующей прямой. Храним в set, упорядоченном по весу,
открытые отрезки.
Online.
Чтобы ответить про точку 𝑥, нужно знать max вес в момент 𝑥.
Между событиями открытий/закрытий max вес не меняется. Запомним, какой после каждого события.
По 𝑥 бинпоиском находим последнее событие с координатой 6 𝑥. Там ответ.
Отрезки не длиннее 𝐿.
Теперь scanline с BST, где ключ – длина, и хранится max вес в поддеревьях.
Чтобы сделать это online для точки 𝑥, нужно знать состояние BST в момент 𝑥.
Ну так давайте помнить состояние BST во все интересные моменты времени (начала и концы
отрезков).
Для этого реализуем BST персистентно.
По 𝑥 бинпоиском находим последнюю версию с координатой 6 𝑥. В этой версии ответ.
7. Запросы на плоскости
Offline. Обрабатываем запросы по увеличению 𝑖.
Поддерживаем BST с числами 𝑎[0..𝑖], знающее размеры поддеревьев. Для ответ на запрос
надо просто считать, сколько в дереве чисел 6 𝑥.
Online. Для каждого 𝑖 персистентно создаем свою версию дерева.
Предподсчет 𝒪(𝑛 log 𝑛) времени и памяти. Запрос 𝒪(log 𝑛).
8. (*) Антитест для persistent RBST
Все операции делаются за высоту дерева ⇒ чтобы получить время Θ(𝑛2 ), нужно дерево
высоты Θ(𝑛), т.е. размера 2Θ(𝑛) .
Такое дерево можно получить за Θ(𝑛) операций так: 𝑡1 = new node(x), 𝑡𝑖+1 = Merge(𝑡𝑖 , 𝑡𝑖 ).
4/11
Алгоритмы, весна 2020
Практика #11. BST, Treap, Персистентность.
Добиться размера 2Θ(𝑛) только операциями add и delete нельзя.
9. (*) Persistent List
Persistent List можно реализовать на Persistent RBST, оно умеет Merge.
За 𝑛 запросов размер может увеличиться до 2𝑛 , как мы видели в прошлой задаче.
Если знаем, что запросов 6 𝑁 , можно хранить лишь 𝑁 самых левых и 𝑁 самых правых
элементов, остальные все равно не успеем запросить.
Если не знаем 𝑁 заранее, то сначала предположим, что 𝑁 равно 1.
Как только придёт (𝑁 + 1)-й запрос, перестроим всю структуру и обработаем все запросы
заново в предположении, что запросов будет не более чем 2𝑁 .
Время работы 𝑘 операций: 𝑇 (𝑘) = 𝑇 ( 𝑘2 ) + 𝑘 log 𝑘 = 𝒪(𝑘 log 𝑘).
5/11
Алгоритмы, весна 2020
Практика #11. BST, Treap, Персистентность.
Домашнее задание
3.1. Обязательная часть
1. (2) STL база
Каждый работник характеризуется своим уникальным id (autoincrement), именем, фамилией
и зарплатой. Придумайте структуру даннных для обработки следующих запросов. Разрешается использовать только стандартные STL-контейнеры.
• Добавить работника с данными именем, фамилией, зарплатой.
• Удалить работника с заданным id.
• По заданным имени/фамилии выведите сумму зарплат всех работников с таким именем/фамилией.
• Выведите id/фамилии/зарплаты всех работников с заданным именем.
• Выведите id/имена/зарплаты всех работников с заданной фамилией.
• Выведите id/имена/фамилии всех работников с заданной зарплатой.
• Выведите фамилии всех работников, зарплата которых находится в заданном диапазоне
[salary_min, salary_max]. Границы – параметры запроса.
Задание в том, чтобы выписать структуры, которыми вы хотите воспользоваться, а после
объяснить, как ими пользоваться для ответов на запросы.
2. (3) Персистентный СНМ
a) (1.5) Online. Не забудьте оценить память и время работы.
b) (1.5) Offline. Не забудьте оценить память и время работы.
Заметим, что СНМ можно реализовывать по-разному: через списки, через деревья с одной
или двумя эвристиками. Какой подход будет лучше в каждом пункте и почему?
3. (2) Корень RB дерева
В красно-черном дереве хранятся числа {1, 2, . . . , 𝑛}.
Какие из этих чисел могут находиться в корне дерева?
4. (2) B-дерево и работа руками
Данные с диска читаются блоками размера ровно 4096 байт. Размер ключа равен 8 байт.
Предложите конкретное 𝑘 для B*-дерева ([𝑘; 23 𝑘]) в предположении, что в дереве всегда будет
< 232 вершин. Оцените глубину полученного дерева.
5. (3) Маленькие числа на пути
Дано дерево. У каждого ребра есть вес 𝑤𝑒 и ценность cost𝑒 .
Запросы ⟨𝑣, 𝑢, 𝑊 ⟩: среди всех рёбер на пути 𝑣
𝑢 веса 6 𝑊 выбрать ребро max стоимости.
6/11
Алгоритмы, весна 2020
Практика #11. BST, Treap, Персистентность.
3.2. Дополнительная часть
1. (2) Персистентное RBST не всегда RBST
Докажите, что версия persistent RBST может не быть RBST (по определению RBST).
2. (3) Средняя максимальная глубина случайного дерева
Найдите матожидание максимальной глубины вершины в случайном дереве.
На частичный балл можно доказать асимптотическую оценку.
3. (3) Двухмерный treap
Придумайте cтруктуру данных для хранения точек на плоскости с операциями splitX,
mergeX, splitY, mergeY. Все операции должны работать за 𝑜(𝑛).
4. (3) List Ordered Maintenance
За 𝒪(1) online отвечать на два запроса: insertAfter(x, y), isBefore(x, y).
(вставить в список непосредственно после; спросить, кто в списке раньше)
7/11
Алгоритмы, весна 2020
Практика #11. BST, Treap, Персистентность.
Разбор домашнего задания
4.1. Обязательная часть
1. STL база
struct Person {
string name, surname;
int salary;
};
vector<Person> people;
unordered_map<string, long long> name2salary_sum, surname2salary_sum;
unordered_map<string, vector<int>> name2ids, surname2ids;
map<int, vector<int>> salary2ids;
// Пример функции
vector<string> getSurnames( int salary_min, int salary_max ) {
auto r = salary2ids.upper_bound(salary_max);
vector<string> result;
for (auto l = salary2ids.lower_bound(salary_min); l < r; l++)
for (int id : l->second)
result.push_back(persons[id].surname);
return result;
}
Удалять рабочих можно лениво. Добавим в Person поле bool isDeleted. Теперь при просмотре id в векторе, если видим уже удалённый, свопаем его с последним и уменьшаем
размер вектора.
Все unordered_map<string, *> можно улучшить, заменив string на uint64_t – полиномиальный хеш от строки. По сути внутри unordered_map-а всё равно считается некоторый
хеш, мы эти вычисления просто переносим в другое место. Плюс в том, что мы теперь не
заставляем unordered_map внутри хранить лишнюю копию string.
2. Персистентный СНМ
a) Online. СНМ – два массива, персистентный СНМ – два персистентных массива.
Персистентный массив = персистентное дерево.
Только ранговая эвристика, амортизация не работает персистентно. Итого 𝒪(log2 𝑛) на
запрос.
b) Offline: как обычно, строим дерево версий, обходим его dfs. 𝒪(log 𝑛) на запрос.
3. Корень RB дерева
Минимальный размер дерева черной высоты ℎ равен 2ℎ − 1 (все вершины черные), максимальный – 22ℎ+1 − 1 (цвета чередуются по уровням, разрешили красный корень). Итого:
2ℎ − 1 6 size(ℎ) 6 22ℎ+1 − 1
Добавляя красные вершины, получаем, что все промежуточные значения тоже достижимы.
Чтобы поставить в корень минимальное число, минимизируем размер левого поддерева.
Получаем (2ℎ − 1) + (22ℎ+1 − 1) + 1 > 𝑛, где l.size = 2ℎ −1, а в корне записано 2ℎ .
8/11
Алгоритмы, весна 2020
Практика #11. BST, Treap, Персистентность.
При этом ℎ – черная высота и левого, и правого поддеревьев. Осталось, зная 𝑛, найти ℎ.
Получаем квадратное уравнение на 2ℎ : 𝑥 + 2𝑥2 − 1 = 𝑛, решаем.
В корне может быть любое число из [𝑥, 𝑛 − 𝑥 + 1].
4. B-дерево и работа руками
В каждом узле B-дерева должно помещаться до 23 𝑘 ключей и 32 𝑘 + 1 поддеревьев.
Для ссылки на поддерево достаточно 4 байта ⇒ ключи и ссылки на поддеревья занимают
максимум 8 · 32 𝑘 + 4( 32 𝑘 + 1) = 18𝑘 + 4 байт.
4096 байт на блок ⇒ 𝑘 = 227.
5. Маленькие числа на пути
Центроидная декомпозиция. Хотим для каждого центроида 𝑎 и 𝑣 ∈ 𝐶(𝑎) узнавать max cost
на пути 𝑎
𝑣 среди весов 6 𝑥.
Можно это делать с помощью BST по ключу 𝑤 с дополнительным полем cost, хранящего
max cost поддеревьев.
Если при спуске по ребру в dfs добавлять ребро в BST персистентно, то всего 𝒪(log 𝑛) новых
вершин.
𝒪(𝑛 log2 𝑛) предподсчет, 𝒪(log 𝑛) запрос.
9/11
Алгоритмы, весна 2020
Практика #11. BST, Treap, Персистентность.
4.2. Дополнительная часть
1. Персистентное RBST не всегда RBST
Сделаем t = merge(a, a).
Пусть корнем t является 𝑖-й по порядку элемент левой копии a.
Тогда корнем t.l не может являться ни один из элементов правого поддерева, кроме 𝑖-го.
Получили неравномерное вероятностное распределение корней t.l (при условии фиксированного корня t).
Проблема в зависимости распределений корней поддеревьев при слиянии дерева с самим
собой. Проблема проявляется только при операциях типа merge(a, a).
2. Средняя максимальная глубина случайного дерева
Подробно можно прочесть здесь.
Задача: оценить матожидание случайной величины 𝑋𝑛 = 1 + max(𝑋𝑘−1 , 𝑋𝑛−𝑘 ).
Рассмотрим 𝑌𝑛 = 2𝑋𝑛 .
𝑛−1
∑︀
∑︀
∑︀
𝐸[𝑌𝑘 ].
𝐸[𝑌𝑛 ] = 𝑛𝑘=1 𝑛1 · 2𝐸[max(𝑌𝑘−1 , 𝑌𝑛−𝑘 )] 6 𝑛2 · 𝑛𝑘=1 𝐸[𝑌𝑘−1 + 𝑌𝑛−𝑘 )] = 𝑛4
𝑘=0
Можно по индукции доказать, что последняя величина есть 𝒪(𝑛3 ).
Именно 𝑛3 для того, чтобы интегрированием сократить 4 перед суммой.
По неравенству Йенсена 2𝐸[𝑋𝑛 ] 6 𝐸[2𝑋𝑛 ] = 𝐸[𝑌𝑛 ] 6 𝒪(𝑛3 ) ⇒ 𝐸[𝑋𝑛 ] 6 3 log2 𝑛 + 𝒪(1).
Замена 𝑋 на 2𝑋 – стандартный прием, позволяющий успешно заменять max на сумму.
3. Двухмерный treap
Структура: корень – точка с min приоритетом 𝑧. Относительно неё все точки делятся на 4
четверти, 4 поддерева (rd, ru, ld, lu).
Чтобы не было проблем с одинаковыми 𝑥 и 𝑦, сортируем не по 𝑥 и 𝑦, а по ⟨𝑥, 𝑦⟩ и ⟨𝑦, 𝑥⟩.
SplitX(t, x) {
if (!t) return {0, 0};
if (t->x < x) {
t->ru, u = SplitX(t->ru, x);
t->rd, d = SplitX(t->rd, x);
} else {
u, t->lu = SplitX(t->lu, x);
d, t->ld = SplitX(t->ld, x);
}
return {t, MergeY(d, u)};
}
MergeX(l, r) {
if (!l || !r) return l ? l : r;
if (l->z < r->z) {
rd, ru = SplitY(r, l->y);
l->rd = MergeX(l->rd, rd);
l->ru = MergeX(l->ru, ru);
return l;
} else {
10/11
Алгоритмы, весна 2020
Практика #11. BST, Treap, Персистентность.
ld, lu = SplitY(l, r->y);
r->ld = MergeX(ld, r->ld);
r->lu = MergeX(lu, r->lu);
return r;
}
}
Почему это работает быстро? Сложно объяснить, интуиция такая:
𝑇 (𝑛) 6 3𝑇 ( 𝑛4 ) ⇒ 𝑇 (𝑛) = 𝒪(𝑛log4 3 ) = 𝒪(𝑛0.8 ).
4. List Ordered Maintenance
MIT.Lection8
11/11
Скачать