Problem overview
The method maxSum receives an Array[Array[Int]] called grid, an m x n grid of integers that may be positive, negative or zero. It returns the largest sum that any rectangular block of cells, a submatrix, can have. A single cell counts as a submatrix.
In the example grid
[ 1, -9, -10, 1]
[-1, 10, 10, 1]
[ 0, 9, 9, -9]
[-1, -1, -1, -1]
the best submatrix is the block [[10, 10], [9, 9]] in the middle, with sum 38.
Approach: sum any rectangle in constant time
A submatrix is fixed by two corners: the top-left cell (x1, y1) and the bottom-right cell (x2, y2). Looping over all pairs of corners is simple. The costly part would be adding up the cells of each rectangle, which can take up to m * n steps and would make the whole search far too slow.
The fix is a table of 2D prefix sums. Entry partialSums(i)(j) holds the sum of the rectangle that runs from (0, 0) to (i, j). It is built in one pass with inclusion-exclusion: the entry is the cell itself, plus the entry above it, plus the entry to its left, minus the entry diagonally above-left, which the two neighbours both include.
For the example grid the table is:
1 -8 -18 -17
0 1 1 3
0 10 19 12
-1 8 16 8
With the table, the sum of any rectangle takes four lookups, again by inclusion-exclusion. Start from the big rectangle that ends at (x2, y2), remove the rows above the submatrix and the columns to its left, and add back the corner that was removed twice:
sum = P(x2, y2) - P(x1 - 1, y2) - P(x2, y1 - 1) + P(x1 - 1, y1 - 1)
Here P is the prefix table, and any entry with a negative index counts as 0. For the middle block, (x1, y1) = (1, 1) and (x2, y2) = (2, 2), so the sum is 19 - (-18) - 0 + 1 = 38.
The search still tries every pair of corners, which is about m^2 * n^2 / 4 rectangles. Each now costs O(1), so the total is O(m^2 * n^2). A later lesson on this problem improves on it.
Step by step
- Create
resultas avarholdinggrid(0)(0). A single cell is a submatrix, so this is a sum that really exists, even when every number is negative. - Create
partialSumsas avalwith a nestedArray.fill:grid.lengthrows, each one anArray.fillofgrid(0).lengthzeros. The innerArray.fillruns once per row, so each row is its own array. - Loop
iover the rows, from 0 untilgrid.length. - Loop
jover the columns inside it, from 0 untilgrid(0).length. - Write the helper
get, placed abovemaxSum. It takes a grid, a rowiand a columnj, and returns the value there, or 0 when the position is outside the grid. This lets the formulas look one row up or one column left without special cases at the edges. - Inside
get, anifchecks that both indices are at least 0 and below the sizes, and returnsgrid(i)(j). After theif, the last expression is0. - Fill the table. In the
jloop, setpartialSums(i)(j)to the cell, plusget(partialSums, i - 1, j), plusget(partialSums, i, j - 1), minusget(partialSums, i - 1, j - 1). - Choose the top-left corner with loops
x1over the rows andy1over the columns. - Choose the bottom-right corner inside them:
x2fromx1until the number of rows, andy2fromy1until the number of columns. The bottom can't be above the top and the right side can't be left of the left side. - Compute
sumas avalwith the four-lookup formula, reading throughgetso that corners on the top or left edge work. - Update
resultwithmath.max(result, sum). - End with
resultas the last expression ofmaxSum.
The solution in Scala
class Solution {
def get(grid: Array[Array[Int]], i: Int, j: Int): Int = {
if (i >= 0 && i < grid.length && j >= 0 && j < grid(0).length) {
return grid(i)(j)
}
0
}
def maxSum(grid: Array[Array[Int]]): Int = {
var result = grid(0)(0)
val partialSums = Array.fill(grid.length)(Array.fill(grid(0).length)(0))
for (i <- 0 until grid.length) {
for (j <- 0 until grid(0).length) {
partialSums(i)(j) = grid(i)(j) + get(partialSums, i - 1, j) +
get(partialSums, i, j - 1) -
get(partialSums, i - 1, j - 1)
}
}
for (x1 <- 0 until grid.length) {
for (y1 <- 0 until grid(0).length) {
for (x2 <- x1 until grid.length) {
for (y2 <- y1 until grid(0).length) {
val sum = get(partialSums, x2, y2) -
get(partialSums, x1 - 1, y2) -
get(partialSums, x2, y1 - 1) +
get(partialSums, x1 - 1, y1 - 1)
result = math.max(result, sum)
}
}
}
}
result
}
}
The Scala details worth knowing:
- A grid is an array of arrays.
grid(i)(j)is the cell in rowiand columnj. The number of rows isgrid.lengthand the number of columns isgrid(0).length. Array.fill(rows)(Array.fill(cols)(0)). The value in the second parentheses is evaluated again for every element, so each row is a separate array. Writing one cell, as inpartialSums(0)(0) = 1, changes that row only. Reusing one array object for all the rows would make the rows share their cells.- Line breaks inside an expression. A line that ends with
+or-clearly continues on the next line, which is how the long formulas in the solution are split. Ending each broken line with its operator is also the safest style, since it is read the same way in any version of Scala. - Mutating a
valarray.partialSumsis aval, but its elements can still be assigned, because the array is mutable. Thevalonly fixes which array the name points to. returningetleaves the method early. The final0is the value when theifdoesn't apply, because the last expression is the method's result.
Complexity
Let the grid have m rows and n columns.
Time: O(m^2 * n^2). Building the table is O(m * n). The four nested loops then visit every pair of corners, and each rectangle sum is a constant number of lookups.
Space: O(m * n). The prefix table has as many entries as the grid.
Edge cases and common mistakes
- All numbers negative.
[[-3, -1], [-2, -4]]gives -1, the best single cell. Startingresultat 0 would wrongly return 0. - One cell.
[[5]]gives 5. - All numbers positive. The whole grid is the best submatrix. For
[[1, 2], [3, 4]]the answer is 10. - Rectangles on the top or left edge. For them
x1 - 1ory1 - 1is -1. The helper turns those lookups into 0, so no special cases are needed. - Forgetting the added-back corner. Without
+ get(partialSums, x1 - 1, y1 - 1)the region above and to the left is subtracted twice, and the sum is too small. - Starting
x2at 0. A bottom row above the top row would describe a rectangle that doesn't exist, and the formula would give a meaningless number.
Testing
- The example grid above gives
38. [[-3, -1], [-2, -4]]gives-1.[[5]]gives5.[[1, 2], [3, 4]]gives10.
