mirror of
https://github.com/TheAlgorithms/Java.git
synced 2026-03-13 08:40:43 +08:00
feat: Add Prefix Sum category with 1D and 2D implementations (#7220)
Co-authored-by: Deniz Altunkapan <deniz.altunkapan@outlook.com>
This commit is contained in:
54
src/main/java/com/thealgorithms/prefixsum/PrefixSum.java
Normal file
54
src/main/java/com/thealgorithms/prefixsum/PrefixSum.java
Normal file
@@ -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
src/main/java/com/thealgorithms/prefixsum/PrefixSum2D.java
Normal file
64
src/main/java/com/thealgorithms/prefixsum/PrefixSum2D.java
Normal file
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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
src/test/java/com/thealgorithms/prefixsum/PrefixSumTest.java
Normal file
80
src/test/java/com/thealgorithms/prefixsum/PrefixSumTest.java
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user