Если хочешь подтянуть свои знания, загляни на наш курс «Алгоритмы и структуры данных», на котором ты:
- углубишься в решение практических задач;
- узнаешь все про сложные алгоритмы, сортировки, сжатие данных и многое другое.
Ты также будешь на связи с преподавателем и другими студентами.
В итоге будешь браться за сложные проекты и повысишь чек за свою работу 🙂
Предположим, есть задача, данные в которой не меняются между запросами. Стоит лишь подвергнуть эти данные предварительной обработке, а уже затем с большой эффективностью (O(1)
) получать ответы на запросы.
Запрос о сумме
Обозначим sum(a, b)
как сумму элементов массива в диапазоне [a..b]
. Любой запрос о сумме можно эффективно обработать, если обработать входной массив и построить по нему массив префиксных сумм. Эта структура данных донельзя проста: каждый элемент массива равен сумме нескольких первых элементов исходного массива, т.е. значение k-го элемента равно sum(0, k)
. Обозначим массив как pref[n]
и применим динамическое программирование: pref[i + 1] = pref[i] + a[i]
.
После построения массива pref
любое значение sum(a, b)
можно вычислить по формулеsum(a, b) = pref[b] – pref[a]
. Поскольку при заполнении мы, фактически, используем 1-индексацию, a
рассматривается включительно, b
– нет. Поэтому важно не забыть уменьшить a
на единицу.
Ещё один важный момент: поскольку в C++ отсутствует длинная арифметика, необходимо использовать кольцо вычетов по модулю (как правило, 109 + 7). Код выглядит следующим образом:
Многомерный случай
Идея массива префиксных сумм обобщается и на многомерный случай. Предположим, мы имеем следующий массив:
Тогда сумму по оранжевому подмассиву можно посчитать как S(o+r+b+g) – S(r + b) – S(r + g) + S(r)
, где o, r, b, g – названия цветов orange, red, blue, green соответственно.
Код (в этот раз используем 0-индексацию):
Запрос о минимуме
Обозначим как min(a, b)
минимальный элемент массива в диапазоне [a..b]
. Рассмотрим способ, позволяющий находить минимум за O(1)
и O(n*logn)
. Этот метод называется алгоритмом разреженной таблицы или sparse table.
Идея заключается в том, чтобы заранее вычислить min(a, b)
для случаев, когда b – a + 1
(т.е. длина диапазона) является степенью двойки.
Количество предвычисляемых значений имеет порядок O(n*logn)
, поскольку существует O(logn)
длин диапазонов, являющихся степенью двойки. Эти значения можно эффективно вычислять по рекуррентной формуле
min(a, b) = min(min(a, a + w — 1), min(a + w, b))
Согласно этой формуле b – a + 1 – степень двойки, а w = (b — a + 1) / 2;
Вычисление всех значений занимает время O(n*logn
).
После этого любое значение min(a, b)
можно вычислить за O(1)
, взяв минимум два предвычисленных значения. Пусть k
– небольшая степень двойки, не превосходящая b — a + 1
. Значение min(a, b)
вычисляется по формуле
min(a, b) = min(min(a, a + k — 1), min(b — k + 1, b))
В этой формуле диапазон [a..b]
представлен как объединение двух диапазонов длины k
: [a .. a + k – 1] и [b – k + 1 .. b]
.
Иными словами, выбирается такое k
, которое является степенью двойки, но не превосходит по длине данный отрезок; диапазон разбивается на два различных по описанным формулам, в каждом из них ищется минимум, далее сравнивается. Алгоритм для максимума аналогичен алгоритму для минимума. Реализация для максимума:
Дерево Фенвика
Довольно простая и быстрая, но совсем не очевидная в плане идеи и понимания структура данных. Позволяет находить сумму на префиксе и изменять отдельные элементы за O(logn)
.
Структура представлена в виде массива f
, в котором f[i]
– сумма всех элементов от F[i]
до i
. Функция F(x)
связана с битовым представлением аргумента. Вкратце можно описать так: F(x)
заменяет группу единичных битов, находящихся в конце числа (младших) на нули. Если x заканчивается на нулевой бит, то F(x)
= x
. В битовых операциях F(x)
задаётся так: F(x) = x & (x + 1)
.
Нам понадобятся три функции: прибавление x
к элементу с индексом i
, получение суммы дерева от 0
до x
и получение суммы на [a..b]
.
Полный код дерева:
Задачи на sparse table
Задачи на дерево Фенвика
Комментарии