Prefix Sum — это предварительно вычисленный массив, где prefix[i] хранит сумму элементов от начала до индекса i. Это позволяет вычислить сумму любого подмассива за O(1).
function buildPrefixSum(array $arr): array
{
$n = count($arr);
$prefix = array_fill(0, $n + 1, 0);
for ($i = 0; $i < $n; $i++) {
$prefix[$i + 1] = $prefix[$i] + $arr[$i];
}
return $prefix;
}
function rangeSum(array $prefix, int $left, int $right): int
{
// Sum of elements from left to right inclusive
return $prefix[$right + 1] - $prefix[$left];
}
// Example
$arr = [3, 1, 4, 1, 5, 9, 2, 6];
$prefix = buildPrefixSum($arr);
// prefix = [0, 3, 4, 8, 9, 14, 23, 25, 31]
echo rangeSum($prefix, 2, 5); // arr[2]+arr[3]+arr[4]+arr[5] = 4+1+5+9 = 19
// prefix[6] - prefix[2] = 23 - 4 = 19 ✓
func buildPrefixSum(arr []int) []int {
n := len(arr)
prefix := make([]int, n+1)
for i := 0; i < n; i++ {
prefix[i+1] = prefix[i] + arr[i]
}
return prefix
}
func rangeSum(prefix []int, left, right int) int {
// Sum of elements from left to right inclusive
return prefix[right+1] - prefix[left]
}
// Example
// arr := []int{3, 1, 4, 1, 5, 9, 2, 6}
// prefix := buildPrefixSum(arr)
// prefix = [0, 3, 4, 8, 9, 14, 23, 25, 31]
// rangeSum(prefix, 2, 5) // arr[2]+arr[3]+arr[4]+arr[5] = 4+1+5+9 = 19
// prefix[6] - prefix[2] = 23 - 4 = 19 ✓
public static int[] BuildPrefixSum(IReadOnlyList<int> arr)
{
int n = arr.Count;
int[] prefix = new int[n + 1];
for (int i = 0; i < n; i++)
{
prefix[i + 1] = prefix[i] + arr[i];
}
return prefix;
}
public static int RangeSum(int[] prefix, int left, int right)
{
// Sum of elements from left to right inclusive
return prefix[right + 1] - prefix[left];
}
// Example
int[] arr = [3, 1, 4, 1, 5, 9, 2, 6];
int[] prefix = BuildPrefixSum(arr);
// prefix = [0, 3, 4, 8, 9, 14, 23, 25, 31]
Console.WriteLine(RangeSum(prefix, 2, 5)); // arr[2]+arr[3]+arr[4]+arr[5] = 4+1+5+9 = 19
// prefix[6] - prefix[2] = 23 - 4 = 19 ✓
from itertools import accumulate
def build_prefix_sum(arr: list[int]) -> list[int]:
# accumulate with initial=0 gives exactly the n+1 prefix array
return list(accumulate(arr, initial=0))
def range_sum(prefix: list[int], left: int, right: int) -> int:
"""Sum of elements from left to right inclusive."""
return prefix[right + 1] - prefix[left]
# Example
arr = [3, 1, 4, 1, 5, 9, 2, 6]
prefix = build_prefix_sum(arr)
# prefix = [0, 3, 4, 8, 9, 14, 23, 25, 31]
print(range_sum(prefix, 2, 5)) # arr[2]+arr[3]+arr[4]+arr[5] = 4+1+5+9 = 19
# prefix[6] - prefix[2] = 23 - 4 = 19 ✓
**Построение:** O(n). **Запрос суммы:** O(1).
Сравни с наивным подходом:
Подход
Построение
Запрос суммы
Наивный
O(1)
O(n)
Prefix Sum
O(n)
O(1)
Prefix Sum выгоден, когда запросов много (больше n).
Задача 1: Subarray Sum Equals K
Условие: посчитать количество подмассивов с суммой равной k.
function subarraySum(array $nums, int $k): int
{
$count = 0;
$prefixSum = 0;
// Stores: prefixSum -> how many times it occurred
$prefixCount = [0 => 1];
foreach ($nums as $num) {
$prefixSum += $num;
// If prefixSum - k was seen before,
// then there is a subarray with sum k
if (isset($prefixCount[$prefixSum - $k])) {
$count += $prefixCount[$prefixSum - $k];
}
$prefixCount[$prefixSum] = ($prefixCount[$prefixSum] ?? 0) + 1;
}
return $count;
}
// nums = [1, 1, 1], k = 2
// prefixSum: 0, 1, 2, 3
// Step 1: ps=1, 1-2=-1 not found, count=0
// Step 2: ps=2, 2-2=0 found(1 time), count=1
// Step 3: ps=3, 3-2=1 found(1 time), count=2
// Answer: 2 (subarrays [1,1] and [1,1])
func subarraySum(nums []int, k int) int {
count := 0
prefixSum := 0
// Stores: prefixSum -> how many times it occurred
prefixCount := map[int]int{0: 1}
for _, num := range nums {
prefixSum += num
// If prefixSum - k was seen before,
// then there is a subarray with sum k
if c, ok := prefixCount[prefixSum-k]; ok {
count += c
}
prefixCount[prefixSum]++
}
return count
}
// nums = [1, 1, 1], k = 2
// prefixSum: 0, 1, 2, 3
// Step 1: ps=1, 1-2=-1 not found, count=0
// Step 2: ps=2, 2-2=0 found(1 time), count=1
// Step 3: ps=3, 3-2=1 found(1 time), count=2
// Answer: 2 (subarrays [1,1] and [1,1])
public static int SubarraySum(IReadOnlyList<int> nums, int k)
{
int count = 0;
int prefixSum = 0;
// Stores: prefixSum -> how many times it occurred
var prefixCount = new Dictionary<int, int> { [0] = 1 };
foreach (int num in nums)
{
prefixSum += num;
// If prefixSum - k was seen before,
// then there is a subarray with sum k
if (prefixCount.TryGetValue(prefixSum - k, out int seen))
{
count += seen;
}
prefixCount[prefixSum] = prefixCount.GetValueOrDefault(prefixSum) + 1;
}
return count;
}
// nums = [1, 1, 1], k = 2
// prefixSum: 0, 1, 2, 3
// Step 1: ps=1, 1-2=-1 not found, count=0
// Step 2: ps=2, 2-2=0 found(1 time), count=1
// Step 3: ps=3, 3-2=1 found(1 time), count=2
// Answer: 2 (subarrays [1,1] and [1,1])
from collections import defaultdict
def subarray_sum(nums: list[int], k: int) -> int:
count = 0
prefix_sum = 0
# Stores: prefix_sum -> how many times it occurred
prefix_count: defaultdict[int, int] = defaultdict(int)
prefix_count[0] = 1
for num in nums:
prefix_sum += num
# If prefix_sum - k was seen before,
# then there is a subarray with sum k
count += prefix_count[prefix_sum - k]
prefix_count[prefix_sum] += 1
return count
# nums = [1, 1, 1], k = 2
# prefix_sum: 0, 1, 2, 3
# Step 1: ps=1, 1-2=-1 not found, count=0
# Step 2: ps=2, 2-2=0 found(1 time), count=1
# Step 3: ps=3, 3-2=1 found(1 time), count=2
# Answer: 2 (subarrays [1,1] and [1,1])
func pivotIndex(nums []int) int {
total := 0
for _, v := range nums {
total += v
}
leftSum := 0
for i, v := range nums {
rightSum := total - leftSum - v
if leftSum == rightSum {
return i
}
leftSum += v
}
return -1
}
// [1, 7, 3, 6, 5, 6]
// total = 28
// i=0: left=0, right=28-0-1=27. 0!=27
// i=1: left=1, right=28-1-7=20. 1!=20
// i=2: left=8, right=28-8-3=17. 8!=17
// i=3: left=11, right=28-11-6=11. 11==11! -> return 3
public static int PivotIndex(IReadOnlyList<int> nums)
{
int total = nums.Sum();
int leftSum = 0;
for (int i = 0; i < nums.Count; i++)
{
int rightSum = total - leftSum - nums[i];
if (leftSum == rightSum)
{
return i;
}
leftSum += nums[i];
}
return -1;
}
// [1, 7, 3, 6, 5, 6]
// total = 28
// i=0: left=0, right=28-0-1=27. 0!=27
// i=1: left=1, right=28-1-7=20. 1!=20
// i=2: left=8, right=28-8-3=17. 8!=17
// i=3: left=11, right=28-11-6=11. 11==11! -> return 3
def pivot_index(nums: list[int]) -> int:
total = sum(nums)
left_sum = 0
for i, value in enumerate(nums):
right_sum = total - left_sum - value
if left_sum == right_sum:
return i
left_sum += value
return -1
# [1, 7, 3, 6, 5, 6]
# total = 28
# i=0: left=0, right=28-0-1=27. 0!=27
# i=1: left=1, right=28-1-7=20. 1!=20
# i=2: left=8, right=28-8-3=17. 8!=17
# i=3: left=11, right=28-11-6=11. 11==11! -> return 3
## Задача 3: Product of Array Except Self
Условие: для каждого элемента — произведение всех остальных (без деления).
func build2dPrefix(matrix [][]int) [][]int {
rows := len(matrix)
cols := len(matrix[0])
prefix := make([][]int, rows+1)
for i := 0; i <= rows; i++ {
prefix[i] = make([]int, cols+1)
}
for i := 1; i <= rows; i++ {
for j := 1; j <= cols; j++ {
prefix[i][j] = matrix[i-1][j-1] +
prefix[i-1][j] +
prefix[i][j-1] -
prefix[i-1][j-1]
}
}
return prefix
}
public static int[,] Build2dPrefix(int[,] matrix)
{
int rows = matrix.GetLength(0);
int cols = matrix.GetLength(1);
int[,] prefix = new int[rows + 1, cols + 1];
for (int i = 1; i <= rows; i++)
{
for (int j = 1; j <= cols; j++)
{
prefix[i, j] = matrix[i - 1, j - 1]
+ prefix[i - 1, j]
+ prefix[i, j - 1]
- prefix[i - 1, j - 1];
}
}
return prefix;
}
def build_2d_prefix(matrix: list[list[int]]) -> list[list[int]]:
rows = len(matrix)
cols = len(matrix[0])
prefix = [[0] * (cols + 1) for _ in range(rows + 1)]
for i in range(1, rows + 1):
for j in range(1, cols + 1):
prefix[i][j] = (
matrix[i - 1][j - 1]
+ prefix[i - 1][j]
+ prefix[i][j - 1]
- prefix[i - 1][j - 1]
)
return prefix
Визуализация формулы:
prefix[i][j] = matrix[i-1][j-1] + A + B - C
+-------+---+
| C | B |
+-------+---+
| A | X | <- matrix[i-1][j-1]
+-------+---+
A = prefix[i][j-1] (всё левее)
B = prefix[i-1][j] (всё выше)
C = prefix[i-1][j-1] (пересечение, вычитаем чтобы не считать дважды)
Запрос суммы прямоугольника
function rangeSum2d(array $prefix, int $r1, int $c1, int $r2, int $c2): int
{
// Sum in rectangle (r1,c1) - (r2,c2) inclusive
return $prefix[$r2 + 1][$c2 + 1]
- $prefix[$r1][$c2 + 1]
- $prefix[$r2 + 1][$c1]
+ $prefix[$r1][$c1];
}
func rangeSum2d(prefix [][]int, r1, c1, r2, c2 int) int {
// Sum in rectangle (r1,c1) - (r2,c2) inclusive
return prefix[r2+1][c2+1] -
prefix[r1][c2+1] -
prefix[r2+1][c1] +
prefix[r1][c1]
}
public static int RangeSum2d(int[,] prefix, int r1, int c1, int r2, int c2)
{
// Sum in rectangle (r1,c1) - (r2,c2) inclusive
return prefix[r2 + 1, c2 + 1]
- prefix[r1, c2 + 1]
- prefix[r2 + 1, c1]
+ prefix[r1, c1];
}
Нужна сумма в области X:
+-------+-------+---+
| D | C | |
+-------+-------+---+
| B | X | | <- (r1,c1) до (r2,c2)
+-------+-------+---+
| | | |
+-------+-------+---+
sum(X) = prefix[r2+1][c2+1] - C - B + D
= total - top - left + overlap
Полный пример
$matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
$prefix = build2dPrefix($matrix);
// Sum of rectangle (1,1) to (2,2): 5+6+8+9 = 28
echo rangeSum2d($prefix, 1, 1, 2, 2); // 28
// Sum of entire matrix (0,0) to (2,2): 1+2+3+4+5+6+7+8+9 = 45
echo rangeSum2d($prefix, 0, 0, 2, 2); // 45
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
prefix := build2dPrefix(matrix)
// Sum of rectangle (1,1) to (2,2): 5+6+8+9 = 28
fmt.Println(rangeSum2d(prefix, 1, 1, 2, 2)) // 28
// Sum of entire matrix (0,0) to (2,2): 1+2+3+4+5+6+7+8+9 = 45
fmt.Println(rangeSum2d(prefix, 0, 0, 2, 2)) // 45
int[,] matrix =
{
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 },
};
int[,] prefix = Build2dPrefix(matrix);
// Sum of rectangle (1,1) to (2,2): 5+6+8+9 = 28
Console.WriteLine(RangeSum2d(prefix, 1, 1, 2, 2)); // 28
// Sum of entire matrix (0,0) to (2,2): 1+2+3+4+5+6+7+8+9 = 45
Console.WriteLine(RangeSum2d(prefix, 0, 0, 2, 2)); // 45
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
prefix = build_2d_prefix(matrix)
# Sum of rectangle (1,1) to (2,2): 5+6+8+9 = 28
print(range_sum_2d(prefix, 1, 1, 2, 2)) # 28
# Sum of entire matrix (0,0) to (2,2): 1+2+3+4+5+6+7+8+9 = 45
print(range_sum_2d(prefix, 0, 0, 2, 2)) # 45
## Задача 4: Подмассив с суммой, делящейся на k
function subarraysDivisibleByK(array $nums, int $k): int
{
$count = 0;
$prefixSum = 0;
$remainderCount = [0 => 1];
foreach ($nums as $num) {
$prefixSum += $num;
$remainder = $prefixSum % $k;
// Normalize for negative numbers
if ($remainder < 0) {
$remainder += $k;
}
if (isset($remainderCount[$remainder])) {
$count += $remainderCount[$remainder];
}
$remainderCount[$remainder] = ($remainderCount[$remainder] ?? 0) + 1;
}
return $count;
}
// If two prefix sums have the same remainder when divided by k,
// the subarray between them is divisible by k.
func subarraysDivisibleByK(nums []int, k int) int {
count := 0
prefixSum := 0
remainderCount := map[int]int{0: 1}
for _, num := range nums {
prefixSum += num
remainder := prefixSum % k
// Normalize for negative numbers
if remainder < 0 {
remainder += k
}
if c, ok := remainderCount[remainder]; ok {
count += c
}
remainderCount[remainder]++
}
return count
}
// If two prefix sums have the same remainder when divided by k,
// the subarray between them is divisible by k.
public static int SubarraysDivisibleByK(IReadOnlyList<int> nums, int k)
{
int count = 0;
int prefixSum = 0;
var remainderCount = new Dictionary<int, int> { [0] = 1 };
foreach (int num in nums)
{
prefixSum += num;
int remainder = prefixSum % k;
// Normalize for negative numbers
if (remainder < 0)
{
remainder += k;
}
if (remainderCount.TryGetValue(remainder, out int seen))
{
count += seen;
}
remainderCount[remainder] = remainderCount.GetValueOrDefault(remainder) + 1;
}
return count;
}
// If two prefix sums have the same remainder when divided by k,
// the subarray between them is divisible by k.
from collections import defaultdict
def subarrays_divisible_by_k(nums: list[int], k: int) -> int:
count = 0
prefix_sum = 0
remainder_count: defaultdict[int, int] = defaultdict(int)
remainder_count[0] = 1
for num in nums:
prefix_sum += num
# Python's % always returns a non-negative result for positive k,
# so no manual normalization is needed
remainder = prefix_sum % k
count += remainder_count[remainder]
remainder_count[remainder] += 1
return count
# If two prefix sums have the same remainder when divided by k,
# the subarray between them is divisible by k.
## Задача 5: Range Sum Query (Immutable)
class NumArray
{
private array $prefix;
public function __construct(array $nums)
{
$this->prefix = array_fill(0, count($nums) + 1, 0);
for ($i = 0; $i < count($nums); $i++) {
$this->prefix[$i + 1] = $this->prefix[$i] + $nums[$i];
}
}
public function sumRange(int $left, int $right): int
{
return $this->prefix[$right + 1] - $this->prefix[$left];
}
}
// Usage
$arr = new NumArray([1, 2, 3, 4, 5]);
$arr->sumRange(1, 3); // 2 + 3 + 4 = 9
$arr->sumRange(0, 4); // 1 + 2 + 3 + 4 + 5 = 15
Prefix Sum + HashMap = подсчёт подмассивов с заданным свойством
Prefix Sum + Binary Search = задачи на бинарный поиск по суммам
2D Prefix Sum = быстрые запросы на матрицах
Prefix XOR = задачи на XOR подмассивов
Запомни: Prefix Sum — это предварительная обработка. Потрать O(n) один раз, чтобы отвечать на запросы за O(1). Комбинация Prefix Sum + HashMap позволяет находить подмассивы с заданной суммой за O(n). Для матриц — 2D Prefix Sum с формулой включения-исключения.
Итоги
Prefix Sum позволяет отвечать на запросы суммы подмассива за O(1)
Формула: sum(l, r) = prefix[r+1] - prefix[l]
Prefix Sum + HashMap = подсчёт подмассивов с суммой k за O(n)
2D Prefix Sum = сумма прямоугольника за O(1) после O(n*m) построения
Трюк с остатками: одинаковый остаток от деления prefix sum = подмассив делится на k
Проверь себя
В задаче Product Except Self зачем нужны два прохода (left и right), если можно просто разделить общее произведение на текущий элемент?
В задаче Subarray Sum Equals K, зачем нужна HashMap с prefix sum, а не просто два вложенных цикла?
Какова сложность вычисления суммы прямоугольной области в матрице с использованием 2D Prefix Sum?
Для массива [1, 2, 3], k=3. Сколько подмассивов имеют сумму равную 3?