Занятие 15. Префиксные суммы и RMQ
Семинар открывает новый класс задач: массив неизменен, а вопросов к нему — миллионы. Наивный ответ на каждый вопрос — честный проход, $O(n)$ на запрос и $O(nq)$ всего. Идея занятия: инвестиция — один раз потратиться на подготовку, дальше отвечать почти бесплатно. Занятие 12 уже так делало: сортировка — инвестиция, бинпоиски — дивиденды. Сегодня строим четыре структуры предпосчёта. Замеры реальные (Apple M-серия, clang++ -O2).
Перед темой — первые 20 минут пары: разбор РК1 по обоим вариантам. Классические места потери баллов: невыписанный инвариант (минус половина задачи), «два цикла ⇒ n log n» вместо суммы прогрессии, амортизация «в среднем» без «по худшей серии», (l + r) / 2 без мысли о переполнении.
Префиксные суммы
$p[i]$ — сумма первых $i$ элементов: $p[0] = 0$, $p[i] = a[0] + \dots + a[i-1]$. Считается одним проходом, а сумма на полуинтервале $[l, r)$ — двумя операциями:
Почему работает: каждый элемент с индексом из $[l, r)$ входит в $p[r]$, но не в $p[l]$ — разность отменяет общий «хвост». Полуинтервалы — те же, что в занятии 12: длина равна $r - l$, и никаких ±1.
Замер: инвестиция и дивиденды
Массив $10^8$ чисел, $10^6$ запросов случайных отрезков:
Запрос стоит дешевле кэш-промаха: два чтения и вычитание. Средний наивный запрос — треть массива, ~33 млн сложений. Итог: полчаса против 94 миллисекунд; асимптотика $O(n + q)$ вместо $O(nq)$.
Три ловушки на ровном месте
- Переполнение. Массив int, но $p$ — уже суммы: $10^8$ элементов по 100 — это $10^{10}$, int лопнул (занятие 3). Префиксы храним в
long longвсегда. - Смешение конвенций. «$p[i]$ — сумма до $i$ включительно или нет?» У нас полуинтервалы: $p[i]$ — сумма строго до $i$, запрос $p[r] - p[l]$ для $[l, r)$. Выберите конвенцию один раз.
- $p[0] = 0$ — сентинель, как ±∞ в бинпоиске: запрос с $l = 0$ не требует отдельного if. Массив $p$ на единицу длиннее — цена отсутствия крайних случаев.
Проверка: sum(0, n) обязан равняться сумме всего массива, sum(i, i) — нулю. Две строчки теста ловят ошибки конвенций сразу.
Индикаторный приём
Вопрос «сколько на отрезке элементов со свойством X» — это сумма индикаторов, 0/1-массива:
Работает для чего угодно: чётные, отрицательные, «спуски» $a[i] > a[i+1]$… Один исходный массив — сколько угодно индикаторных префиксов рядом.
Разностный массив
Зеркальная задача: $k$ раз «прибавить $x$ на $[l, r)$», ответ нужен один раз в конце. Обновление отрезка — две точечные записи:
Итого $O(n + k)$ вместо $O(nk)$. Префиксы и разности — пара взаимно обратных операций, дискретные «интеграл» и «производная»:
| префиксные суммы | разностный массив | |
|---|---|---|
| быстрый запрос суммы | O(1) | — |
| быстрое обновление отрезка | — | O(1) |
| подготовка / финал | один проход | один проход |
| аналогия | интеграл | производная |
Выбор — по тому, чего много: запросов или обновлений. Много и того и другого вперемешку — сегодняшние структуры не тянут: нужны дерево Фенвика или дерево отрезков (второй семестр).
Двумерные префиксы
$p[i][j]$ — сумма прямоугольника $[0, i) \times [0, j)$. Строится включением-исключением: складываем «верх» и «лево», их общий угол вошёл дважды — вычитаем:
Запрос суммы в произвольном прямоугольнике $[l_1, r_1) \times [l_2, r_2)$ — четыре слагаемых, тем же принципом:
Построение $O(nm)$, запрос $O(1)$ при любом размере прямоугольника. Нулевая строка и столбец — сентинели; в формуле только границы $l$ и $r$, без −1: полуинтервалы спасают и в 2D. Проверка себя: sum2d(0, n, 0, m) — сумма всей таблицы.
RMQ: почему префиксы бессильны
Range Minimum Query — тот же формат: массив неизменен, миллион вопросов «каков минимум на $[l, r)$?». Попытка по аналогии — $pm[i] = \min$ первых $i$ элементов и ответ «$pm[r] \ominus pm[l]$» — разбивается о простой факт: вычитания для минимума не существует. Зная min первых 10 и min первых 7 элементов, минимум $a[7..10)$ не восстановить; узнав $\min(x, 5) = 5$, не вернуть $x$.
Разность $p[r] - p[l]$ работала, потому что у сложения есть обратная операция:
| операция | обратная | префиксы работают? |
|---|---|---|
| сложение | вычитание | да |
| XOR | сам XOR | да |
| умножение | деление* | да, но мешают нули |
| min / max | нет | нет |
Урок: инструмент определяется алгеброй операции. Для min нужен другой предпосчёт.
Sparse Table
Идея: заранее посчитать минимумы всех отрезков длины — степени двойки. $st[k][i]$ — минимум на $[i, i + 2^k)$:
Динамика: отрезок длины $2^k$ — это две половины длины $2^{k-1}$, уже посчитанные. Уровней $\log_2 n$, память и построение $O(n \log n)$.
Запрос за O(1): перекрытие безвредно
Для запроса $[l, r)$ берём $k = \lfloor \log_2(r - l) \rfloor$ и два перекрывающихся окна длины $2^k$ — от начала и до конца:
Перекрытие законно из-за идемпотентности: $\min(x, x) = x$ — элемент, учтённый дважды, минимума не меняет. Той же схемой живут max, gcd, побитовые and/or. А вот сумма перекрытия не прощает — для неё Sparse Table в один запрос не работает.
Замер
Массив $10^6$, $10^6$ запросов минимума:
Запрос — два чтения и один min: 4 нс, в сто раз быстрее бинпоиска из занятия 12 (там мешали кэш-промахи по 400 МБ данных). Цена — память ×20 от исходного массива: классический размен «память ↔ время».
Сводка инструментов
| инструмент | подготовка | операция | когда брать |
|---|---|---|---|
| префиксные суммы | $O(n)$ | сумма на отрезке за $O(1)$ | много запросов сумм / количеств |
| разностный массив | $O(n)$ | $+x$ на отрезке за $O(1)$ | много обновлений, ответ в конце |
| 2D префиксы | $O(nm)$ | сумма прямоугольника за $O(1)$ | таблицы, картинки, поля |
| Sparse Table | $O(n \log n)$ | min/max/gcd на отрезке за $O(1)$ | идемпотентные операции |
Общий каркас: инвестиция в подготовку → дешёвые запросы. Выбор определяет алгебра: есть обратная операция — префиксы; идемпотентность — Sparse Table. Все четыре — для неизменяемых данных; запросы вперемешку с обновлениями — тема второго семестра.
Задачи семинара
-
Нули на отрезке: массив и $q$ запросов «сколько нулей на $[l, r)$?» — индикаторный префикс, ответ за $O(1)$. Проверьте на запросах $(0, n)$ и $(i, i)$.
-
Полив газона: газон из $n$ клеток, $k$ поливов «отрезок $[l, r)$ получает +1». Найдите самую политую клетку за $O(n + k)$.
-
Отсортирован ли отрезок? $q$ запросов «верно ли, что $a[l..r)$ не убывает?» за $O(1)$ каждый.
Домашнее задание (сдача через Git)
- Суммы и количества: задача 1 семинара + запросы «сколько чётных на отрезке» — оба через префиксы, с тестами
sum(0, n)иsum(i, i). - Полив газона: задача 2 — разностный массив, $k$ обновлений и один проход.
- Прямоугольники: таблица $n \times m$ и $q$ запросов суммы в прямоугольнике; сверьте
sum2d(0, n, 0, m)с полной суммой. - Sparse Table: RMQ-минимум + стресс-тест против наивного скана на $10^5$ случайных запросов.
- * Отсортированность за O(1): задача 3 семинара; бонус — вторым запросом «максимум на отрезке» через тот же Sparse Table проверяйте, что отрезок не превышает порог.
В каждой задаче — комментарий: какой инструмент выбран и почему (по алгебре операции).
Следующее занятие — стандартная библиотека на практике: std::vector изнутри, std::chrono (наконец узаконим наши замеры), первый взгляд на std::thread, пространства имён и перегрузка операторов.