Блог пользователя pakhomovee

Автор pakhomovee, 4 года назад, По-русски

Кратко про алгоритм Мо

Задача 1
Задача 2

Рассмотрим задачу 1. Заметим, что её можно решить с помощью персистентных структур данных. Однако, поставим себе цель решить задачу 2. Да и с персистентностью бывают проблемы.

Алгоритм 1

Будем решать задачу offline. Отсортируем запросы по <$$$\frac{l}{k}$$$, r>, где k — некоторое число. Очевидно, что мы можем легко добавить/удалить одно число (для этого можно поддерживать отдельный массив B, что $$$B_{i}$$$ = количеству учтённых вхождений числа i). Разобьем запросы на блоки с одинаковым $$$\frac{l}{k}$$$. Очевидно, что таких блоков $$$O(\frac{n}{k})$$$. Пусть сейчас насчитан ответ для отрезка [L; R]. Будем обновлять ответ, сдвигая левую и правую границу, пока они не станут равны границам текущего запроса. Заметим, что для каждого блока левая граница пройдет $$$O(k)$$$ шагов для каждого запроса блока, а правая — $$$O(n)$$$, так как правая граница запросов в блоке неубывает. Тогда всего левая граница пройдет $$$O(q \cdot k)$$$ шагов, а правая — $$$O(\frac{n^2}{k})$$$. Приняв n = q, k = $$$\sqrt{n}$$$ получаем асимптотику $$$O((n + q)\cdot\sqrt{n})$$$.

Немного кода

У этого варианта алгоритма есть есть две проблемы. Во-первых, он достаточно медленный.

Подумайте над второй проблемой сами.

Проблема

Алгоритм 2

Отсортируем запросы таким же образом, как и в первом алгоритме.

Назовём блоком запросы с одинаковым $$$\frac{l}{k}$$$. Заметим, что блоков $$$O(k)$$$. Обработаем запросы длины $$$\leq$$$ k отдельно. Теперь в каждом блоке длина запросы $$$\geq$$$ k, значит, в него входит позиция $$$k \cdot (\lfloor \frac{l}{k} \rfloor + 1)$$$. Для каждого такого блока сохраним указатель на максимальную правую границу R, которую мы обработали для текущего блока (изначально R = $$$k \cdot (\lfloor \frac{l}{k} \rfloor + 1)$$$). Пусть текущий запрос (l; r), а максимальная правая граница — R. Тогда добавим элементы R + 1, R + 2, ..., r. Обновим R. Заметим, что не были учтены только элементы l, l + 1, ..., $$$k \cdot (\lfloor \frac{l}{k} \rfloor + 1) - 1$$$. Их $$$O(k)$$$. Таким образом, на обработку каждого запроса необходимо $$$O(k)$$$ времени, а на обработку блока — $$$O(n)$$$. Так как всего блоков $$$O(\frac{n}{k})$$$, итоговая асимптотика — $$$O(\frac{n^{2}}{k} + q \cdot k)$$$. При k = $$$\sqrt n$$$, итоговая асимптотика составит $$$O((n + q) \sqrt n)$$$.

Еще несколько замечаний

3D Mo

Настало время научиться решать вторую задачу.

Алгоритм 1.

Заметим, что нам ничего не мешает сделать корневую по запросам одновременно с алгоритмом Мо. Разобьем запросы на блоки размера k. Внутри каждого такого блока запросов запустим алгоритм Мо. Итоговая асимптотика — $$$O(\frac{q}{k} \cdot (k^{2} + \frac{n^{2}}{k}))$$$. При n = k алгоритм имеет асимптотику $$$O(nk + \frac{n^3}{k^{2}})$$$. При $$$k = n^{\frac{2}{3}}$$$ получаем $$$O(n^{\frac{5}{3}})$$$.

Псевдокод

Алгоритм 2.

Отсортируем запросы, кроме изменений по <l / k, r / k, i>. Тогда у каждого запроса каждая граница лежит в определенном блоке. Для каждой такой пары блоков научимся отвечать на запросы. Давайте перебирать запросы изменения в хронологическом порядке, то есть переберем для каждой пары блока все запросы изменения, будем двигать указатели как в первом алгоритме. Для этого можно поддерживать указатель на последний обработанный запрос. Тогда асимптотика работы алгоритма — $$$O(\frac{n}{k} \cdot \frac{n}{k} \cdot q + qk)$$$ (так как каждый раз каждый l или r может пройти $$$O(k)$$$ шагов. Пусть n = q. Тогда асимптотика — $$$O(\frac{n^{3}}{k^{2}} + qk)$$$. Взяв $$$k = n^{\frac{2}{3}}$$$ получаем $$$O(n^{\frac{5}{3}})$$$.

Псевдокод

Алгоритм 3.

Будем сортировать по <i / k, l / k, r>.

Назовем блоком запросы с одинаковым i / k и l / k. Тогда для такого блока можно перебрать все запросы изменения, которые не были обработаны и актуальны для этого блока. Указатель L сдвинется не более чем на k для каждого запроса, а R — на $$$O(n)$$$ для каждого блока. Итого (n = q) $$$O(\frac{n^{3}}{k^{2}} + nk)$$$. При k = $$$n^{\frac{2}{3}}$$$ получаем асимптотику $$$O(n^{\frac{5}{3}})$$$.

Еще несколько задач.

Задача 1
Задача 2
Задача 3
Задача 4
Задача 5
Задача 6
Задача 7
  • Проголосовать: нравится
  • +88
  • Проголосовать: не нравится

»
4 года назад, # |
Rev. 4   Проголосовать: нравится +10 Проголосовать: не нравится

Хочется отметить, что:

  • Минимум выражения $$$\frac{n^2}{k} + qk$$$ достигается не при $$$k = \sqrt{n}$$$, а при $$$k = \frac{n}{\sqrt{q}}$$$ и итоговая асимптотика получается $$$2n\sqrt{q}$$$.
  • Задача 2 решается за $$$O(\log^2{n})$$$ с помощью двумерной структуры данных. Расскажу два решения:
  1. Специфическое для этой задачи — задачу о количестве различных чисел на отрезке можно свести к задаче о подсчете на отрезке количества чисел со значениями в некотором диапазоне. Обозначим наши числа $$$a_1 \dots a_n$$$. Заведем массив $$$b$$$, где $$$b_i$$$ — первое $$$j$$$, левее $$$i$$$, такое что $$$a_j = a_i$$$. Тогда количеством различных чисел на отрезке $$$[L;R]$$$ в массиве $$$a$$$ будет количеством чисел на этом же отрезке в массиве $$$b$$$ со значениями меньшими $$$L$$$. А подсчет на отрезке количества чисел со значениями в некотором диапазоне и изменение одного элемента — стандартная задача на двумерную структуру данных.
  2. Более общую конструкцию. Рассмотрим для примера похожую задачу — найти количество чисел на отрезке, встречающихся ровно один раз. Понятно, что задача практически та же самая, но сводить её к запросу подсчета на отрезке количества чисел со значениями в некотором диапазоне, как предыдущую, я не умею. Посмотрим на offline решение этой задачи в версии, где элементы не изменяются. В момент времени, когда мы отвечаем на все запросы с правой границей, равной $$$i$$$, происходят два события изменения ДО — значение первой ячейки $$$j$$$, левее $$$i$$$, в которой $$$a_j = a_i$$$ уменьшается на $$$2$$$ ($$$1 \rightarrow -1$$$), а значением второй ячейки $$$k$$$, в которой $$$a_k = a_i$$$ увеличивается на $$$1$$$ ($$$-1 \rightarrow 0$$$). И ответом на запрос является сумма чисел в этом ДО на отрезке $$$[L;R]$$$. Сопоставим этим событиям точки с координатами $$$(j, i)$$$ и $$$(k, i)$$$ (первая координата — номер ячейки, вторая — время события) и значениями $$$-2$$$ и $$$+1$$$. Тогда значение ячейки $$$i$$$ в момент времени $$$t$$$ — это сумма значений всех точек на прямоугольнике $$$(i,0) \times (i,t)$$$. А значит ответ на запрос $$$[L;R]$$$ — это сумма на прямоугольнике $$$(L, 0) \times (R, R)$$$. Добавлять/удалять точки и искать сумму значений точек в прямоугольнике — тоже стандартная задача на двумерную структуру.
  • Задача про mex на отрезке с изменениями — это задача D с заочного тура Открытой олимпиады 2018-2019 года. Решение жюри там тоже за $$$O(\log^2{n})$$$ с помощью двумерной структуры, несколько похитрее вышеописанной конструкции, деталей уже не помню. Можно скачать архив с материалами и почитать код решения.

  • Задача про пересечение прямоугольников на пути в дереве решается независимо по каждой осей и требует нахождения максимума из левых концов и минимума из правых, т.е. просто поиска на пути минимального/максимального числа. Что делается, например, за $$$O(\log{n})$$$ простыми двоичными подъемами.

»
3 года назад, # |
  Проголосовать: нравится +33 Проголосовать: не нравится

You can find a lot of difficult sqrt problems (and some polylog problems) here prepared by ODT

»
3 года назад, # |
  Проголосовать: нравится -39 Проголосовать: не нравится

Well known in CNOI because of ODT lol

»
3 года назад, # |
  Проголосовать: нравится +5 Проголосовать: не нравится

I think a better way of thinking about Mo's algorithm that allows you to also optimize it in a lot of cases is to think about inserting a separator each $$$k$$$ positions and "bucketing" the queries depending on the first separator that is inside the query (not surprisingly, the l/k-th). For each bucket, you sort the queries increasingly by r and do sweep-line. This way, you only need a data structure that should be able to:

  • insert a value (in good complexity, say $$$O(1)$$$);
  • compute the answer (in relatively good complexity, say $$$O(\sqrt{N})$$$);
  • restore to a previous checkpoint [e.g. undo] (in relatively good complexity, say $$$O(\sqrt{N})$$$).

Sometimes not even that is needed for the problem. Nonetheless, you don't need to explicitly have the option to remove.

This is important in a lot of scenarios, for example the "most frequent element in range" problem, which you can solve in $$$O(Q \sqrt{N} \log{N})$$$ with the "standard" Mo's algorithm but rather trivially in $$$O(Q \sqrt{N})$$$ using this formulation. The only downside is that you have to write extra code to handle small queries (length smaller than $$$k$$$), but usually that is just glue code, as you can use the same data structure for that.