Problem
There are famous problem for given array of numbers and number $$$K$$$ find minimums for all segments of length $$$K$$$.
More general variant: for given array of size $$$n$$$ and $$$q$$$ queries $$$[l_i,r_i]$$$ need to find minimums on segmets at $$$l_i \leq l_{i+1}$$$ и $$$r_i \leq r_{i+1}$$$. Solution must be $$$O(n+q)$$$.
To solve this problem we need sliding window method, we numbers from segments is window. Needed data structure should be able to push_back
(add number to the end of window), pop_front
(remove first element) и get_min
— current minimum in window.
I was surprised to find that there are two types of such data structure. First is very popular and can be easily found in web. But type that I use whole life I can't find. I'll describe both of them.
Решение #1
Идея в том, чтобы хранить последовательность возрастающих минимумов. Первый элемент последовательности равен минимуму всего текущего окна, следом идёт минимум на суффиксе после этого элемента, и так далее. Например, для значений в окне [2,3,1,5,4,8,7,6,9]
это будут минимумы [1,4,6,9]
.
Когда происходит добавление элемента справа (incR, push_back), последовательность меняется следующим образом: все большие элементы убираются, сам элемент добавляется в конец.
Несколько примеров, для [1,4,6,9]
:
Добавление 5: [1,4,6,9] -> [1,4] -> [1,4,5]
Добавление 100: [1,4,6,9] -> [1,4,6,9,100]
Добавление 0: [1,4,6,9] -> [] -> [0]
При удалении элемента слева (incL, pop_front) нужно знать, был ли главный минимум первым элементом окна. По значению этого не понять, поэтому в последовательности помимо самих значений нужно хранить в паре с ними позиции этих значении. Пример выше превратится в последовательность [(1,2), (4,4), (6,7), (9,8)]
, если элементы нумеруются с нуля. Границы текущего окна нужно хранить явно в виде пары чисел $$$L$$$ и $$$R$$$. Итак, если первый элемент последовательности является первым элементом окна, то есть его позиция равна $$$L$$$, то его необходимо просто удалить, более ничего в последовательности не изменится.
Для реализации потребуется структура данных, позволяющая эффективно делать операции удаления слева и справа, а также добавления справа. Для этого можно использовать дек (std::deque в c++)
Решение #2
Представим окно в виде двух стеков: префикс окна находится в левом стеке $$$L$$$ так, что первый элемент окна вверху $$$L$$$, а суффикс окна в правом стеке $$$R$$$ так, что последний элемент окна вверху $$$R$$$.
[2,3,1,5,4,8,7,6,9]
<-------|--------->
L = [5,1,3,2]
R = [4,8,7,6,9]
Когда происходит добавление элемента справа (incR, push_back), элемент попадает в вершину правого стека
При удалении элемента слева (incL, pop_front), элемент уходит с вершины левого стека. Исключение составляет случай, когда в этот момент левый стек пуст. Тогда перед удалением происходит операция перебрасывания правого стека в левый: пока правый стек не закончится, его верхний элемент перемещается в левый стек. Например,
[4,3,1,2]
|------->
L = []
R = [4,3,1,2]
L = [2]
R = [4,3,1]
L = [2,1]
R = [4,3]
L = [2,1,3]
R = [4]
L = [2,1,3,4]
R = []
[4,3,2,1]
<-------|
Теперь, после перебрасывания, из левого стека можно удалять верхний элемент.
Чтобы отвечать в любой момент о текущем минимуме в окне нужно знать минимумы в стеках. Так как правый стек только увеличивается или очищается полностью, его минимум можно поддерживать в отдельной переменной $$$Rmin$$$.
В левом стеке вместо самих значений нужно хранить минимум среди значений от этого элемента до дна стека. Например для стека [5,7,3,4,2,1,8,6]
это будет стек [5,5,3,3,2,1,1,1]
Итого, минимум в окне будет равен либо верхнему элементу левого стека, либо $$$Rmin$$$.
Примечание
Оба решения работают за одинаковую ассимптотику. Второе считаю более простым для понимания как алгоритм, но в первом меньше кода. Про сравнительное время работы ничего не могу сказать.
Кстати, оба решения можно проапгрейдить до операции push_front
, хотя кажется, такая операция нигде не нужна.