Разреженная таблица (англ. sparse table) — структура данных, позволяющая отвечать на запросы минимума на отрезке за
Определение. Разреженная таблица — это следующий двумерный массив размера
По-русски: считаем минимумы на каждом отрезке длины
Такой массив можно посчитать за его размер, итерируясь либо по
Имея таком массив, мы можем для любого отрезка быстро посчитать минимум на нём. Заметим, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам.
Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов.
int a[maxn], lg[maxn], mn[maxn][logn];
int rmq(int l, int r) { // полуинтервал [l; r)
int t = lg[r-l];
return min(mn[l][t], mn[r - (1 << t)][t]);
}
// Это считается где-то в первых строчках main:
for (int l = 0; l < logn; l++)
for (int i = (1<<l); i < maxn; i++)
lg[i] = l;
for (int i = n-1; i >= 0; i--) {
mn[i][0] = a[i];
for (int l = 0; l < logn-1; l++)
mn[i][l+1] = min(mn[i][l], mn[i+(1<<l)][l]);
}
Для больших таблиц порядок итерирования и расположение данных в памяти сильно влияет на скорость построения — это связано с работой кэшей.
Упражнение. Какой из 4 вариантов итерирования и layout-а самый эффективный? (Подсказка: не тот, который приведен выше.)
Разреженная таблица является статической структурой данных, то есть её нельзя дёшево обновлять (но можно достраивать на ходу — см. задачу «Антиматерия» с РОИ-2017).
Разреженную таблицу часто применяют для решения задачи о наименьшем общем предке, так как её можно свести к RMQ.
Эту структуру тоже можно обобщить на большие размерности. Пусть мы хотим посчитать RMQ на подквадратах. Тогда вместо массива t[i][k]
у нас будет массив t[i][j][k]
, в котором вместо минимума на отрезах будет храниться минимум на квадратах тех же степеней двоек. Получение минимума на произвольном квадрате тогда уже распадется на четыре минимума на квадратах длины
В общем же случае от нас просят минимум на прямоугольниках
Разреженную таблицу можно применять не только для минимума или максимума. От операции требуется только ассоциативность (
Если операция не идемпотентна, то для нахождения её результата можно действовать так: возьмём самый длинный упирающийся в левую границу запроса отрезок, прибавим его к ответу, сдвинем указатель на его правый конец и будем так продолжать, пока не обработаем весь запрос целиком.
int sum(int l, int r) { // [l, r)
int res = 0;
for (int d = logn - 1; d >= 0; d--) {
if (l + (1 << d) < r) {
res += t[l][d];
l += (1 << d);
}
}
return res;
}
Это работает быстрее, чем, например, дерево отрезков, но тоже асимптотически за
Мы хотим иметь какую-то структуру, которая может считать функцию
Сделаем следующее: мысленно построим на массиве дерево отрезков и (уже не мысленно) для каждого его отрезка
Утверждение. Любой запрос
Доказательство. Возьмем самый высокий центральный элемент
Решать задачу мы так и будем: найдём нужный центральный элемент и сделаем два запроса от него.
Сложная часть — найти этот центральный элемент за константное время — станет чуть проще, если мы будем работать только с массивами длины степени двойки и, соответственно, полными деревьями отрезков. Массивы неподходящей длины дополним до ближайшей степени двойки специальным нейтральным элементом, зависящим от самой операции (например,
Будем хранить всю структуру (предпосчитанные значения на отрезках) в массиве t[logn][maxn]
, в котором первым параметром будет уровень в дереве отрезков (число
Для ответа на запрос нам достаточно найти только уровень нужного центрального элемента. Чтобы научиться делать это эффективно, нам понадобится немного поразмышлять о природе дерева отрезков.
Заметим, что любая вершина
Нам нужно найти уровень нужного центрального элемента — это то же самое, что и уровень наименьшего общего отрезка для элементов
Для примера, построим DST для умножения по составному модулю:
const int maxn = (1 << logn);
int a[maxn], lg[maxn], t[logn][maxn];
const int neutral = 1;
int f(int a, int b) {
return (a * b) % 1000;
}
void build(int l, int r, int level = logn - 1) {
int m = (l + r) / 2;
int cur = neutral;
for (int i = m + 1; i < r; i++) {
cur = f(cur, a[i]);
t[level][i] = cur;
}
cur = neutral;
for (int i = m; i >= l; i--) {
cur = f(cur, a[i]);
t[level][i] = cur;
}
if (r - l > 1) {
build(l, mid, level+1);
build(mid, r, level+1);
}
}
int rmq(int l, int r) { // [l, r)
int level = lg[l ^ r];
int res = t[level][l];
// и, если правый отрезок не пустой:
if (r & ((1 << lg[l ^ r]) - 1)))
res = f(res, t[level][r]);
return res;
}
TODO: скорее всего, тут есть баги.