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

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

Как вы поняли, этот пост посвящён задаче F с прошедшего полуфинала ICPC CERC 2025

Эта задача не так проста, поэтому, чтобы додуматься до решения, необходимо знать две вещи:

  1. Структуру данных "Дерево отрезков"
  2. Решение задачи о нахождении наибольшей возрастающей подпоследовательности в массиве длины $$$n$$$ и элементами от $$$0$$$ до $$$N$$$ за $$$O(n \cdot log(N))$$$ времени. При желании Вы можете придумать его сами или посмотреть решение ниже
Нахождение наибольшей возрастающей подпоследовательности

Итак, теперь мы можем переходить к задаче F. Коротко повторю условия и немного их перефразирую для удобства: нам дан массив $$$a$$$ длины $$$n \leq 2 \cdot 10^5$$$ с элементами от $$$1$$$ до $$$10^6$$$. Изначально курсор стоит на индексе $$$0$$$, а также нам приходит $$$m \leq 5 \cdot 10^5$$$ запросов. За один запрос мы можем переместить курсор влево или вправо на 1, либо изменить значение массива $$$a$$$ в индексе, на который указывает курсор, на новое число $$$x$$$. После каждого запроса изменения числа нужно вывести длину наибольшей возрастающей подпоследовательности (Longest Increasing Subsequence, далее LIS) нового массива $$$a$$$

То, что за один запрос разрешено двигать курсор только на 1 позицию, наталкивает на мысль, что мы должны поддерживать какую-то информацию о префиксе перед курсором и о суффиксе, который идёт после курсора. Для удобства обозначим за $$$A$$$ префикс $$$a$$$ до элемента $$$a[cur]$$$ не включительно, а за $$$B$$$ суффикс $$$a$$$, начиная с элемента $$$a[cur + 1]$$$ включительно, где $$$cur$$$ — текущее значение курсора. То есть $$$a = A + a[cur] + B$$$

Ну раз уж мы знаем про прекрасное решение задачи о нахождении LIS, давайте хранить ровно ту же информацию в двух деревьях отрезков. Дерево отрезков для префикса будет хранить массив $$$sta$$$, где $$$sta[x]$$$ — это длина LIS в $$$A$$$, последний элемент которой равен $$$x$$$. Если таких подпоследовательностей не существует, $$$sta[x] = 0$$$. Аналогично, $$$stb$$$ — массив для суффикса, в котором $$$stb[y]$$$ — это длина LIS в $$$B$$$, первый элемент которой равен $$$y$$$

Вопрос 1: как обновлять деревья отрезков для sta и stb при перемещении курсора на 1 позицию?

Подсказка 1.1
Ответ 1.1
Подсказка 1.2
Ответ 1.2

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

Вопрос 2: как посчитать длину LIS, проходящей через $$$a[cur]$$$?

Подсказка 2
Ответ 2

Теперь перейдём к более сложной части, а именно к подсчёту LIS, не проходящих через $$$a[cur]$$$

Вопрос 3: как выражается эта длина в терминах $$$sta$$$ и $$$stb$$$?

Ответ 3

Теперь, раз мы понимаем как выражается ответ, мы его можем посчитать наивным образом за $$$O(N^2)$$$

Вопрос 4: как ускорить подсчёт ответа?

Подсказка 4.1
Подсказка 4.2
Ответ 4.2
Подсказка 4.3
Ответ 4.3
Подсказка 4.4
Ответ 4.4

Мы научились считать максимум на этом треугольнике за $$$O(N)$$$, но это всё равно очень долго. С другой стороны, эта таблица практически не меняется при изменении одного элемента $$$sta$$$ или $$$stb$$$

Вопрос 5: как пересчитывать максимум на треугольнике при изменении одного элемента $$$sta$$$ или $$$stb$$$?

Подсказка 5.1
Ответ 5.1
Подсказка 5.2
Ответ 5.2

Итого, мы за $$$O(log(N))$$$ умеем считать LIS, среди всех, кто проходит через $$$a[cur]$$$ и за столько же LIS среди всех, кто не проходит через $$$a[cur]$$$. Чтобы посчитать итоговый LIS, нужно просто взять максимум из этих двух значений

Итоговая сложность решения по времени $$$O(m \cdot log(N))$$$ — хватит с запасом при данных ограничениях

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

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

Можно и другой алгоритм для поиска LIS приспособить, так дерево отрезков не пригодится.

А именно, вспомним следующий алгоритм для поиска LIS: для каждого префикса насчитаем $$$\mathrm{dp}$$$, где $$$\mathrm{dp}_i$$$ — наименьший элемент, на который заканчивается возрастающая подпоследовательность длины $$$i$$$ (или $$$\infty$$$, если такой нет). Чтобы пересчитать $$$\mathrm{dp}$$$ для нового префикса, добавив в конец старого префикса $$$x$$$, надо записать на позиции dp.lower_bound(x) сам элемент $$$x$$$.

Для решения задачи будем поддерживать $$$\mathrm{dp\_pref}$$$ (для префикса, как выше) и симметричный $$$\mathrm{dp\_suf}$$$ (для суффикса, $$$\mathrm{dp\_suf}_i$$$ — наибольший элемент, на который начинается возрастающая подпоследовательность длины $$$i$$$, или $$$-\infty$$$). Когда курсор сдвигается вправо, $$$\mathrm{dp\_pref}$$$ пересчитывается бинпоиском, как выше, а в $$$\mathrm{dp\_suf}$$$ происходит откат (надо хранить для каждого из этих двух $$$\mathrm{dp}$$$ стек изменений). Когда влево, то наоборот.

Теперь научимся отвечать на запросы. Пусть $$$\mathrm{dp\_pref}_i \lt \mathrm{dp\_suf}_j$$$, тогда мы знаем, что ответ хотя бы $$$i+j$$$. Чтобы решить задачу, нужно уметь эффективно поддерживать это множество пар $$$(i, j)$$$. Будем поддерживать только углы: такие пары $$$(i, j)$$$, что dp_pref[i] < dp_suf[j] && dp_pref[i+1] >= dp_suf[j] && dp_pref[i] >= dp_suf[j+1]. Для каждого $$$i$$$ существует не более одного $$$j$$$, такого, что $$$(i, j)$$$ — угол, и для каждого $$$j$$$ — не более одного $$$i$$$. Значит, когда курсор сдвигается, надо обновить $$$\mathcal{O}(1)$$$ пар. А именно, если изменилось $$$\mathrm{dp\_pref}_i$$$, то сначала удаляем старые углы с первой координатой $$$i-1$$$ или $$$i$$$, а потом заново высчитываем бинпоиском углы с такой первой координатой, и если они есть, то добавляем. Тут надо быть достаточно аккуратным, так как нужна какая-то структура (подойдут два массива), которая по $$$i$$$ ищет углы $$$(i, j)$$$, по $$$j$$$ ищет те же углы, и при обновлении всё это обновляет.

Ну и наконец будем поддерживать например мультисет со всевозможными ответами (суммами $$$i + j$$$ для углов $$$(i, j)$$$, и на запрос выводить наибольший. Вот посылка