Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/main/java/com/thealgorithms/prefixsum/PrefixSum.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.thealgorithms.prefixsum;

/**
* A class that implements the Prefix Sum algorithm.
*
* <p>Prefix Sum is a technique used to preprocess an array such that
* range sum queries can be answered in O(1) time.
* The preprocessing step takes O(N) time.
*
* <p>This implementation uses a long array for the prefix sums to prevent
* integer overflow when the sum of elements exceeds Integer.MAX_VALUE.
*
* @see <a href="https://en.wikipedia.org/wiki/Prefix_sum">Prefix Sum (Wikipedia)</a>
* @author Chahat Sandhu, <a href="https://github.com/singhc7">singhc7</a>
*/
public class PrefixSum {

private final long[] prefixSums;

/**
* Constructor to preprocess the input array.
*
* @param array The input integer array.
* @throws IllegalArgumentException if the array is null.
*/
public PrefixSum(int[] array) {
if (array == null) {
throw new IllegalArgumentException("Input array cannot be null");
}
this.prefixSums = new long[array.length + 1];
this.prefixSums[0] = 0;

for (int i = 0; i < array.length; i++) {
// Automatically promotes int to long during addition
this.prefixSums[i + 1] = this.prefixSums[i] + array[i];
}
}

/**
* Calculates the sum of elements in the range [left, right].
* Indices are 0-based.
*
* @param left The starting index (inclusive).
* @param right The ending index (inclusive).
* @return The sum of elements from index left to right as a long.
* @throws IndexOutOfBoundsException if indices are out of valid range.
*/
public long sumRange(int left, int right) {
if (left < 0 || right >= prefixSums.length - 1 || left > right) {
throw new IndexOutOfBoundsException("Invalid range indices");
}
return prefixSums[right + 1] - prefixSums[left];
}
}
64 changes: 64 additions & 0 deletions src/main/java/com/thealgorithms/prefixsum/PrefixSum2D.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.thealgorithms.prefixsum;

/**
* A class that implements the 2D Prefix Sum algorithm.
*
* <p>2D Prefix Sum is a technique used to preprocess a 2D matrix such that
* sub-matrix sum queries can be answered in O(1) time.
* The preprocessing step takes O(N*M) time.
*
* <p>This implementation uses a long array for the prefix sums to prevent
* integer overflow.
*
* @see <a href="https://en.wikipedia.org/wiki/Summed-area_table">Summed-area table (Wikipedia)</a>
* @author Chahat Sandhu, <a href="https://github.com/singhc7">singhc7</a>
*/
public class PrefixSum2D {

private final long[][] prefixSums;

/**
* Constructor to preprocess the input matrix.
*
* @param matrix The input integer matrix.
* @throws IllegalArgumentException if the matrix is null or empty.
*/
public PrefixSum2D(int[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
throw new IllegalArgumentException("Input matrix cannot be null or empty");
}

int rows = matrix.length;
int cols = matrix[0].length;
this.prefixSums = new long[rows + 1][cols + 1];

for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
// P[i+1][j+1] = current + above + left - diagonal_overlap
this.prefixSums[i + 1][j + 1] = matrix[i][j] + this.prefixSums[i][j + 1] + this.prefixSums[i + 1][j] - this.prefixSums[i][j];
}
}
}

/**
* Calculates the sum of the sub-matrix defined by (row1, col1) to (row2, col2).
* Indices are 0-based.
*
* @param row1 Top row index.
* @param col1 Left column index.
* @param row2 Bottom row index.
* @param col2 Right column index.
* @return The sum of the sub-matrix.
* @throws IndexOutOfBoundsException if indices are invalid.
*/
public long sumRegion(int row1, int col1, int row2, int col2) {
if (row1 < 0 || row2 >= prefixSums.length - 1 || row2 < row1) {
throw new IndexOutOfBoundsException("Invalid row indices");
}
if (col1 < 0 || col2 >= prefixSums[0].length - 1 || col2 < col1) {
throw new IndexOutOfBoundsException("Invalid column indices");
}

return prefixSums[row2 + 1][col2 + 1] - prefixSums[row1][col2 + 1] - prefixSums[row2 + 1][col1] + prefixSums[row1][col1];
}
}
92 changes: 92 additions & 0 deletions src/test/java/com/thealgorithms/prefixsum/PrefixSum2DTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.thealgorithms.prefixsum;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class PrefixSum2DTest {

@Test
@DisplayName("Test basic 3x3 square matrix")
void testStandardSquare() {
int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
PrefixSum2D ps = new PrefixSum2D(matrix);

// Sum of top-left 2x2: {1,2, 4,5} -> 12
assertEquals(12L, ps.sumRegion(0, 0, 1, 1));
// Sum of bottom-right 2x2: {5,6, 8,9} -> 28
assertEquals(28L, ps.sumRegion(1, 1, 2, 2));
// Full matrix -> 45
assertEquals(45L, ps.sumRegion(0, 0, 2, 2));
}

@Test
@DisplayName("Test rectangular matrix (more cols than rows)")
void testRectangularWide() {
int[][] matrix = {{1, 1, 1, 1}, {2, 2, 2, 2}};
PrefixSum2D ps = new PrefixSum2D(matrix);

// Sum of first 3 columns of both rows -> (1*3) + (2*3) = 9
assertEquals(9L, ps.sumRegion(0, 0, 1, 2));
}

@Test
@DisplayName("Test rectangular matrix (more rows than cols)")
void testRectangularTall() {
int[][] matrix = {{1}, {2}, {3}, {4}};
PrefixSum2D ps = new PrefixSum2D(matrix);

// Sum of middle two elements -> 2+3 = 5
assertEquals(5L, ps.sumRegion(1, 0, 2, 0));
}

@Test
@DisplayName("Test single element matrix")
void testSingleElement() {
int[][] matrix = {{100}};
PrefixSum2D ps = new PrefixSum2D(matrix);

assertEquals(100L, ps.sumRegion(0, 0, 0, 0));
}

@Test
@DisplayName("Test large numbers for overflow (Integer -> Long)")
void testLargeNumbers() {
// 2 billion. Two of these sum to > MAX_INT
int val = 2_000_000_000;
int[][] matrix = {{val, val}, {val, val}};
PrefixSum2D ps = new PrefixSum2D(matrix);

// 4 * 2B = 8 Billion
assertEquals(8_000_000_000L, ps.sumRegion(0, 0, 1, 1));
}

@Test
@DisplayName("Test invalid inputs")
void testInvalidInputs() {
assertThrows(IllegalArgumentException.class, () -> new PrefixSum2D(null));
assertThrows(IllegalArgumentException.class, () -> new PrefixSum2D(new int[][] {})); // empty
assertThrows(IllegalArgumentException.class, () -> new PrefixSum2D(new int[][] {{}})); // empty row
}

@Test
@DisplayName("Test invalid query ranges")
void testInvalidRanges() {
int[][] matrix = {{1, 2}, {3, 4}};
PrefixSum2D ps = new PrefixSum2D(matrix);

// Negative indices
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(-1, 0, 0, 0));
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, -1, 0, 0));

// Out of bounds
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, 0, 2, 0)); // row2 too big
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, 0, 0, 2)); // col2 too big

// Inverted ranges (start > end)
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(1, 0, 0, 0)); // row1 > row2
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRegion(0, 1, 0, 0)); // col1 > col2
}
}
80 changes: 80 additions & 0 deletions src/test/java/com/thealgorithms/prefixsum/PrefixSumTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.thealgorithms.prefixsum;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class PrefixSumTest {

@Test
@DisplayName("Test basic sum with positive integers")
void testStandardCase() {
int[] input = {1, 2, 3, 4, 5};
PrefixSum ps = new PrefixSum(input);

// Sum of range [0, 4] -> 15
assertEquals(15L, ps.sumRange(0, 4));

// Sum of range [1, 3] -> 9
assertEquals(9L, ps.sumRange(1, 3));
}

@Test
@DisplayName("Test array with negative numbers and zeros")
void testNegativeAndZeros() {
int[] input = {-2, 0, 3, -5, 2, -1};
PrefixSum ps = new PrefixSum(input);

assertEquals(1L, ps.sumRange(0, 2));
assertEquals(-1L, ps.sumRange(2, 5));
assertEquals(0L, ps.sumRange(1, 1));
}

@Test
@DisplayName("Test with large integers to verify overflow handling")
void testLargeNumbers() {
// Two values that fit in int, but their sum exceeds Integer.MAX_VALUE
// Integer.MAX_VALUE is approx 2.14 billion.
int val = 2_000_000_000;
int[] input = {val, val, val};
PrefixSum ps = new PrefixSum(input);

// Sum of three 2 billion values is 6 billion (fits in long, overflows int)
assertEquals(6_000_000_000L, ps.sumRange(0, 2));
}

@Test
@DisplayName("Test single element array")
void testSingleElement() {
int[] input = {42};
PrefixSum ps = new PrefixSum(input);
assertEquals(42L, ps.sumRange(0, 0));
}

@Test
@DisplayName("Test constructor with null input")
void testNullInput() {
assertThrows(IllegalArgumentException.class, () -> new PrefixSum(null));
}

@Test
@DisplayName("Test empty array behavior")
void testEmptyArray() {
int[] input = {};
PrefixSum ps = new PrefixSum(input);
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(0, 0));
}

@Test
@DisplayName("Test invalid range indices")
void testInvalidIndices() {
int[] input = {10, 20, 30};
PrefixSum ps = new PrefixSum(input);

assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(-1, 1));
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(0, 3));
assertThrows(IndexOutOfBoundsException.class, () -> ps.sumRange(2, 1));
}
}