← назад к разделу

Скользящее окно отлично работает, когда отрезок один и он ползёт. А если отрезки произвольные и их много: «сколько заработали с третьего по десятое», потом «с первого по пятое», и так тысячу раз? Каждый раз складывать заново — O(N) на запрос. Приём префиксных сумм отвечает на такой запрос за O(1) после одного подготовительного прохода.

Обязательно

Идея: нарастающий итог

Заведём массив, в котором на месте i лежит сумма всех элементов до i. Это и есть нарастающий итог — то же самое, что накопленная выручка с начала года.

Тогда сумма на отрезке — просто разность двух накопленных итогов: «сколько накопилось к концу отрезка» минус «сколько накопилось к его началу». Всё, что до начала отрезка, вычитается и не мешает.

выручка по дням 2 5 1 4 3 prefix[i] — сумма всех дней до i 0 2 7 8 12 15 0 1 2 3 4 5 prefix[4] − prefix[1] = 12 − 2 = 10

Нарастающий итог считается один раз слева направо. Дальше сумма дней с первого по третий — это не три сложения, а одно вычитание: 12 − 2 = 10.

живой пример

import java.util.Arrays;

public class Revenue {
    private final long[] prefix;

    Revenue(int[] daily) {
        prefix = new long[daily.length + 1];
        for (int i = 0; i < daily.length; i++) prefix[i + 1] = prefix[i] + daily[i];
    }

    long range(int l, int r) {
        return prefix[r + 1] - prefix[l];
    }

    public static void main(String[] args) {
        Revenue revenue = new Revenue(new int[]{2, 5, 1, 4, 3, 7, 2, 6});
        System.out.println("префиксы: " + Arrays.toString(revenue.prefix));
        System.out.println("дни 1..3: " + revenue.range(1, 3));
        System.out.println("дни 4..7: " + revenue.range(4, 7));
    }
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

живой пример

package main

import "fmt"

type Revenue struct{ prefix []int64 }

func NewRevenue(daily []int) *Revenue {
	prefix := make([]int64, len(daily)+1)
	for i, v := range daily {
		prefix[i+1] = prefix[i] + int64(v)
	}
	return &Revenue{prefix}
}

func (r *Revenue) Range(l, h int) int64 { return r.prefix[h+1] - r.prefix[l] }

func main() {
	revenue := NewRevenue([]int{2, 5, 1, 4, 3, 7, 2, 6})
	fmt.Println("префиксы:", revenue.prefix)
	fmt.Println("дни 1..3:", revenue.Range(1, 3))
	fmt.Println("дни 4..7:", revenue.Range(4, 7))
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

живой пример

class Revenue {
  constructor(daily) {
    this.prefix = new Array(daily.length + 1).fill(0);
    for (let i = 0; i < daily.length; i++) this.prefix[i + 1] = this.prefix[i] + daily[i];
  }
  range(l, r) { return this.prefix[r + 1] - this.prefix[l]; }
}

const revenue = new Revenue([2, 5, 1, 4, 3, 7, 2, 6]);
console.log("префиксы:", revenue.prefix);
console.log("дни 1..3:", revenue.range(1, 3));
console.log("дни 4..7:", revenue.range(4, 7));
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

живой пример

class Revenue:
    def __init__(self, daily: list[int]) -> None:
        self.prefix = [0] * (len(daily) + 1)
        for i, v in enumerate(daily):
            self.prefix[i + 1] = self.prefix[i] + v

    def range(self, l: int, r: int) -> int:
        return self.prefix[r + 1] - self.prefix[l]


revenue = Revenue([2, 5, 1, 4, 3, 7, 2, 6])
print("префиксы:", revenue.prefix)
print("дни 1..3:", revenue.range(1, 3))
print("дни 4..7:", revenue.range(4, 7))
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

Подготовка — O(N) один раз. Каждый запрос — O(1). При тысяче запросов на миллионе дней это разница между «мгновенно» и «полминуты».

Где приём начинает окупаться, считается в одну строку. Наивный ответ на Q запросов о отрезках средней длины L стоит Q·L шагов, префиксы стоят N на подготовку плюс Q на ответы. Значит, выигрыш есть, когда Q·L больше N + Q, грубо когда запросов больше, чем N / L. Для десятка запросов на миллионе дней подготовка не окупится, а для тысячи запросов о недельных отрезках уже да; когда запросов сравнимо с длиной массива, приём выигрывает всегда.

Почему массив на единицу длиннее

Обратите внимание: prefix длиннее исходного массива, а prefix[0] равен нулю. Это не случайность, а способ избавиться от особого случая.

Если бы префиксы совпадали с данными по длине (prefix[i] = сумма по i включительно), то формула была бы prefix[r] - prefix[l - 1], и при l == 0 мы обратились бы к prefix[-1]. Пришлось бы каждый раз писать проверку. Лишний нулевой элемент в начале — это «пустая сумма», и она делает формулу единообразной для всех отрезков.

Такой приём — добавить фиктивный элемент, чтобы убрать особый случай — встречается часто; в связных списках ту же роль играет фиктивная голова.

Не только суммы

Приём работает с любой операцией, у которой есть обратная.

Количество по признаку. Чтобы быстро отвечать «сколько отменённых заказов между днями l и r», строим префиксы не по выручке, а по единицам и нулям: единица, если заказ отменён. Сумма на отрезке превращается в количество.

Среднее. Сумма на отрезке, делённая на длину отрезка, — тоже O(1).

XOR и произведение. Обратная операция есть не только у сложения. Исключающее ИЛИ обратно само себе: префикс по XOR отвечает на «XOR отрезка» той же формулой prefix[r + 1] ^ prefix[l], и на этом строят проверки чётности и поиск числа, встречающегося нечётное число раз. Произведению обратно деление, но с двумя оговорками: нули (одна нулевая ячейка обнуляет все префиксы после себя, поэтому нули считают отдельно) и переполнение, которое у произведения наступает через десяток элементов, отчего его считают по модулю или в логарифмах.

А вот с минимумом или максимумом приём не работает: у них нет обратной операции, из «минимума на префиксе» нельзя вычесть лишнее. Для таких запросов нужны другие структуры: дерево отрезков, а если массив не меняется — разреженная таблица. А вот куча здесь не выручит, хотя название и напрашивается: она отдаёт минимум по всему набору целиком, а не по выбранному отрезку.

Обратная задача: много изменений, один запрос

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

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

diff[1] += 1 начало отрезка diff[6] -= 1 сразу за концом один проход нарастающий итог готовый массив значение каждого дня

Отрезок правят двумя отметками вместо всех его дней, а настоящие значения появляются только в конце, за один проход с нарастающим итогом.

живой пример

import java.util.Arrays;

public class Promotions {
    public static void main(String[] args) {
        int days = 8;
        int[][] ranges = {{0, 2}, {1, 5}, {4, 7}};
        int[] diff = new int[days + 1];
        for (int[] r : ranges) {
            diff[r[0]] += 1;
            diff[r[1] + 1] -= 1;
        }
        int[] result = new int[days];
        int running = 0;
        for (int i = 0; i < days; i++) {
            running += diff[i];
            result[i] = running;
        }
        System.out.println("акций в день: " + Arrays.toString(result));
    }
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

живой пример

package main

import "fmt"

func main() {
	days := 8
	ranges := [][2]int{{0, 2}, {1, 5}, {4, 7}}
	diff := make([]int, days+1)
	for _, r := range ranges {
		diff[r[0]]++
		diff[r[1]+1]--
	}
	result := make([]int, days)
	running := 0
	for i := 0; i < days; i++ {
		running += diff[i]
		result[i] = running
	}
	fmt.Println("акций в день:", result)
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

живой пример

const days = 8;
const ranges = [[0, 2], [1, 5], [4, 7]];
const diff = new Array(days + 1).fill(0);
for (const [from, to] of ranges) {
  diff[from] += 1;
  diff[to + 1] -= 1;
}
const result = [];
let running = 0;
for (let i = 0; i < days; i++) {
  running += diff[i];
  result.push(running);
}
console.log("акций в день:", result);
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

живой пример

days = 8
ranges = [(0, 2), (1, 5), (4, 7)]
diff = [0] * (days + 1)
for start, end in ranges:
    diff[start] += 1
    diff[end + 1] -= 1
result = []
running = 0
for i in range(days):
    running += diff[i]
    result.append(running)
print("акций в день:", result)
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

Каждое изменение — O(1) вместо O(длины отрезка), а в конце один проход с нарастающим итогом восстанавливает ответ. По сути это префиксные суммы, применённые наоборот.

Два измерения

Если данные лежат таблицей (например, продажи по дням и складам), тот же приём распространяется на прямоугольники. В prefix[i][j] кладут сумму всего прямоугольника от левого верхнего угла до клетки (i, j).

Сумма произвольного прямоугольника считается из четырёх чисел: берём большой прямоугольник, вычитаем полосу слева и полосу сверху — при этом угол вычли дважды, поэтому его возвращают обратно. В формулах, с тем же лишним нулевым рядом и нулевой колонкой, что и в одномерном случае: P[i+1][j+1] = a[i][j] + P[i][j+1] + P[i+1][j] − P[i][j] при подготовке, а сумма клеток от (r1, c1) до (r2, c2) включительно это P[r2+1][c2+1] − P[r1][c2+1] − P[r2+1][c1] + P[r1][c1].

P[r2+1][c2+1] − P[r1][c2+1] − P[r2+1][c1] + P[r1][c1] весь прямоугольник полоса сверху полоса слева угол вернули

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

живой пример

public class Prefix2D {
    public static void main(String[] args) {
        int[][] sales = {{2, 5, 1}, {4, 3, 7}, {2, 6, 1}};
        long[][] p = new long[4][4];
        for (int i = 0; i < 3; i++)
            for (int j = 0; j < 3; j++)
                p[i + 1][j + 1] = sales[i][j] + p[i][j + 1] + p[i + 1][j] - p[i][j];
        long rect = p[3][3] - p[1][3] - p[3][1] + p[1][1];
        System.out.println("сумма нижнего правого квадрата 2×2: " + rect);
    }
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

живой пример

package main

import "fmt"

func main() {
	sales := [][]int{{2, 5, 1}, {4, 3, 7}, {2, 6, 1}}
	p := make([][]int64, 4)
	for i := range p {
		p[i] = make([]int64, 4)
	}
	for i := 0; i < 3; i++ {
		for j := 0; j < 3; j++ {
			p[i+1][j+1] = int64(sales[i][j]) + p[i][j+1] + p[i+1][j] - p[i][j]
		}
	}
	rect := p[3][3] - p[1][3] - p[3][1] + p[1][1]
	fmt.Println("сумма нижнего правого квадрата 2×2:", rect)
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

живой пример

const sales = [[2, 5, 1], [4, 3, 7], [2, 6, 1]];
const p = Array.from({ length: 4 }, () => new Array(4).fill(0));
for (let i = 0; i < 3; i++)
  for (let j = 0; j < 3; j++)
    p[i + 1][j + 1] = sales[i][j] + p[i][j + 1] + p[i + 1][j] - p[i][j];
const rect = p[3][3] - p[1][3] - p[3][1] + p[1][1];
console.log("сумма нижнего правого квадрата 2×2:", rect);
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

живой пример

sales = [[2, 5, 1], [4, 3, 7], [2, 6, 1]]
p = [[0] * 4 for _ in range(4)]
for i in range(3):
    for j in range(3):
        p[i + 1][j + 1] = sales[i][j] + p[i][j + 1] + p[i + 1][j] - p[i][j]
rect = p[3][3] - p[1][3] - p[3][1] + p[1][1]
print("сумма нижнего правого квадрата 2×2:", rect)
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

Подготовка — O(N·M), любой запрос — O(1). Та же схема включения и исключения работает в трёх измерениях, только слагаемых становится восемь.

Чем платим

Приём не бесплатный, и стоит помнить о трёх вещах.

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

Только неизменяемые данные. Если элемент поменялся, все префиксы после него становятся неверными и требуют пересчёта за O(N). Префиксные суммы хороши там, где данные записали один раз и много раз читают.

Переполнение. Суммы растут, и 32-битное целое кончается быстрее, чем кажется: миллион дней по паре тысяч даёт два миллиарда — это впритык к пределу int в Java и int32 в Go (2 147 483 647), и достаточно чуть больших чисел, чтобы сумма стала отрицательной. Причём молча: ни Java, ни Go о переполнении не сообщают, они просто продолжают считать с испорченным значением; JavaScript вместо этого теряет точность выше 2⁵³, а в Python целые не переполняются вовсе. В примерах выше префиксы объявлены 64-битными именно поэтому.

Как это сделано в стандартной библиотеке

Готовой структуры «сумма на отрезке» в стандартной библиотеке нет — префиксный массив строят сами. Зато есть готовый способ его посчитать и выбор, в чём хранить.

Хранят в примитивном long[], и не из вредности. ArrayList<Long> держит не числа, а ссылки на объекты Long: каждое значение — отдельный объект в куче, памяти уходит в несколько раз больше, а проход по списку прыгает по памяти вместо того, чтобы идти подряд. Формально сложность та же, на деле разница в разы — а нарастающий итог как раз про один длинный последовательный проход.

Сам нарастающий итог библиотека посчитать умеет — это Arrays.parallelPrefix:

живой пример

import java.util.Arrays;

public class ParallelPrefix {
    public static void main(String[] args) {
        long[] daily = {2, 5, 1, 4};
        System.out.println("до вызова: " + Arrays.toString(daily));
        Arrays.parallelPrefix(daily, Long::sum);
        System.out.println("после:     " + Arrays.toString(daily));
    }
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

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

Грабля тут одна, зато обидная, и пример её показывает: метод работает на месте. После вызова исходных чисел у вас больше нет — в массиве лежит только накопленный итог, и дневную выручку уже не восстановить. Нулевого элемента слева он тоже не добавляет, а без него формула требует особого случая для отрезка с нулевой позиции. Массив под префиксы всё равно готовят руками: на единицу длиннее, с нулём в начале и копией данных внутри. И «parallel» в названии — не обещание выигрыша: разбиение работы по потокам окупается на сотнях тысяч элементов, на коротком массиве обычный цикл в одну строку быстрее.

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

Хранят в []int64 или []int, который на 64-битных платформах и есть int64. Упаковки в Go у чисел нет: срез хранит сами значения подряд, проход по нему идёт по памяти последовательно, и это ровно тот случай, где срез раскрывается лучше всего, потому что нарастающий итог — один длинный последовательный проход.

Сам нарастающий итог считают так:

живой пример

package main

import "fmt"

func main() {
	daily := []int64{2, 5, 1, 4}
	prefix := make([]int64, len(daily)+1)
	for i, v := range daily {
		prefix[i+1] = prefix[i] + v
	}
	fmt.Println("данные:  ", daily)
	fmt.Println("префиксы:", prefix)
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

Отдельный срез на единицу длиннее, с нулём в начале — и исходные данные целы. Соблазн сэкономить и накапливать на месте, daily[i] += daily[i-1], кончается тем же, чем в других языках: исходных чисел у вас больше нет, дневную выручку уже не восстановить, а формула для отрезка с нулевой позиции требует особого случая. Параллельного варианта в стандартной библиотеке нет; когда данных сотни миллионов, срез режут на куски по горутинам, считают итоги кусков, а потом сдвигают каждый кусок на сумму предыдущих, это два прохода вместо одного, и окупается такое редко.

Грабля тут одна: переполнение int64 в Go такое же молчаливое, как в Java, число просто переходит через край и становится отрицательным. Для денег в копейках 64 бит хватает с запасом, а вот int32 под суммы не берут никогда.

Готовой структуры «сумма на отрезке» в стандартной библиотеке нет — префиксный массив строят сами. Готовой функции для нарастающего итога тоже нет, но reduce с накоплением в массив пишется в одну строку.

Хранят в обычном массиве чисел: пока в нём лежат только числа, движок V8 держит их плотно, а для больших объёмов есть Float64Array. Упаковки в объекты, как в Java, здесь нет, зато есть другое: число в JavaScript всегда дробное, и целые точны только до 2⁵³, около девяти квадриллионов.

Сам нарастающий итог считают так:

живой пример

const daily = [2, 5, 1, 4];
const prefix = daily.reduce((acc, v) => (acc.push(acc.at(-1) + v), acc), [0]);
console.log("данные:  ", daily);
console.log("префиксы:", prefix);
console.log("2⁵³ + 1 =", 2 ** 53 + 1, "— точность кончилась");
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

Отдельный массив на единицу длиннее, с нулём в начале — и исходные данные целы. Соблазн накапливать на месте, daily[i] += daily[i - 1], кончается тем же, чем в других языках: исходных чисел у вас больше нет, а формула для отрезка с нулевой позиции требует особого случая. Обычный цикл for здесь быстрее reduce и читается не хуже; reduce берут, когда хочется одной строки.

Грабля тут одна, и она не похожа на переполнение в Java: сумма никогда не станет отрицательной, она потеряет точность. За 2⁵³ соседние целые числа перестают различаться, и 2 ** 53 + 1 печатается как 2 ** 53, без ошибки и без предупреждения. Для денег в копейках это далеко, а для сумм байтов или наносекунд уже нет; тогда берут BigInt и BigInt64Array, где арифметика точная, но заметно медленнее.

Готовой структуры «сумма на отрезке» в стандартной библиотеке нет — префиксный массив строят сами. Зато нарастающий итог библиотека считает сама, и делает это удобнее, чем аналог в Java.

Хранят в обычном list. Число в Python это отдельный объект, и список хранит ссылки на объекты, поэтому проход по нему прыгает по памяти, а не идёт подряд; на миллионах элементов это в разы медленнее, чем в Go, и тогда берут NumPy с np.cumsum, где числа лежат плотно. Но на тысячах и десятках тысяч элементов список справляется.

Сам нарастающий итог считает itertools.accumulate:

живой пример

from itertools import accumulate

daily = [2, 5, 1, 4]
prefix = list(accumulate(daily, initial=0))
print("данные:  ", daily)
print("префиксы:", prefix)
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

Параметр initial=0 добавляет тот самый нулевой элемент слева, так что формула для отрезка работает без особых случаев, а исходный список остаётся цел: accumulate ленивый и ничего не правит на месте. Вторым аргументом можно передать операцию, необязательно сложение, accumulate(daily, max) даёт нарастающий максимум.

Грабля тут одна, и она обратная к переполнению в Java: целые в Python не переполняются никогда, сумма растёт сколько нужно, и об этом легко забыть, перейдя на NumPy. np.cumsum над массивом int32 молча переходит через край и даёт отрицательную сумму, как Java; для сумм там всегда указывают dtype=np.int64.

Дополнительно: при первом чтении можно пропустить

Глубже: много запросов и много изменений: дерево Фенвикарасширенное

Выше сказано, что при частых изменениях и частых запросах нарастающий итог не спасает: каждое изменение элемента заставляет пересчитать все итоги после него, O(N). Ответ на «много того и другого» есть, и он умещается в двадцать строк: дерево Фенвика (binary indexed tree). Массив той же длины, где ячейка i хранит сумму отрезка, длина которого равна младшему единичному биту индекса; и обновление, и сумма префикса идут по индексам, прыгая на этот бит, за O(log N).

class Fenwick {
    private final long[] tree;           // индексы с 1
    Fenwick(int n) { tree = new long[n + 1]; }
    void add(int i, long delta) {        // a[i] += delta
        for (; i < tree.length; i += i & -i) tree[i] += delta;
    }
    long prefix(int i) {                 // сумма a[1..i]
        long s = 0;
        for (; i > 0; i -= i & -i) s += tree[i];
        return s;
    }
    long range(int l, int r) { return prefix(r) - prefix(l - 1); }
}
type Fenwick struct{ tree []int64 } // индексы с 1

func NewFenwick(n int) *Fenwick { return &Fenwick{make([]int64, n+1)} }

func (f *Fenwick) Add(i int, delta int64) { // a[i] += delta
	for ; i < len(f.tree); i += i & -i {
		f.tree[i] += delta
	}
}

func (f *Fenwick) Prefix(i int) int64 { // сумма a[1..i]
	var s int64
	for ; i > 0; i -= i & -i {
		s += f.tree[i]
	}
	return s
}

func (f *Fenwick) Range(l, r int) int64 { return f.Prefix(r) - f.Prefix(l-1) }
class Fenwick {
  constructor(n) { this.tree = new Array(n + 1).fill(0); }   // индексы с 1
  add(i, delta) {                                             // a[i] += delta
    for (; i < this.tree.length; i += i & -i) this.tree[i] += delta;
  }
  prefix(i) {                                                 // сумма a[1..i]
    let s = 0;
    for (; i > 0; i -= i & -i) s += this.tree[i];
    return s;
  }
  range(l, r) { return this.prefix(r) - this.prefix(l - 1); }
}
class Fenwick:
    def __init__(self, n: int) -> None:
        self.tree = [0] * (n + 1)          # индексы с 1

    def add(self, i: int, delta: int) -> None:      # a[i] += delta
        while i < len(self.tree):
            self.tree[i] += delta
            i += i & -i

    def prefix(self, i: int) -> int:                # сумма a[1..i]
        s = 0
        while i > 0:
            s += self.tree[i]
            i -= i & -i
        return s

    def range(self, l: int, r: int) -> int:
        return self.prefix(r) - self.prefix(l - 1)

i & -i выделяет младший единичный бит числа: для 12 (1100) это 4, для 7 (0111) это 1. Обновление поднимается по всем ячейкам, в чей отрезок попал элемент, запрос спускается, собирая непересекающиеся отрезки. Дерево отрезков делает то же для минимума, максимума и любой операции, у которой нет обратной, ценой вчетверо большей памяти и кода подлиннее; Фенвик берут, когда нужна сумма или счёт. Обе структуры отвечают и на «минимум на отрезке при изменениях», где нарастающий итог бессилен.

Коротко

  • Префиксные суммы — предподсчитанный нарастающий итог; сумма любого отрезка становится разностью двух чисел.
  • Подготовка O(N) один раз, каждый запрос O(1) — приём окупается, когда запросов много.
  • Массив префиксов делают на единицу длиннее с нулём в начале: так формула работает и для отрезка с нулевой позиции.
  • Работает с суммой и количеством по признаку; не работает с минимумом и максимумом — у них нет обратной операции.
  • Зеркальный вариант — разностный массив: массовые прибавления к отрезкам за O(1) каждое, итог собирается одним проходом в конце.
  • Ограничения: лишняя память, данные должны быть неизменяемыми, суммы легко переполняют int.
  • Много изменений и много запросов сразу закрывает дерево Фенвика: обновление и сумма префикса за O(log N); для минимума на отрезке дерево отрезков.
  • Приём окупается, когда запросов больше, чем N делить на среднюю длину отрезка; XOR обратно самому себе и работает той же формулой, произведение мешают нули и переполнение.
  • Двумерные префиксы: P[i+1][j+1] = a + сверху + слева − угол, сумма прямоугольника из четырёх чисел по включению-исключению.

Что почитать дальше