Problem overview
The method maxSum receives a grid of integers, a [][]int with m rows and n columns. It returns the largest sum over all submatrices: rectangles of adjacent rows and columns. The numbers may be negative.
In the example grid, the submatrix made of the rows 1 and 2 and the columns 1 and 2 holds 10, 10, 9 and 9, which add up to 38. No other rectangle does better, so the answer is 38.
Approach: prefix sums on a grid
A submatrix is fixed by its top-left corner (x1, y1) and its bottom-right corner (x2, y2). There are about m²n²/4 such pairs, so trying all of them is only feasible if each rectangle's sum costs O(1). Adding up the cells one by one would multiply the cost by the area.
The 2D prefix sum gives the constant-time sum. Let partialSums[i][j] be the sum of the rectangle from (0, 0) to (i, j). It is built by inclusion and exclusion: the cell itself, plus the rectangle above, plus the rectangle to the left, minus the rectangle above-left, which the previous two both include. Any rectangle's sum is then the same trick run backwards: the big prefix rectangle, minus the rows above, minus the columns to the left, plus the corner that was subtracted twice.
Step by step
- Start
resultatgrid[0][0]. A single cell is a submatrix, so this is a safe start even when everything is negative. - Allocate
partialSums. Make a[][]intwithlen(grid)rows, then make each row a slice oflen(grid[0])zeros.make([][]int, n)alone leaves every rownil, so each row must be made before it is written. - Loop over rows
iand columnsjto fill the table. - Write the helper
get. It is a method onSolutionthat returns the value at(i, j), or 0 when the position is outside the grid. Then the formulas can look one row up or one column left without special cases for the first row and column. - Use a bounds check. If
iandjare both at least 0 and below the row and column counts, returngrid[i][j]; otherwise return 0. - Fill each prefix cell as
grid[i][j]plusget(i-1, j)plusget(i, j-1)minusget(i-1, j-1). - Choose the top-left corner with the loops
x1over rows andy1over columns. - Choose the bottom-right corner with
x2fromx1andy2fromy1, so the rectangle is never inverted. - Compute the rectangle's sum as
get(x2, y2) - get(x1-1, y2) - get(x2, y1-1) + get(x1-1, y1-1), all onpartialSums. - Update
resultwhen the sum is larger, and return it after all the loops.
The solution in Go
type Solution struct {}
func (this Solution) get(grid [][]int, i, j int) int {
if i >= 0 && i < len(grid) && j >= 0 && j < len(grid[0]) {
return grid[i][j]
}
return 0
}
func (this Solution) maxSum(grid [][]int) int {
result := grid[0][0]
partialSums := make([][]int, len(grid))
for i := range partialSums {
partialSums[i] = make([]int, len(grid[0]))
}
for i := 0; i < len(grid); i++ {
for j := 0; j < len(grid[0]); j++ {
partialSums[i][j] = grid[i][j] + this.get(partialSums, i-1, j) +
this.get(partialSums, i, j-1) -
this.get(partialSums, i-1, j-1)
}
}
for x1 := 0; x1 < len(grid); x1++ {
for y1 := 0; y1 < len(grid[0]); y1++ {
for x2 := x1; x2 < len(grid); x2++ {
for y2 := y1; y2 < len(grid[0]); y2++ {
sum := this.get(partialSums, x2, y2) -
this.get(partialSums, x1-1, y2) -
this.get(partialSums, x2, y1-1) +
this.get(partialSums, x1-1, y1-1)
if sum > result {
result = sum
}
}
}
}
}
return result
}
Go details:
- A slice of slices. A
[][]intis a slice whose elements are slices, so rows may in principle differ in length. The code assumes a rectangular grid and useslen(grid[0])as the width. for i := range partialSumswith one variable ranges over the indices, which is exactly what is needed to make each row.- Line breaks and operators. Go inserts a semicolon at the end of a line that ends with an identifier, a literal or a closing bracket. That is why the broken lines in the code end with
+or-: a line ending in an operator continues onto the next one. Starting the next line with the operator would be a syntax error. - Passing a slice shares data.
getreceives the slice header, not a copy of the numbers, so passingpartialSumsis cheap. - No overflow concerns.
intis 64 bits, so ordinary grid values and their sums will not overflow.
Worked check
For the example grid, the prefix table is [[1 -8 -18 -17] [0 1 1 3] [0 10 19 12] [-1 8 16 8]]. For the corners (1, 1) to (2, 2), the terms are get(2,2) = 19, get(0,2) = -18, get(2,0) = 0 and get(0,0) = 1, so the sum is 19 - (-18) - 0 + 1 = 38.
Complexity
Time: O(m² * n²). Building the table takes O(m * n). The four nested loops visit every pair of corners, and each rectangle costs a constant number of lookups. For a 100 by 100 grid that is roughly 25 million rectangles, which is fine, but it grows quickly. The next lesson on this problem fixes two row boundaries and solves the column part as a maximum-subarray problem, which removes one factor of n.
Space: O(m * n). The table has the same size as the grid.
Edge cases and common mistakes
- All negative grid. For
[[-3, -1], [-2, -5]]the answer is -1, the best single cell. Startingresultat 0 would give a wrong 0. - Subtracting the corner. Forgetting
+ get(x1-1, y1-1)subtracts the top-left overlap twice. - Row and column mix-ups. Index the grid as
grid[row][col]. Passing(y, x)togetreads the wrong cell. - Unmade rows. Writing to
partialSums[i][j]before the row is made panics, because the row isnil. - Loop bounds.
x2must start atx1andy2aty1, not at 0.
Testing
- The example grid gives
38. [[-3, -1], [-2, -5]]gives-1.[[5]]gives5.