Алгоритмы и структуры данных

Prefix Sums

Префиксные суммы

актуальноТекущий рабочий стандарт

Направления: Основы · Алгоритмы и структуры данных

Один предварительный проход превращает сумму на любом отрезке в одно вычитание. Тот же приём вместе с хеш-таблицей находит подотрезок с заданной суммой за линейное время.

Обозначения
  • награда, полученная агентом на шаге
  • номер или количество: индекс шага, число соседей, кластеров или позиций
  • суммирование по всем перечисленным элементам
Сумма любого отрезка — разность двух накопленных сумм

Ключевые тезисы

  • Сумма на отрезке за O(1) после O(n) предподсчёта — базовый обмен памяти на время.
  • Префикс плюс ищут не сам отрезок, а его левую границу — и работают там, где ломается об отрицательные числа.
  • Обратная операция — разностный массив: много прибавлений на отрезках за константу каждое, значения восстанавливаются одним проходом.

Какую задачу решает

Сумма на отрезке считается проходом по нему — и это приемлемо ровно один раз. Когда запросов много, повторное сложение одних и тех же чисел становится главным расходом: наивное решение делает операций там, где хватает .

Приём тривиален и от этого особенно ценен: один проход запоминает сумму всего, что стоит до каждой позиции, и дальше сумма любого отрезка — разность двух чисел. В связке с хеш-таблицей та же идея отвечает на вопрос «сколько подотрезков имеют сумму k» — причём и при отрицательных значениях, где неприменимо.

Подробный разбор

10 подтем — объяснения, формулы, примеры и интерактивные графики.

Сумма на отрезке считается проходом по нему — это нормально ровно один раз. Когда запросов сто тысяч, а массив на миллион элементов, повторное сложение одних и тех же чисел становится главным расходом: операций там, где хватило бы миллиона.

Вторая форма той же боли — «сколько подотрезков имеют сумму k». Перебор пар границ даёт отрезков, и внутри каждого снова складываются те же элементы.

На практике

Запомнить сумму всего, что стоит до каждой позиции. Тогда сумма любого отрезка — разность двух запомненных чисел: общая часть сокращается.

Это обмен памяти на время в чистом виде: один проход и массив той же длины — и после этого каждый запрос стоит одну операцию вместо прохода.

индекс:    0   1   2   3   4
a:         3   5   2   7   4

P:     0   3   8  10  17  21      P[i] = a[0] + ... + a[i-1]
       0   1   2   3   4   5      индекс в P

сумма a[2..4] = P[5] - P[2] = 21 - 8 = 13
                              (2 + 7 + 4 = 13)
Массив, его префиксные суммы и запрос отрезка

Тот же массив префиксов отвечает и на вопрос о числе подотрезков с суммой . Условие переписывается как : правая граница у нас в руках, левую достаточно узнать среди уже встреченных.

ЭлементНакопленная суммаИщем сумму (текущая − 5)Встречалась раньшеНайдено отрезков
11−4нет0
23−2нет0
361да, после первого элемента1 — это отрезок 2 + 3

P = [0] * (len(a) + 1)
for i, x in enumerate(a):
    P[i + 1] = P[i] + x

sum_lr = P[r + 1] - P[l]        # сумма a[l..r] включительно

# сколько подотрезков имеют сумму k
seen = {0: 1}                   # пустой префикс
total = answer = 0
for x in a:
    total += x
    answer += seen.get(total - k, 0)
    seen[total] = seen.get(total, 0) + 1
Построение префиксов и два типовых запроса
Обозначения
  • награда, полученная агентом на шаге
  • номер или количество: индекс шага, число соседей, кластеров или позиций
  • суммирование по всем перечисленным элементам
Сумма отрезка как разность накопленных сумм
Двумерный случай: вычли две полосы, вернули дважды вычтенный угол
  • Время на построение и на запрос; вариант с хеш-таблицей — на всё.
  • Память на массив префиксов, на таблицу встреченных сумм.
  • Предусловие — массив не меняется между запросами.

Сумма телескопируется: , и всё, что стоит левее , сокращается. Никаких предположений о знаках элементов при этом не делается — именно поэтому приём работает там, где скользящее окно неверно.

Нулевой элемент — не украшение, а способ убрать частный случай: отрезок, начинающийся с нулевой позиции, считается той же формулой, что и все остальные. В варианте с хеш-таблицей ему соответствует запись seen[0] = 1, и её пропуск теряет ровно те отрезки, что начинаются с начала массива.

Сигнал в условииВариант приёма
Много запросов суммы, массив не меняетсяобычные префиксные суммы
«Сколько подотрезков с суммой k», есть отрицательныепрефикс плюс
«Сумма делится на k»префикс по остатку от деления
«Поровну нулей и единиц»замена нуля на −1 и поиск нулевой суммы
Много прибавлений на отрезках, ответ в концеразностный массив
Сумма по прямоугольнику двумерные префиксы
На практике

в чужом коде: sum(a[i:j]) внутри цикла. Это скрытый квадрат, и почти всегда он снимается префиксами правкой в три строки.

ВариантЧто накапливаемЗадача
Суммасумма и среднее на отрезке
Двумерныйсумма прямоугольника от углаинтегральное изображение, суммы по областям
По остаткуподотрезки с суммой, делящейся на k
По XORподотрезки с заданным XOR
Разностный массивнакапливаем изменения, а не значениямного прибавлений на отрезках
Два префикса сразусуммы и суммы квадратов на отрезке за константу
исходно:        0   0   0   0   0

отметки:        d[1] += 3,  d[4] -= 3
d:              0   3   0   0  -3

префиксы d:     0   3   3   3   0    <- итоговый массив
Разностный массив: прибавить 3 на отрезке [1..3]

  • Массив изменился. Одна правка обесценивает весь хвост префиксов; для изменяемых данных нужно дерево Фенвика.
  • Забытая запись `seen[0] = 1`. Теряются отрезки, начинающиеся с начала массива, — на большинстве тестов этого не видно.
  • Границы. Решите один раз, что означает P[i] — сумму до позиции или включая её, — и держитесь этого во всём коде.
  • Переполнение. Сумма миллиона больших чисел выходит за 32-битный тип задолго до конца массива.
  • Вещественные числа. Разность двух больших сумм теряет точность; для float лучше суммировать отрезок честно или применять компенсированное суммирование.

ПриёмКогда он лучшеВ чём разница
Скользящее окнозначения неотрицательны, нужно самое длинное или короткое памяти, но требует монотонности
Дерево Фенвикамассив меняется между запросами на запрос и на обновление
Хеш-таблицаусловие не про сумму, а про наличие элементадругой ключ, тот же принцип узнавания
Бинарный поискпрефиксы монотонны (все элементы неотрицательны)поиск границы отрезка по сумме

  1. Почему префиксные суммы работают с отрицательными числами, а — нет?
  2. Чему равно P[0] и что сломается, если его не заводить?
  3. Сумма делится на k: что класть в хеш-таблицу и почему остаток, а не сама сумма?
  4. Массив меняется после каждого десятого запроса — что выгоднее: перестраивать префиксы или взять дерево Фенвика?
  5. Как посчитать на произвольном отрезке за константу?

Чем заменяют

Видеолекции

Тайм-кода именно на эту тему в записях нет. Но глава «Алгоритмы и структуры данных» разобрана в курсе целиком — с той оговоркой, что место в записи придётся искать самому.

Связанные темы

Два указателя и окно

Two Pointers and Sliding Window90%

Два указателя: семейство приёмов · Алгоритмы и структуры данных

Зонтичная тема: что общего у четырёх приёмов с двумя индексами и как выбрать нужный. Сами приёмы разобраны каждый на своей странице.

Opposite-Direction Pointers90%

Встречные указатели · Алгоритмы и структуры данных

Два индекса сходятся с концов отсортированного массива. На каждом шаге отбрасывается конец, который заведомо не даст лучшего ответа, — и вместе с ним целая полоса пар.

Sliding Window90%

Скользящее окно · Алгоритмы и структуры данных

Окно между двумя границами: правая расширяет, левая чинит. Состояние не пересчитывается, а обновляется инкрементально — отсюда линейное время на задачах о подотрезках.

Fast and Slow Pointers90%

Быстрый и медленный указатель · Алгоритмы и структуры данных

Два указателя идут в одну сторону с разной скоростью. Если структура зациклена, быстрый догоняет медленного — цикл находится без журнала посещённых узлов.

Same-Direction Pointers90%

Два указателя в одну сторону · Алгоритмы и структуры данных

Слияние, пересечение и запись на месте: каждому источнику свой указатель, ни один не откатывается назад. Основа сортировки слиянием и merge join в базах данных.

Monotonic Stack and Deque90%

Монотонный стек и дек · Алгоритмы и структуры данных

Приём поверх стека: хранить только тех, для кого ответ ещё не найден. Даёт ближайший больший элемент для всех позиций сразу и максимум в скользящем окне — за один проход.

Проверить себя

Тема встречается в тесте по направлениям — ошибки приведут обратно на эту страницу.

Следующий шагПроверить себя: Алгоритмы и структуры данныхТема встречается в этом тесте 3 раза. Ошибка приведёт обратно на эту страницу — с объяснением, что именно не сошлось.Перейти →

Глава «Алгоритмы и структуры данных» последний раз правилась . Нашли ошибку — напишите.