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

Автор CtrlAlt, 8 лет назад, По-русски

За последнее время у меня набралось определённое количество заметок на различные темы, с которыми до конца не ясно что делать. Скорее всего, для синих эта информация не слишком актуальна, для красных — давно очевидна. Из этих заметок вряд ли получится сделать статьи, а достаточно заинтересованный человек вполне может собрать всю приведённую в них информацию из интернета по частям самостоятельно. Тем не менее, я всё же решил сделать пост (или, может быть несколько) на Codeforces, в надежде, что эти сведения для кого-нибудь окажутся полезными.

Оригинальная задача KQUERY формулируется следующим образом. Дан массив размера , а также запросов вида — определить количество элементов, больших , на отрезке .

Сразу следует заметить, что из-за достаточно малого TL вердикт Accepted по оригинальной задаче получает только одно из приведённых ниже решений. Тем не менее, рассмотрение альтернативных подходов к задаче также не лишено смысла.

Online-версия задачи KQUERY — KQUERYO. Среди её особенностей — увеличенный TL и некорректные тесты (см. комментарии к задаче). Все показанные online-решения получают применительно к данной задаче вердикт Accepted.

Dynamic-версия задачи KQUERY — KQUERY2. В данном посте эта задача (пока) не рассматривается; её обсуждение можно найти здесь.

Решение #1: merge tree

Препроцессинг , ответ на запрос , online

Код решения

Создадим дерево отрезков, у которого в вершине, соответствующей подотрезку , хранится отсортированный вектор элементов массива (в иностранных источниках такое дерево отрезков иногда называют merge tree). Объединять отсортированные вектора можно при помощи алгоритма STL merge().

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

Видео об этом решении

Решение #1a: частичное каскадирование

Препроцессинг , ответ на запрос , online

Код решения

В предыдущем решении мы использовали двоичный поиск, чтобы в каждой вершине, подотрезок которой входит в отрезок запроса, находить индекс первого элемента, большего . Мы могли бы определять этот индекс за , если бы в каждой вершине была сохранена дополнительная информация.

Пусть — отсортированный вектор, хранящийся в вершине , и — отсортированные вектора, хранящиеся в её левом и правом потомках. Пусть в вершине также имеются массивы и , такие что — индекс первого элемента, большего или равного , в массиве , — аналогичный индекс в массиве . Тогда, зная индекс первого элемента, большего , в массиве вершины , мы за определяем индекс первого элемента, большего , в массивах вершин-потомков (это и ).

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

При обработке запроса достаточно выполнить бинпоиск в корневой вершине, чтобы найти индекс первого элемента, большего , а далее обновлять его значениями из и . Данную технику называют частичным каскадированием (fractional cascading), она позволяет снять лишний логарифм при обработке запросов.

Практика, однако, показывает, что несмотря на лучшую асимптотическую оценку, данное решение работает на 30-40% медленнее предыдущего, поэтому пользоваться им не стоит.

Решение #2: сканирующая прямая по значениям (ординатам)

Препроцессинг , ответ на запрос , offline

Код решения

Рассмотрим геометрическую интерпретацию исходной задачи. Если элементы массива представить на плоскости в виде точек , то ответом на запрос является количество точек, лежащих выше отрезка

Введём два вида событий: появление точки и появление запроса. Отсортируем все события в порядке убывания ординат (для точек это , для запросов — ). По абсциссам построим стандартное дерево отрезков для суммы, в котором будем отмечать появление точек.

При обработке появления точки присваиваем в дереве отрезков единицу её абсциссе . При обработке появления запроса определяем сумму на отрезке . Заметим, что если разные события имеют одинаковую ординату, в первую очередь должны обрабатываться события, связанные с запросами.

Решение #3: сканирующая прямая по индексам (абсциссам)

Препроцессинг , ответ на запрос , offline

Код решения

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

Введём три вида событий: появление точки, начало запроса, конец запроса. Отсортируем все события в порядке возрастания абсцисс (для точек это , для начал запросов — , для концов запросов — ). По ординатам построим стандартное дерево отрезков для суммы, в котором будем отмечать появление точек.

При обработке появления точки присваиваем в дереве отрезков единицу её ординате . При обработке начала запроса вычитаем из ответа сумму на отрезке , при обработке конца запроса добавляем к ответу сумму на отрезке . Если разные события имеют одинаковую абсциссу, сначала обрабатываются начала запросов, затем точки, затем концы запросов.

Решение #3a: неявное дерево отрезков

Препроцессинг , ответ на запрос , offline

Код решения

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

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

Решение #3b: персистентное дерево отрезков

Препроцессинг , ответ на запрос , online

Код решения

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

Для ответа на запрос требуется вычислить сумму на отрезке в версии и вычесть сумму на отрезке в версии .

Решение #4: sqrt-декомпозиция

Препроцессинг , ответ на запрос , online

Код решения

Разделим исходный массив на блоки размером (получится блоков). Отсортируем каждый из блоков.

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

При получаем классическую sqrt-декомпозицию, имеющую время обработки запроса . Можно попытаться улучшить это значение, подобрав размер так, чтобы минимизировать выражение . При таким значением будет . Тем не менее, фактическое увеличение производительности по сравнению с sqrt-декомпозицией составляет ~5%, что влечёт общую нецелесообразность подобной оптимизации.

Сравнение быстродействия решений

Сформируем массив размера , заполненный случайными числами из диапазона . Затем выполним случайных KQUERY-запросов к данному массиву. Для каждого значения произведём 10 тестов, в качестве итогового времени возьмём среднее.

Можно видеть, что наиболее быстрым из online-решений является использование дерева отрезков с отсортированными подмассивами в вершинах, имеющее асимптотику на запрос (!).

Как было упомянуто ранее, в оригинальной задаче KQUERY вердикт Accepted получает только offline-решение #2 (сканирующая прямая по убыванию ординат). Можно попробовать произвести (неасимптотические) оптимизации других решений: не использовать ООП-стиль, заменить std::vector на статические массивы и т. д. Описание оптимизаций, достойных внимания, приветствуется в комментариях.

Другие задачи, сводящиеся к KQUERY

Количество различных чисел на отрезке (DQUERY)

Существует простое сведение задачи DQUERY к задаче KQUERY за время .

Определим массив , такой что . Другими словами, -й элемент массива содержит индекс следующего справа элемента, равного (если — последнее появление элемента, то ).

Количество различных элементов на отрезке — это количество элементов, справа от которых на отрезке нет равных им, то есть количество таких , что . Таким образом, результат запроса DQUERY к массиву равен результату запроса KQUERY к массиву .

На spoj.com задачу DQUERY можно сдать с использованием любого из показанных выше решений, кроме самого медленного (#4, sqrt-декомпозиция).

Обсуждение задачи на Codeforces

K-я порядковая статистика на отрезке (MKTHNUM)

Используя бинарный поиск по ответу, любое online-решение задачи KQUERY можно преобразовать в решение задачи MKTHNUM, асимптотика которого будет в раз выше: из решений KQUERY с асимптотикой и получаются решения MKTHNUM с асимптотикой соответственно.

На spoj.com задачу MKTHNUM можно сдать с использованием любого из online-решений, кроме самого медленного (#4, sqrt-декомпозиция). В решении #3b нужно аккуратно учитывать отрицательные элементы массива.

Важно, что эта задача имеет online-решение с временем обработки запроса . Это решение (не рассматриваемое здесь подробно) получается из решения #3b, но вместо бинарного поиска по ответу используется параллельный спуск по деревьям отрезков версий и .

Обсуждение задачи на Codeforces

Решения указанных задач также приведены в лекции ЗКШ 2015 (автор Burunduk1).

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

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

Существует еще решение этой задачи, при помощи алгоритма МО, например, алгоритмом МО можно загнать в ТЛ задачу DQUERY: код

»
8 лет назад, # |
Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

Решение с корневой можно ускорить (АССИМПТОТИЧЕСКИ). Пусть мы разбили на A блоков. Тогда на запрос мы будем тратить O(N * logN / A + A). Баланс этих двух слагаемых — это когда они равны : N logN / A = A, то есть A = sqrt(NlogN). Ассимптотика улучшилась до O(N*sqrt(NlogN)).

Еще можно за O(N*sqrt(N)) алгоритмом МО (оффлайн, разновидность корневой), о котором речь в предыдущем комментарии. Постараюсь разъяснить идею подробнее : разобъем запросы на группы. Каждому запросу [l; r] будет соответствовать группа с номером l / sqrt(N). То есть это номер блока длины sqrt(N), в который попадет левая граница запроса. Для каждой группы решим задачу независимо: 1. Отсортируем запросы внутри группы по правой границе (не учитывая левые никак) 2. Будем идти двумя указателями l1 и r1 и поддерживать S — ответ внутри отрезка [l1;r1]. Изначально l1 и r1 установлены в самом начале блока. Пройдемся по всем запросам. r1 будем двигать "направо" на 1 позицию, пока не достигнем r[i] — правой границы i-го запроса в блоке. При переходе от отрезка [l1; r1] к [l1; r1 + 1] нужно к S добавить 1, если a[r1 + 1] > k, а иначе — оставить S прежним. Итак, теперь одна проблема : индекс r1 уже достиг позиции конца текущего запроса. А вот l1 стоит либо в начале (если текущий запрос — самый первый), либо в позиции l[i — 1] — левой границе предыдущего запроса. Будем двигать l1 до l[i] (неизвестно — направо или налево, l[i] не отсортированы внутри блока). Если l1 придется двигать налево, то будем проверять, что a[l1] > k и добавлять 1 к S, если ДА. А если l1 двиграется налево, то в случае a[l1] > k, нужно будет отнять от S единицу, т.к. a[l1] раньше было в нашем отрезке, а теперь уже нет.

Докажем ассимптотику : пусть в i-й группе находится cnt[i] запросов. Тогда Заметим, что указаьтель r1 двигается только направо, а значит суммарно он пройдет максимум n позиций. А вот указатель l1 при переходе к следующему запросу может в худшем случае "пробежать" весь блок длины sqrt(N) (если он был в начале блока, а левая граница след. запроса — в конце). То есть суммарно на i-ю группу мы потратили O(cnt[i] * sqrt(N) + N) в худшем случае. Просуммируем это по всем i = 0..sqrt(N). Первое слагаемое превратится в sqrt(N) * sum(cnt[i]) = sqrt(N) * N, т.к. всего запросов N. А второе слагаемое превратится N * sqrt(N), т.к. групп sqrt(N). Значит решение отработает O(N * sqrt(N)). На практике это очень быстрое решение с хорошей константой, в отличае от дерева отрезков, т.к. все операции быстрые и нет никакой рекурсии, и на самом деле "худшие случаи", о которых шла речь в доказательстве ассимптотики редко достигаются. Так что это решение может оказаться быстрее остальных

UPD : я алгоритмом МО решил немного другую задачу, где k = const для всех запросов. Чтобы превратить мое решение в решение исходной задачи, нужно хранить, например, дерево Фенвика (оно достаточно быстрое) на сумму и при добавлении a[i] в отрезок [l1;r1] прибавлять 1 в точке a[i] в дереве Фенвика, а при удалении — отнимать. Ответ на запрос — это кол-во чисел > k, то есть текущая сумма на отрезке [k; +inf]. Такое решение отработает за O(N * sqrt(NlogN)), если сделать блоки длиной sqrt(N / logN), но константа будет очень хорошая

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

О а еще можно сжимать координаты и тогда решение такое же как неявным деревом отрезков только ответ на запрос за log N.