Start learning free

LESSON 243 OF 414 · PARTIAL SUMS #2

Maximum Sum Submatrix in Rust: 2D Prefix Sums

Practice O(n2 * m2) Read the full explanation

Given a m x n grid filled with integers, find the submatrix with maximum sum among all submatrices.

Example:

Input:
[
  [ 1,  -9, -10,  1],
  [-1,  10,  10,  1],
  [ 0,   9,   9, -9],
  [-1,  -1,  -1, -1]
]
Output: 38
Explanation: Submatrix [[10, 10], [9, 9]]
STEP 1 OF 18
    Ctrl + Enter to check

    RUST · PARTIAL SUMS #2

    Maximum Sum Submatrix in Rust, explained

    Problem overview

    The function max_sum receives a grid of integers, grid, as a Vec<Vec<i32>>. It must return the largest sum that any rectangular block of adjacent cells can have. The block can be a single cell, a row segment, a column segment or any rectangle, and the numbers can be negative.

    In the example the grid is

     1  -9 -10   1
    -1  10  10   1
     0   9   9  -9
    -1  -1  -1  -1
    

    and the answer is 38. The block made of the rows 1 and 2 and the columns 1 and 2 holds 10, 10, 9 and 9. No other rectangle does better.

    Approach: sum any rectangle in constant time

    A rectangle is fixed by two corners: the top-left one (x1, y1) and the bottom-right one (x2, y2). A grid with n rows and m columns has about n² m² / 4 rectangles, so checking them all is only cheap if each rectangle's sum costs O(1). Adding up its cells every time would cost up to O(nm) more, for O(n³ m³) overall.

    The tool for that is a 2D prefix sum. Let partial[i][j] be the sum of the whole rectangle from (0, 0) to (i, j). Two rules do the work.

    • Building it: partial[i][j] is the cell, plus the prefix above it, plus the prefix to its left, minus the prefix diagonally above-left. The top and left prefixes both contain the diagonal one, so it was counted twice and must be removed once.
    • Using it: the sum of the rectangle from (x1, y1) to (x2, y2) is partial[x2][y2], minus the rows above the rectangle, minus the columns to its left, plus back the corner that both subtractions removed.

    With that, the solution tries every pair of corners, computes each sum with four lookups and keeps the maximum. The lesson trades speed for simplicity: it is O(n² m²), which a later lesson on the same problem improves.

    Step by step

    1. Store the sizes. n is the number of rows and m the number of columns, taken from grid.len() and grid[0].len().
    2. Start result at grid[0][0]. A single cell is a valid rectangle, so this is a real sum. Starting at 0 would be wrong when every number is negative, because the best rectangle then has a negative sum.
    3. Allocate partial as n rows of m zeros. The 0i32 literal fixes the element type.
    4. Loop over every cell (i, j), rows outside and columns inside.
    5. Write the helper get first, because the formulas need to read "one row up" or "one column left" even at the border. It returns the grid value at (i, j), or 0 when the position is outside the grid. Its indices are i32 because callers pass -1.
    6. Fill partial[i][j] with the cell plus the prefix above, plus the prefix to the left, minus the diagonal prefix. Outside the grid, get supplies 0, so the first row and column need no special code.
    7. Choose the top-left corner with x1 over the rows and y1 over the columns.
    8. Choose the bottom-right corner with x2 from x1 to the last row and y2 from y1 to the last column. The bottom edge can't be above the top edge, and the right edge can't be left of the left edge.
    9. Compute sum from the four prefix values, using get again so that a rectangle touching the top or left border subtracts 0.
    10. Keep the larger value in result, and return it after all the loops.

    The solution in Rust

    impl Solution {
        fn get(grid: &Vec<Vec<i32>>, i: i32, j: i32) -> i32 {
            if i >= 0 && i < grid.len() as i32 && j >= 0 && j < grid[0].len() as i32 {
                return grid[i as usize][j as usize];
            }
            0
        }
    
        fn max_sum(grid: Vec<Vec<i32>>) -> i32 {
            let n = grid.len();
            let m = grid[0].len();
            let mut result = grid[0][0];
            let mut partial = vec![vec![0i32; m]; n];
    
            for i in 0..n {
                for j in 0..m {
                    partial[i][j] = grid[i][j]
                        + Self::get(&partial, i as i32 - 1, j as i32)
                        + Self::get(&partial, i as i32, j as i32 - 1)
                        - Self::get(&partial, i as i32 - 1, j as i32 - 1);
                }
            }
    
            for x1 in 0..n {
                for y1 in 0..m {
                    for x2 in x1..n {
                        for y2 in y1..m {
                            let sum = Self::get(&partial, x2 as i32, y2 as i32)
                                - Self::get(&partial, x1 as i32 - 1, y2 as i32)
                                - Self::get(&partial, x2 as i32, y1 as i32 - 1)
                                + Self::get(&partial, x1 as i32 - 1, y1 as i32 - 1);
                            if sum > result {
                                result = sum;
                            }
                        }
                    }
                }
            }
            result
        }
    }
    

    The Rust details worth a closer look:

    • Why get takes i32. The loop variables are usize, which can't hold -1: in an optimized build 0usize - 1 wraps to a huge number, and in a debug build it panics. Casting with as i32 first makes i as i32 - 1 a safe -1, and get turns it into 0.
    • Precedence of as. as binds tighter than -, so i as i32 - 1 means (i as i32) - 1.
    • Casting back to index. Vectors are indexed with usize. Once the bounds check has shown that i and j are not negative, i as usize is safe.
    • Borrowing. Self::get(&partial, ...) only reads, so a shared borrow is enough. The call sits inside the assignment partial[i][j] = ..., which is fine because the borrow ends before the assignment happens.
    • vec![vec![0i32; m]; n]. The inner vector is cloned n times, so each row is independent.
    • Multi-line expressions. A line that starts with + or - continues the previous one, since the statement ends only at the semicolon.
    • Tail expressions. The final 0 in get and result in max_sum have no semicolon, so they are the returned values.

    Complexity

    Time: O(n² m²). Building partial takes O(nm). The four nested loops choose a pair of corners, which is up to about n² m² / 4 pairs, and each pair costs a constant number of lookups.

    Space: O(nm). The table partial has the same size as the grid.

    Edge cases and common mistakes

    • All numbers negative. For [[-3, -1], [-2, -5]] the answer is -1, the largest single cell. Initializing result with 0 would return 0, which no rectangle has.
    • A single cell. For [[5]] the loops each run once and the answer is 5.
    • One row or one column. [[1, 2, 3]] gives 6, and [[2], [-1], [3]] gives 4. The same code covers both, since get returns 0 above the first row.
    • Forgetting the added-back corner. Subtracting the rows above and the columns to the left removes the overlap twice. The fourth term of sum restores it.
    • Looping from 0 instead of from the first corner. If x2 or y2 started below x1 or y1, the formula would describe a rectangle that doesn't exist.
    • Empty grid. grid[0] panics on an empty vector, so the function assumes at least one row and one column.
    • Overflow. Sums are i32. With optimizations they wrap silently on overflow, so very large grids of large numbers would need i64.

    Testing

    • The example grid gives 38.
    • [[-3, -1], [-2, -5]] gives -1.
    • [[5]] gives 5.
    • [[1, 2, 3]] gives 6.
    • [[2], [-1], [3]] gives 4.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free