The Discrete Fundamental Theorem of Calculus
Given an array $A[0 \dots N-1]$ over an abelian group (such as $(\mathbb{Z}, +)$ or bitvectors under $\oplus$), define the 1-indexed prefix sum array $P[0 \dots N]$ with sentinel $P[0] = 0$ and recurrence $P[i] = P[i-1] + A[i-1]$ for $1 \le i \le N$. Any contiguous range sum over the inclusive 0-indexed interval $[L, R]$ reduces to a single subtraction:
$$\sum_{k=L}^{R} A[k] = P[R + 1] - P[L]$$
Notice why the $N+1$ length with sentinel $P[0] = 0$ is mandatory in clean systems code: it eliminates the branch if (L == 0) on every query. Conversely, define the Difference Array (discrete derivative) $D[0 \dots N]$ where $D[0] = A[0]$ and $D[i] = A[i] - A[i-1]$ for $1 \le i < N$. The prefix sum of $D$ reconstructs $A$:
$$A[i] = \sum_{k=0}^{i} D[k]$$
Because every element $A[i]$ for $i \in [L, R]$ includes $D[L]$ in its prefix sum, adding a scalar $\Delta$ to the entire subarray $A[L \dots R]$ only modifies two adjacent boundaries in $D$: $D[L] \mathrel{+}= \Delta$ and $D[R+1] \mathrel{-}= \Delta$.
In two dimensions, given matrix $A[0 \dots R-1][0 \dots C-1]$, the 1-indexed 2D prefix sum $P[r][c]$ stores the sum of the subgrid $[0 \dots r-1] \times [0 \dots c-1]$. By the Principle of Inclusion-Exclusion:
$$P[r][c] = A[r-1][c-1] + P[r-1][c] + P[r][c-1] - P[r-1][c-1]$$
Querying any subrectangle $[r_1, r_2] \times [c_1, c_2]$ takes $O(1)$ time via four table lookups:
$$\text{Sum}(r_1, c_1, r_2, c_2) = P[r_2+1][c_2+1] - P[r_1][c_2+1] - P[r_2+1][c_1] + P[r_1][c_1]$$
Dually, adding $\Delta$ to every cell in $[r_1, r_2] \times [c_1, c_2]$ of a 2D difference array $D$ updates 4 corners in $O(1)$: $D[r_1][c_1] \mathrel{+}= \Delta$, $D[r_1][c_2+1] \mathrel{-}= \Delta$, $D[r_2+1][c_1] \mathrel{-}= \Delta$, and $D[r_2+1][c_2+1] \mathrel{+}= \Delta$.
Trade-off Matrix: Static vs. Batched vs. Dynamic Range Operations
| Structure | Precomputation | Range Sum Query $[L, R]$ | Range Add Update $[L, R] + \Delta$ | Point Query $A[i]$ | Ideal Workload Profile |
|---|---|---|---|---|---|
| Raw Array $A$ | $O(1)$ | $O(N)$ | $O(N)$ | $O(1)$ | Point reads and point writes only |
| 1D / 2D Prefix Sum $P$ | $O(N)$ / $O(RC)$ | $O(1)$ | $O(N)$ (rebuilds $P$) | $O(1)$ | Immutable array, millions of range queries |
| Difference Array $D$ | $O(N)$ / $O(RC)$ | $O(N)$ (after reconstruction) | $O(1)$ | $O(N)$ online / $O(1)$ offline | Batched offline range updates, then readout |
| Fenwick Tree (BIT) | $O(N)$ | $O(\log N)$ | $O(\log N)$ (with 2 BITs) | $O(\log N)$ | Interleaved online updates and queries |
Visualizing 2D Inclusion-Exclusion & Difference Array Boundaries
Prefix Sum + Hash Map Pattern for Subarray Sum Equals $K$
running_sum += A[j]"] --> B["Need P[j+1] - P[i] == K
iff P[i] == running_sum - K"] B --> C{"Is (running_sum - K)
in freq_map?"} C -- "Yes" --> D["count += freq_map[running_sum - K]"] C -- "No" --> E["freq_map[running_sum]++"] D --> E
At first glance, the recurrence $P[i] = P[i-1] + A[i]$ looks inherently serial due to its loop-carried dependency of distance 1. However, because integer addition is associative, modern CPUs and GPUs compute prefix sums using the Hillis-Steele / Blelloch parallel scan inside SIMD registers: within a 256-bit AVX2 register holding eight 32-bit integers, three shift-and-add steps (_mm256_slli_si256 + _mm256_add_epi32 by 1, 2, and 4 lanes) compute the prefix sum of 8 elements in $O(\log_2 W)$ instructions. Furthermore, in 2D prefix sums, never allocate a jagged std::vector<std::vector<int64_t>>; each row is a separate heap allocation that causes L1 TLB misses when reading $P[r-1][c]$. Flatten the matrix into a single contiguous array of length $(R+1)(C+1)$ indexed via r * (C + 1) + c.
Trace 2D prefix-sum inclusion-exclusion P[r2][c2] - P[r1-1][c2] - P[r2][c1-1] + P[r1-1][c1-1], O(1) difference-array range updates, and Kadane circular max subarray.
Invariant & State
Production Implementation: Contiguous 2D Prefix Sum & 2D Difference Stencil
#include <cstdint>
#include <vector>
class FlatGrid2D {
std::size_t rows_;
std::size_t cols_;
std::vector<std::int64_t> data_; // (rows_ + 1) * (cols_ + 2) for safe sentinel padding
std::size_t idx(std::size_t r, std::size_t c) const noexcept {
return r * (cols_ + 2) + c;
}
public:
FlatGrid2D(std::size_t rows, std::size_t cols)
: rows_(rows), cols_(cols), data_((rows + 2) * (cols + 2), 0) {}
// O(1) 2D Difference Array range update on 0-indexed inclusive [r1..r2] x [c1..c2]
void range_add(std::size_t r1, std::size_t c1, std::size_t r2, std::size_t c2, std::int64_t delta) noexcept {
// Shift to 1-indexed internal coordinates
++r1; ++c1; ++r2; ++c2;
data_[idx(r1, c1)] += delta;
data_[idx(r1, c2 + 1)] -= delta;
data_[idx(r2 + 1, c1)] -= delta;
data_[idx(r2 + 1, c2 + 1)] += delta;
}
// O(R * C) in-place 2D prefix sum reconstruction (call once to materialize values,
// or twice to turn a 2D difference array directly into a 2D prefix sum query table!)
void materialize_prefix_sums() noexcept {
for (std::size_t r = 1; r <= rows_; ++r) {
for (std::size_t c = 1; c <= cols_; ++c) {
data_[idx(r, c)] += data_[idx(r - 1, c)]
+ data_[idx(r, c - 1)]
- data_[idx(r - 1, c - 1)];
}
}
}
// O(1) subrectangle sum query on 0-indexed inclusive [r1..r2] x [c1..c2]
// Prerequisite: data_ holds 2D prefix sums
std::int64_t query_rect(std::size_t r1, std::size_t c1, std::size_t r2, std::size_t c2) const noexcept {
return data_[idx(r2 + 1, c2 + 1)]
- data_[idx(r1, c2 + 1)]
- data_[idx(r2 + 1, c1)]
+ data_[idx(r1, c1)];
}
};
Even when input array elements fit comfortably inside 32-bit signed integers ($|A[i]| \le 10^9$), a prefix sum over $N = 10^5$ elements reaches $10^{14}$, overflowing int32_t (max $\approx 2.14 \times 10^9$) and invoking Undefined Behavior in C/C++. Always widen prefix sum accumulators to int64_t. Additionally, remember that prefix queries require an invertible binary operation (an abelian group like $+$ with inverse $-$, or $\oplus$ with self-inverse $\oplus$). You cannot use prefix arrays for range minimum/maximum or $\gcd$ queries because $\min$ and $\gcd$ lack inverse elements—use a Sparse Table or Segment Tree instead.