Пожалуйста, скажите мне эффективный алгоритм запроса Range Mex

у меня есть вопрос об этой проблеме.

вопрос

  • вам дается последовательность a[0], a 1],..., a[N-1], и набор серии (l[i], r[i]) (0 <= i <= Q - 1).
  • вычислить mex(a[l[i]], a[l[i] + 1],..., a[r[i] - 1]) для всех (l[i], r[i]).

функция mex является минимальным исключенным значением.
страница Википедии функции mex

вы можете предположить, что N <= 100000, Q <= 100000, and a[i] <= 100000.
O(N * (r[i] - l[i]) log(r[i] - l[i]) ) алгоритм очевиден, но это не так эффективный.

Мой Текущий Подход

#include <bits/stdc++.h>
using namespace std;
int N, Q, a[100009], l, r;
int main() {
    cin >> N >> Q;
    for(int i = 0; i < N; i++) cin >> a[i];
    for(int i = 0; i < Q; i++) {
        cin >> l >> r;
        set<int> s;
        for(int j = l; j < r; j++) s.insert(a[i]);
        int ret = 0;
        while(s.count(ret)) ret++;
        cout << ret << endl;
    }
    return 0;
}

Пожалуйста, скажите мне, как решить.

EDIT: O (N^2) медленно. Пожалуйста, расскажите мне более быстрый алгоритм.

2 ответов


здесь O((Q + N) log N) устранение:

  1. давайте переберем все позиции в массиве слева направо и сохраним последние вхождения для каждого значения в дереве сегментов (дерево сегментов должно хранить минимум в каждом узле).

  2. после добавления i-ое число, мы можем ответить на все запросы с правая граница равна i.

  3. ответ наименьшее значение x такие, что last[x] < l. Мы можем найти, спустившись по дереву сегментов, начиная с корня (если минимум в левом дочернем меньше l, мы идем туда. В противном случае мы идем к правильному ребенку).

вот и все.

вот какой-то псевдокод:

tree = new SegmentTree() // A minimum segment tree with -1 in each position
for i = 0 .. n - 1
    tree.put(a[i], i)
    for all queries with r = i
         ans for this query = tree.findFirstSmaller(l)

функция поиска меньшего размера выглядит так:

int findFirstSmaller(node, value)
    if node.isLeaf()
        return node.position()
    if node.leftChild.minimum < value
        return findFirstSmaller(node.leftChild, value)
    return findFirstSmaller(node.rightChild)

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


давайте обрабатывать как наши запросы, так и наши элементы слева направо, что-то вроде

for (int i = 0; i < N; ++i) {
    // 1. Add a[i] to all internal data structures
    // 2. Calculate answers for all queries q such that r[q] == i
}

здесь у нас O(N) итерации этого цикла, и мы хотим сделать как обновление структуры данных, так и запрос ответа на суффикс обрабатываемой части в o(N) времени.

давайте использовать массив contains[i][j] имеющего 1 если суффикс, начинающийся с позиции i содержит и 0 иначе. Учтите также, что у нас есть вычисленные суммы префиксов для каждого contains[i] отдельно. В этом случае мы могли бы ответить на каждый конкретный запрос суффикса в O(log N) время с помощью двоичного поиска: мы должны просто найти первый ноль в соответствующем contains[l[i]] массив, который является точно первой позицией, где частичная сумма равна индексу, а не индексу + 1. К сожалению, такие массивы будут принимать O(N^2) пространство и нужно O(N^2) время для каждого обновления.

Итак, мы должны оптимизировать. Построим 2-мерного дерево диапазон с операциями диапазона" sum query "и" assignment". В таком дереве мы можем запросить sum на любом под-прямоугольнике и присвоить одинаковое значение всем элементам любого под-прямоугольника в O(log^2 N) время, которое позволяет нам сделать обновление в O(log^2 N) время и запросы в O(log^3 N) время, давая сложность времени O(Nlog^2 N + Qlog^3 N). Сложность пространства O((N + Q)log^2 N) (и то же время для инициализации массивов) достигается с помощью lazy инициализация.

до: давайте пересмотрим, как запрос работает в деревьях диапазона с "sum". Для 1-мерного дерева (чтобы не делать этот ответ слишком длинным), это что-то вроде этого:

class Tree
{
    int l, r;           // begin and end of the interval represented by this vertex
    int sum;            // already calculated sum
    int overriden;      // value of override or special constant
    Tree *left, *right; // pointers to children
}
// returns sum of the part of this subtree that lies between from and to
int Tree::get(int from, int to)
{
    if (from > r || to < l) // no intersection
    {
        return 0;
    }
    if (l <= from && to <= r) // whole subtree lies within the interval
    {
        return sum;
    }
    if (overriden != NO_OVERRIDE) // should push override to children
    {
        left->overriden = right->overriden = overriden;
        left->sum = right->sum = (r - l) / 2 * overriden;
        overriden = NO_OVERRIDE;
    }
    return left->get(from, to) + right->get(from, to); // split to 2 queries
}

учитывая, что в нашем конкретном случае все запросы для дерева префикс запросы сумме from всегда равна 0, так, один из звонков детям всегда возвращает тривиальный ответ (0 или уже просчитанных sum). Итак, вместо того, чтобы делать O(log N) запросы для 2-мерного дерева в алгоритме бинарного поиска мы могли бы реализовать специальную процедуру поиска, очень похожую на эту get запрос. Сначала он должен получить значение левого ребенка (который принимает O(1) поскольку он уже вычислен), затем проверьте, находится ли узел, который мы ищем, слева (эта сумма меньше количества листьев в левом поддереве) и перейдите влево или вправо на основе этой информации. Этот подход позволит дополнительно оптимизировать запрос до O(log^2 N) времени (с теперь это одна операция дерева), дающая результирующую сложность O((N + Q)log^2 N)) как время, так и пространство.

не уверен, что это решение достаточно быстро, так как Q и N до 10^5, но это, вероятно, может быть дополнительно оптимизирован.