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

Автор FairyWinx, 23 месяца назад, По-русски

Дана следующая задача: дан массив целых чисел $$$a_1, \ldots, a_n$$$, а также $$$q$$$ запросов одного из двух видов:

  1. $$$a_i += x$$$, где $$$l \leq i \leq r$$$.
  2. узнать количество индексов $$$i$$$, где $$$l \leq i \leq r$$$ и $$$a_j < a_i$$$, для всех $$$j:$$$ $$$l \leq j < i$$$. (Или по-человчески — количество префиксных максимумов на отрезке)

Очень хочется это решать быстрее, чем за $$$O(sqrt(n))$$$ на запрос.

Рассмотрим более легкий вариант, когда у нас нет запросов изменения. Тогда можно просто сделать Дерево отрезков, где в вершине хранится отсортированный список префиксных максимумов, если наш отрезок — это отрезок, соответствующий вершине дерева отрезков. Тогда мы должны просто сделать алгоритм, анологичный Merge Sort Tree, только мы должны в запросе вершины, на которые мы разбиваем разбиваем запрос, рассматривать слева направо и хранить максимум среди предыдущих элементов на подходящем отрезке, и к ответу прибавлять количество элементов в вершине, больших максимума до этого, а затем обновить максимум.

Это работает за $$$O(log(n)^2)$$$ на запрос, а также $$$O(nlog(n))$$$ памяти и времени на построение.


Заметим, что это можно переделать, чтобы были запросы изменения. Тогда нам нужно только научиться объединять такие списки и прибавлять к ним значение. А для этого подходит, например, персистентное декартво дерево. Тогда мы в вершине будем хранить персистентное декартово дерево из всех префиксных максимумов. Тогда при обновлении, если отрезок вершины лежит полностью внутри запроса, то просто добавим в наше декартово дерево нужное значение (ну и понятное дело, пропушим вниз это значение). А для объединения списков нужно просто скопировать значения из левого поддерева, а также выбрать нужный суффикс из правого поддерева и его также скопировать, а затем объеденить эти деревья.

Это уже работает за $$$O(log(n)^2)$$$ на запрос, но в этом случае будет построение за $$$O(nlog(n)^2)$$$, а, что самое главное, памяти будет $$$O((n + q)log(n)^2)$$$ суммарно, что пихать, конечно же, не очень хочется.


Теперь нормальное решение за $$$O(n)$$$ памяти и $$$O(log(n)^2)$$$ на запрос.

Пусть есть самое обычное дерево отрезок (У вершины $$$v$$$ есть левое поддерево — это $$$v.l$$$, а правое — это $$$v.r$$$), тогда пусть, мы для каждой вершины $$$v$$$ научились считать две величины $$$cnt_v$$$ — количество префиксных максимумов, если наш отрезок — это отрезок соответствующий вершине $$$v$$$, и $$$max_v$$$ — это максимум на том же отрезке.

Тогда реализуем функцию $$$f(v, x)$$$, которая будет считать количество префиксных максимумов строго больших $$$x$$$ на отреке вершины $$$v$$$.

У нас есть всего три случая:

  1. Если $$$v$$$ — это лист, то все просто, нужно сравнить значение у этого элемента с $$$x$$$.
  2. Если $$$x \geq max_{v.l}$$$, тогда заметим, что в левом поддереве нет точно подходящих элементов (так как они меньше или равны $$$x$$$), тогда нам интересно только правое поддерево, то есть $$$f(v, x) = f(v.r, x)$$$.
  3. Если $$$x < max_{v.l}$$$, тогда нам уже не интересно значение $$$f(v.r, x)$$$, так как элемент со значением $$$max_{v.l}$$$ точно будет среди префиксных максимумов, значит количество префиксных максимумов справа будет такое же, как и в случае отсутствия ограничения на $$$x$$$, а это количество мы уже знаем — это $$$cnt_v - cnt_{v.l}$$$, так как нам нужно количество максимумов справа, а это все, кроме тех, кто слева (логично, не правда-ли) (и это не $$$cnt_{v.l}$$$, так как в этом случае могут быть элементы меньше $$$max_{v.l}$$$). Значит $$$f(v, x) = f(v.l, x) + cnt_v - cnt_{v.l}$$$.

Эта функция работает за $$$O(log(n))$$$, так как каждый раз мы спускаемся ровно в одно поддерева, а глубина дерева отрезков как раз логарифм.

Понятно, что с этой функцией решать задачу легко, мы можем решать задачу, также, как и в первом случае, только нам нужно будет сделать не $$$lowerbound$$$ по списку, а просто запустить эту функцию. Построение и тому подобное также тривиально с этой функцией. (Все тонкости можно посмотреть в коде, вроде, он понятно написан)))

Реализация

Также спасибо Um_nik, который, собственно, придумал последнее решение в задаче, которая сводилась к этому и сделал ее значительно интереснее)))

А также примеры задач на эту технику 1, 2.

  • Проголосовать: нравится
  • +140
  • Проголосовать: не нравится

»
23 месяца назад, # |
  Проголосовать: нравится +10 Проголосовать: не нравится

You can use non-persistent treap in second approach.

When we calculate treap in segment tree node $$$v$$$ just move treap vertices from childs and remember "split point" of right child. Now when we want to go down just reconstruct childs from treap vertices of current segment tree node, go down and then, when we go up, recalculate current treap with moving treap vertices from childs once again.

It's $$$O(n)$$$ memory and $$$O(n \log^2 n)$$$ time. It is not that fast as your final solution because of constant factor, but it's allow you to find $$$k$$$-th element in this sequence for example.

And funny fact — we have update in $$$O(\log^2 n)$$$ time, but $$$k$$$-th element request in $$$O(\log n)$$$ time.