Start learning free

LESSON 218 OF 348 · PARTIAL SUMS #2

Maximum Sum Submatrix in TypeScript: Reduce to Max Subarray

Practice O(n2 * m) 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 14
    Ctrl + Enter to check

    TYPESCRIPT · PARTIAL SUMS #2

    Maximum Sum Submatrix in TypeScript, explained

    Problem overview

    The method maxSum receives a rectangular grid of integers as a number[][]. It returns the largest sum over all submatrices, where a submatrix is a rectangle of adjacent rows and columns with at least one cell.

    In the example grid

    [ 1, -9, -10,  1]
    [-1, 10,  10,  1]
    [ 0,  9,   9, -9]
    [-1, -1,  -1, -1]
    

    the best rectangle is [[10, 10], [9, 9]], with sum 38.

    Approach: fix two rows, then solve a 1D problem

    Choose a top row x1 and a bottom row x2. Every rectangle between those two rows spans all the rows from x1 to x2 and some run of consecutive columns. Now squash the rows: for each column, add up its cells from row x1 to row x2. That gives one array of column sums, and the sum of a rectangle between those rows and columns a..b is exactly the sum of the squashed array over a..b.

    So the best rectangle between rows x1 and x2 is the maximum sum subarray of the squashed array. That is the problem solved earlier in this chapter, and it takes one linear pass. Trying all pairs of rows covers every rectangle.

    The squashed array needs to be cheap to build. A table of column prefix sums does it: columnPartialSums[i][j] holds the sum of column j from row 0 down to row i. The cells of column j between rows x1 and x2 then sum to columnPartialSums[x2][j] minus columnPartialSums[x1 - 1][j], where the subtraction is skipped when x1 is 0.

    On the example, take rows 1 and 2. The column sums are -1 + 0 = -1, 10 + 9 = 19, 10 + 9 = 19 and 1 - 9 = -8. The best subarray of [-1, 19, 19, -8] is [19, 19], which is 38.

    Step by step

    1. Store the sizes. n is the number of rows (grid.length) and m the number of columns (grid[0].length). result starts at grid[0][0], because a single cell is a submatrix and the answer can be negative.
    2. Create columnPartialSums: an array of n rows, each a new array of m zeros.
    3. Fill it. Loop over every cell (i, j). Set the entry to grid[i][j] and, when i is above zero, add the entry directly above it. Row 0 has nothing above it.
    4. Choose the top row x1 and the bottom row x2, with x2 starting at x1.
    5. Build the squashed array nums of length m. For each column y, set nums[y] to columnPartialSums[x2][y], and when x1 is above zero, subtract columnPartialSums[x1 - 1][y]. This leaves only rows x1 to x2.
    6. Solve the 1D problem by calling this.maxSumSubarray(nums) and update result with Math.max.
    7. Return result.
    8. Write maxSumSubarray. It keeps the best sum in result, starting at nums[0], and the best sum of a run that ends at the current element in maxSum, starting at 0. For each element val, maxSum becomes val + Math.max(maxSum, 0): a run with a negative sum is dropped, and the new run starts at val. Then result takes the larger of itself and maxSum.

    The solution in TypeScript

    class Solution {
        maxSumSubarray(nums: number[]): number {
            let result = nums[0];
            let maxSum = 0;
            for (const val of nums) {
                maxSum = val + Math.max(maxSum, 0);
                result = Math.max(result, maxSum);
            }
            return result;
        }
    
        maxSum(grid: number[][]): number {
            const n = grid.length;
            const m = grid[0].length;
            let result = grid[0][0];
    
            const columnPartialSums: number[][] = [];
            for (let i = 0; i < n; ++i) {
                columnPartialSums.push(new Array(m).fill(0));
            }
            for (let i = 0; i < n; ++i) {
                for (let j = 0; j < m; ++j) {
                    columnPartialSums[i][j] = grid[i][j];
                    if (i > 0) {
                        columnPartialSums[i][j] += columnPartialSums[i - 1][j];
                    }
                }
            }
    
            for (let x1 = 0; x1 < n; ++x1) {
                for (let x2 = x1; x2 < n; ++x2) {
                    const nums: number[] = new Array(m);
                    for (let y = 0; y < m; ++y) {
                        nums[y] = columnPartialSums[x2][y];
                        if (x1 > 0) {
                            nums[y] -= columnPartialSums[x1 - 1][y];
                        }
                    }
                    const sum = this.maxSumSubarray(nums);
                    result = Math.max(result, sum);
                }
            }
            return result;
        }
    }
    

    Some TypeScript details:

    • const arrays can change. columnPartialSums and nums are const, which only stops you from pointing the name at a different array. Pushing rows and assigning cells is fine.
    • Allocating without filling. new Array(m) creates an array of length m with empty slots. That is enough for nums, because the next loop assigns every index before anything reads it. Reading a never-assigned slot would give undefined.
    • Compound assignment. nums[y] -= columnPartialSums[x1 - 1][y] means nums[y] = nums[y] - columnPartialSums[x1 - 1][y].
    • Methods reach each other through this. maxSum calls its sibling as this.maxSumSubarray(nums). The call works because it is made as a method call on the object, so this is the instance.
    • Annotations are erased. const nums: number[] is checked by the compiler only.

    Complexity

    Let the grid have n rows and m columns.

    Time: O(n² * m). Building the column table costs O(n * m). There are about n² / 2 pairs of rows, and each pair costs O(m) to build nums and O(m) for the 1D pass. This beats the earlier approach that tried every pair of corners, which was O(n² * m²). If the grid has more rows than columns, you can transpose it first so the pair loop runs over the shorter side.

    Space: O(n * m). The column table has the grid's shape. The array nums and the variables in the 1D pass add only O(m).

    Edge cases and common mistakes

    • All negative. For [[-3, -1], [-2, -5]] the answer is -1. This works because both result variables start from a real cell or element, not from 0.
    • Single row. [[1, 2, 3]] gives 6. The pair loop has only x1 = x2 = 0, and the problem is exactly the 1D one.
    • Single column. [[1], [-5], [4]] gives 4. Each nums has one element.
    • Forgetting to subtract above the top row. Without the x1 > 0 subtraction, every rectangle would be counted as if it started at row 0.
    • Starting the 1D pass at 0. If result in maxSumSubarray started at 0, any row pair with all negative column sums would report 0, which is not a real submatrix sum.
    • Mixing up the sizes. The grid has n rows and m columns here. Using n as the column count in the y loop reads past the row's end for a non-square grid.

    Testing

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