Лекция по корневой декомпозиции

Простейшая корневая на массиве

Задача 1

Разбиваем массив на корень кусочков. В каждом кусочке храним соответствующий массив-подотрезок и его сумму.

Задача 2

Дополнительно храним модификатор в каждом блоке. Проталкиваем когда нужно (например когда нужно поработать с нецелым блоком)

Как обычно, если модификатор простой (например как выше), то можно вообще не проталкивать.

Корневая на массиве, блок cо структурой данных внутри

Решение: поддерживаем для каждого отрезка обычный и сортированный массив.

Split-Rebuild корневая

Задача: Массив "физической памяти". Список "отрезков-указателей".

Остальные операции выражаем через split.

Например, чтобы вычислить sum(l, r), делаем split(l), split(r+1), после чего для каждого отрезочка внутри [l; r] считаем суммы на нём (например через префиксные суммы физической памяти).

Время работы: Невыгодно иметь много отрезков. Каждые sqrt(n) операций делаем Rebuild ⇒ O(n sqrt(n)).

Вариации:

Корневая декомпозиция по запросам

В предыдущих пунктах мы делили массив на корень кусочков длины корень. Это позволяло делать какие-то операции с массивом за корень. Давайте попробуем пойти в другую сторону и делить не массив, а запросы к нему.

Задача: Винни-Пух и бочки мёда

Дано n изначально пустых бочек мёда, в i-й из них помещается не более, чем bi мёда.

Происходит q операций вида:

Вывести для каждой бочки первый запрос, когда она заполнится.

Решение

Разгребаем запросы блоками по k=sqrt(q)

Если предположить, что никакой бочонок не переполняется, то не очень сложно c помощью сканлайна за O(k + n) найти итоговый объём каждого бочонка.

Если никакой бочонок не наполнился, то больше ничего не делаем.

Иначе для каждого бочонка, который только что заполнился (то есть который был не заполнен до блока из K запросов, но стал заполнен после) делаем следующее:

Оценим время работы:

Задача о Винни-Пухе: параллельный бинпоиск

Отвлечёмся немного в сторону от темы SQRT: предыдущую задачу можно решить с помощью параллельного бинпоиска.

Давайте попробуем для каждого бочонка сделать бинпоиск: с какого-то запрос абочонок заполнится, а до этого будет не заполнен.

Есть лишь один нюанс: нельзя совсем просто взять и вычислить предикат бинпоиска ok(mid) быстро.

Давайте сделаем небольшой трюк и будем делать все n бинпоисков одновременно. Состояние бинпоиска для i-го бочонка характеризуется числами <l, r, mid>, где l и r это текущие границы бинпоиска, а mid это место, где нужно узнавать истинность предиката бинпоиска

Итак, у нас есть n бочонков, q запросов, и для каждого бочонка позиция, в которой нужно узнать истинность предиката

Давайте сделаем сканлайн с деревом отрезком сверху вниз: для каждого элемента в ДО храним сколько там есть мёда (в промежуточных вершинах можно даже ничего не хранить), делаем массовую операцию "добавить прогрессию на отрезке". Когда нас просят вычислить предикат бинпоиска, спрашиваем сколько мёда в этом бочонке и сдвигаем бинпоиск или в одну сторону, или в другую

Оценим время работы. Нужно сделать log(q) фаз бинпоиска. Каждая из фаз работает за сканлайн с деревом отрезков, то есть за (n + q)log(n).

Итоговое время работы составляет O((n + q) log(n) log(q)).

Задача про set

Решение

Храним большом сортированном массиве и буферизируем новопришедшие числа отдельно.

count делает бинарный поиск в большом массиве и пробегается по всему малому, O(log(n) + sqrt(n)).

add либо просто добавляет в малый массив, либо сливает два массива на O(n log n)).

Для фазы из k+1 запросов: add k раз работает время O(1) и один раз O(n log(n)).

При выборе k = sqrt(n log(n)) получаем решение за O(sqrt(n log(n))) на операцию.

Можно перестраивать большой массив за O(n), если отсортировать малый отдельно и сделать std::merge. В таком случае оптимально k = sqrt(n) и решение за O(n sqrt(n)).

Dynamic Connectivity Offline

Запросы: Все запросы даны в offline.

Решение

Бьём запросы на блоки по корню.

Внутри блока не более корня рёбер меняют свой статус присутствия.

Сжимаем все стабильные компоненты связности, а дальше работаем в тупую за dfs.

В графе не более n вершин и 2k рёбер, обращаем внимание, что dfs работает за O(k), а не за O(n).

k := sqrt(n) ⇒ время работы O(n sqrt(n)).

Чтобы сделать dfs за O(k) можно использовать "быстрое очищение" массива used. Это можно сделать одним из двух способов:

Корневая на графе

Задача на графе.

Дан граф из n вершин и m рёбер. Происходят запросы:

Решение

Если бы все степени были маленькие, скажем меньше корня, то у нас есть очевидное решение: каждый раз перебираем всех соседей.

Def. Скажем, что вершина жирная, если у неё степень больше sqrt(E).

Значит нужно как-то научится решать задачу для жирных вершин. sumv deg(v) = 2E ⇒ жирных не более O(sqrt(E)).

Заранее насчитаем "базовый ответ" (с массивом/set) для всех жирных вершин.

Каждый раз, когда перекрашиваем вершину, пробегаем по всем её жирным соседям. Их не более корня, так как в принципе в графе их тоже не более корня (но нужно заранее сохранить жирный подсписок).

Корневая со строками

Задача про строки

Дан текст и некоторый набор строк-шаблонов.

Найти (например посчитать суммарное количество) всех совпадений. Текст ограничен как 105, суммарная длина шаблонов такая же.

Решение:

Суммарная длина шаблонов 105 ⇒ количество различных длин не более sqrt(2 * 105).

Если бы все шаблоны были бы одной длины, то мы бы решили задачу хешами.
Если разных, то сгруппируем по длине и переберём длину.

Задача min на отрезке

Дан массив из n элементов, нужно ответить на q запросов "узнать минимум на [l; r]".

Все запросы даны в offline, то есть на них можно ответить одновременно.

Обычно мы применяем Мо, когда функция от отрезка сложнее, чем минимум, но для простоты иллюстрации обсудим именно минимум.

Решение:

Если знать (multi)-set значений, которые есть на отрезке, то ответ легко вычиcлить

Если насчитать set для одного отрезка, то его пересчитать для сдвинутого отрезка (l++, l--, r++ или r--), за O(log n) на одну позицию.

Если правильно определить отсортировать все отрезки, то получается O(n sqrt(n) log(n)).

Отрезки выгодно сортировать по <l/k, r>, где k = sqrt(n) (предполагаем, что n≈q).

Оценим время работы:

Разгоняем МО!

Хотим сделать то же самое, что и выше, но быстрее, без лишнего логарифма, используя как внутреннюю структуру данных корневую.

Замечаем, что МО работает за O(n sqrt(n) * tсдвига + n * tзапроса).

Выгодно перебалансировать в сторону второго.

Например заведём корневую: будем хранить в ячейке количество вхождений числа (пусть числа лежат от [0; n]), а также для каждого блока размера sqrt(n) храним суммарное число вхождений в нём

Тогда tсдвига = O(1), а tзапроса = O(sqrt(n))

Итого целиком решение за O(n sqrt(n)).

Мо и обновления, трёхмерное Мо

Рассмотрим задачу на МО, но теперь разрешим запросы точечного обновления массива.

Тогда запрос чтения <l, r> можно описать как <t, l, r>, где t --- количество запросов модификации до этого чтения.

Раньше в МО состоянием являлся отрезок <l, r> и какая-то структура данных об этом отрезке.

Теперь скажем, что состояние это <t, l, r>, массив для текущего времени и структуру данных об этом отрезке в этот момент.

Раньше у нас было tсдвига одной из границ отрезка на ±1. Заметим, что теперь мы можем двигать на ±1 ещё и время.

Отсортируем запросы лексикографически по <t/k, l/k, r>, где k = n2/3 и обойдём в таком порядке.

Несложно доказать (аналогично с тем, как мы это сделали для обычного МО), что время работы будет O(n5/3).

На самом деле алгоритм МО это алгоритм обхода точек на плоскости за O(n sqrt(n)). Только что мы его обобщили до 3d и получили O(n5/3). Можно пойти дальше и обобщить до размерности k. Тогда получится асимптотика O(n2 - 1/k)



Дмитрий Саютин, 2018-2019