Skip to content

Commit 109ed2e

Browse files
feat: Add Prefix Sum category with 1D and 2D implementations (#7220)
Co-authored-by: Deniz Altunkapan <deniz.altunkapan@outlook.com>
1 parent 1b9373e commit 109ed2e

File tree

4 files changed

+290
-0
lines changed

4 files changed

+290
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.thealgorithms.prefixsum;
2+
3+
/**
4+
* A class that implements the Prefix Sum algorithm.
5+
*
6+
* <p>Prefix Sum is a technique used to preprocess an array such that
7+
* range sum queries can be answered in O(1) time.
8+
* The preprocessing step takes O(N) time.
9+
*
10+
* <p>This implementation uses a long array for the prefix sums to prevent
11+
* integer overflow when the sum of elements exceeds Integer.MAX_VALUE.
12+
*
13+
* @see <a href="https://en.wikipedia.org/wiki/Prefix_sum">Prefix Sum (Wikipedia)</a>
14+
* @author Chahat Sandhu, <a href="https://github.com/singhc7">singhc7</a>
15+
*/
16+
public class PrefixSum {
17+
18+
private final long[] prefixSums;
19+
20+
/**
21+
* Constructor to preprocess the input array.
22+
*
23+
* @param array The input integer array.
24+
* @throws IllegalArgumentException if the array is null.
25+
*/
26+
public PrefixSum(int[] array) {
27+
if (array == null) {
28+
throw new IllegalArgumentException("Input array cannot be null");
29+
}
30+
this.prefixSums = new long[array.length + 1];
31+
this.prefixSums[0] = 0;
32+
33+
for (int i = 0; i < array.length; i++) {
34+
// Automatically promotes int to long during addition
35+
this.prefixSums[i + 1] = this.prefixSums[i] + array[i];
36+
}
37+
}
38+
39+
/**
40+
* Calculates the sum of elements in the range [left, right].
41+
* Indices are 0-based.
42+
*
43+
* @param left The starting index (inclusive).
44+
* @param right The ending index (inclusive).
45+
* @return The sum of elements from index left to right as a long.
46+
* @throws IndexOutOfBoundsException if indices are out of valid range.
47+
*/
48+
public long sumRange(int left, int right) {
49+
if (left < 0 || right >= prefixSums.length - 1 || left > right) {
50+
throw new IndexOutOfBoundsException("Invalid range indices");
51+
}
52+
return prefixSums[right + 1] - prefixSums[left];
53+
}
54+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.thealgorithms.prefixsum;
2+
3+
/**
4+
* A class that implements the 2D Prefix Sum algorithm.
5+
*
6+
* <p>2D Prefix Sum is a technique used to preprocess a 2D matrix such that
7+
* sub-matrix sum queries can be answered in O(1) time.
8+
* The preprocessing step takes O(N*M) time.
9+
*
10+
* <p>This implementation uses a long array for the prefix sums to prevent
11+
* integer overflow.
12+
*
13+
* @see <a href="https://en.wikipedia.org/wiki/Summed-area_table">Summed-area table (Wikipedia)</a>
14+
* @author Chahat Sandhu, <a href="https://github.com/singhc7">singhc7</a>
15+
*/
16+
public class PrefixSum2D {
17+
18+
private final long[][] prefixSums;
19+
20+
/**
21+
* Constructor to preprocess the input matrix.
22+
*
23+
* @param matrix The input integer matrix.
24+
* @throws IllegalArgumentException if the matrix is null or empty.
25+
*/
26+
public PrefixSum2D(int[][] matrix) {
27+
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
28+
throw new IllegalArgumentException("Input matrix cannot be null or empty");
29+
}
30+
31+
int rows = matrix.length;
32+
int cols = matrix[0].length;
33+
this.prefixSums = new long[rows + 1][cols + 1];
34+
35+
for (int i = 0; i < rows; i++) {
36+
for (int j = 0; j < cols; j++) {
37+
// P[i+1][j+1] = current + above + left - diagonal_overlap
38+
this.prefixSums[i + 1][j + 1] = matrix[i][j] + this.prefixSums[i][j + 1] + this.prefixSums[i + 1][j] - this.prefixSums[i][j];
39+
}
40+
}
41+
}
42+
43+
/**
44+
* Calculates the sum of the sub-matrix defined by (row1, col1) to (row2, col2).
45+
* Indices are 0-based.
46+
*
47+
* @param row1 Top row index.
48+
* @param col1 Left column index.
49+
* @param row2 Bottom row index.
50+
* @param col2 Right column index.
51+
* @return The sum of the sub-matrix.
52+
* @throws IndexOutOfBoundsException if indices are invalid.
53+
*/
54+
public long sumRegion(int row1, int col1, int row2, int col2) {
55+
if (row1 < 0 || row2 >= prefixSums.length - 1 || row2 < row1) {
56+
throw new IndexOutOfBoundsException("Invalid row indices");
57+
}
58+
if (col1 < 0 || col2 >= prefixSums[0].length - 1 || col2 < col1) {
59+
throw new IndexOutOfBoundsException("Invalid column indices");
60+
}
61+
62+
return prefixSums[row2 + 1][col2 + 1] - prefixSums[row1][col2 + 1] - prefixSums[row2 + 1][col1] + prefixSums[row1][col1];
63+
}
64+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.thealgorithms.prefixsum;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
6+
import org.junit.jupiter.api.DisplayName;
7+
import org.junit.jupiter.api.Test;
8+
9+
class PrefixSum2DTest {
10+
11+
@Test
12+
@DisplayName("Test basic 3x3 square matrix")
13+
void testStandardSquare() {
14+
int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
15+
PrefixSum2D ps = new PrefixSum2D(matrix);
16+
17+
// Sum of top-left 2x2: {1,2, 4,5} -> 12
18+
assertEquals(12L, ps.sumRegion(0, 0, 1, 1));
19+
// Sum of bottom-right 2x2: {5,6, 8,9} -> 28
20+
assertEquals(28L, ps.sumRegion(1, 1, 2, 2));
21+
// Full matrix -> 45
22+
assertEquals(45L, ps.sumRegion(0, 0, 2, 2));
23+
}
24+
25+
@Test
26+
@DisplayName("Test rectangular matrix (more cols than rows)")
27+
void testRectangularWide() {
28+
int[][] matrix = {{1, 1, 1, 1}, {2, 2, 2, 2}};
29+
PrefixSum2D ps = new PrefixSum2D(matrix);
30+
31+
// Sum of first 3 columns of both rows -> (1*3) + (2*3) = 9
32+
assertEquals(9L, ps.sumRegion(0, 0, 1, 2));
33+
}
34+
35+
@Test
36+
@DisplayName("Test rectangular matrix (more rows than cols)")
37+
void testRectangularTall() {
38+
int[][] matrix = {{1}, {2}, {3}, {4}};
39+
PrefixSum2D ps = new PrefixSum2D(matrix);
40+
41+
// Sum of middle two elements -> 2+3 = 5
42+
assertEquals(5L, ps.sumRegion(1, 0, 2, 0));
43+
}
44+
45+
@Test
46+
@DisplayName("Test single element matrix")
47+
void testSingleElement() {
48+
int[][] matrix = {{100}};
49+
PrefixSum2D ps = new PrefixSum2D(matrix);
50+
51+
assertEquals(100L, ps.sumRegion(0, 0, 0, 0));
52+
}
53+
54+
@Test
55+
@DisplayName("Test large numbers for overflow (Integer -> Long)")
56+
void testLargeNumbers() {
57+
// 2 billion. Two of these sum to > MAX_INT
58+
int val = 2_000_000_000;
59+
int[][] matrix = {{val, val}, {val, val}};
60+
PrefixSum2D ps = new PrefixSum2D(matrix);
61+
62+
// 4 * 2B = 8 Billion
63+
assertEquals(8_000_000_000L, ps.sumRegion(0, 0, 1, 1));
64+
}
65+
66+
@Test
67+
@DisplayName("Test invalid inputs")
68+
void testInvalidInputs() {
69+
assertThrows(IllegalArgumentException.class, () -> new PrefixSum2D(null));
70+
assertThrows(IllegalArgumentException.class, () -> new PrefixSum2D(new int[][] {})); // empty
71+
assertThrows(IllegalArgumentException.class, () -> new PrefixSum2D(new int[][] {{}})); // empty row
72+
}
73+
74+
@Test
75+
@DisplayName("Test invalid query ranges")
76+
void testInvalidRanges() {
77+
int[][] matrix = {{1, 2}, {3, 4}};
78+
PrefixSum2D ps = new PrefixSum2D(matrix);
79+
80+
// Negative indices
81+
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(-1, 0, 0, 0));
82+
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, -1, 0, 0));
83+
84+
// Out of bounds
85+
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, 0, 2, 0)); // row2 too big
86+
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, 0, 0, 2)); // col2 too big
87+
88+
// Inverted ranges (start > end)
89+
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(1, 0, 0, 0)); // row1 > row2
90+
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, 1, 0, 0)); // col1 > col2
91+
}
92+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.thealgorithms.prefixsum;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
6+
import org.junit.jupiter.api.DisplayName;
7+
import org.junit.jupiter.api.Test;
8+
9+
class PrefixSumTest {
10+
11+
@Test
12+
@DisplayName("Test basic sum with positive integers")
13+
void testStandardCase() {
14+
int[] input = {1, 2, 3, 4, 5};
15+
PrefixSum ps = new PrefixSum(input);
16+
17+
// Sum of range [0, 4] -> 15
18+
assertEquals(15L, ps.sumRange(0, 4));
19+
20+
// Sum of range [1, 3] -> 9
21+
assertEquals(9L, ps.sumRange(1, 3));
22+
}
23+
24+
@Test
25+
@DisplayName("Test array with negative numbers and zeros")
26+
void testNegativeAndZeros() {
27+
int[] input = {-2, 0, 3, -5, 2, -1};
28+
PrefixSum ps = new PrefixSum(input);
29+
30+
assertEquals(1L, ps.sumRange(0, 2));
31+
assertEquals(-1L, ps.sumRange(2, 5));
32+
assertEquals(0L, ps.sumRange(1, 1));
33+
}
34+
35+
@Test
36+
@DisplayName("Test with large integers to verify overflow handling")
37+
void testLargeNumbers() {
38+
// Two values that fit in int, but their sum exceeds Integer.MAX_VALUE
39+
// Integer.MAX_VALUE is approx 2.14 billion.
40+
int val = 2_000_000_000;
41+
int[] input = {val, val, val};
42+
PrefixSum ps = new PrefixSum(input);
43+
44+
// Sum of three 2 billion values is 6 billion (fits in long, overflows int)
45+
assertEquals(6_000_000_000L, ps.sumRange(0, 2));
46+
}
47+
48+
@Test
49+
@DisplayName("Test single element array")
50+
void testSingleElement() {
51+
int[] input = {42};
52+
PrefixSum ps = new PrefixSum(input);
53+
assertEquals(42L, ps.sumRange(0, 0));
54+
}
55+
56+
@Test
57+
@DisplayName("Test constructor with null input")
58+
void testNullInput() {
59+
assertThrows(IllegalArgumentException.class, () -> new PrefixSum(null));
60+
}
61+
62+
@Test
63+
@DisplayName("Test empty array behavior")
64+
void testEmptyArray() {
65+
int[] input = {};
66+
PrefixSum ps = new PrefixSum(input);
67+
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(0, 0));
68+
}
69+
70+
@Test
71+
@DisplayName("Test invalid range indices")
72+
void testInvalidIndices() {
73+
int[] input = {10, 20, 30};
74+
PrefixSum ps = new PrefixSum(input);
75+
76+
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(-1, 1));
77+
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(0, 3));
78+
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(2, 1));
79+
}
80+
}

0 commit comments

Comments
 (0)