Problem overview
The function maxSum receives a grid of integers, an Array<IntArray> with n rows and m columns. It returns the largest sum of any submatrix, which is a rectangle of adjacent rows and columns. 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
The best rectangle is [[10, 10], [9, 9]], with sum 38.
Approach: fix two rows, then solve a one-dimensional problem
The other lesson on this problem tries every pair of corners, which needs four nested loops. This version fixes only the top and bottom rows, x1 and x2, and gets rid of two of the loops.
Once the two rows are fixed, a submatrix is a choice of a left and a right column. Add up each column between the two rows and put the results in an array nums of length m. A submatrix between those rows now corresponds to a contiguous subarray of nums, and its sum is the subarray's sum. The best submatrix between the two rows is therefore the maximum subarray sum of nums, a problem with a linear-time solution.
The column sums for every pair of rows come from a table of running totals down each column. columnPartialSums[i][j] is the sum of column j from row 0 to row i. The sum of column j between rows x1 and x2 is columnPartialSums[x2][j] minus columnPartialSums[x1 - 1][j], and the second term is left out when x1 is 0.
Take the rows x1 = 1 and x2 = 2 in the example. The column sums are -1 + 0, 10 + 9, 10 + 9 and 1 + (-9), so nums = [-1, 19, 19, -8]. The best subarray is [19, 19], with sum 38, which matches the expected answer.
Step by step
- Store the dimensions. Put
grid.sizeinnandgrid[0].sizeinm, both withval. - Declare
resultwithvar, starting atgrid[0][0]. A single cell is a submatrix, so this is a safe starting value, even if the answer is negative. - Create
columnPartialSumswithval, asArray(n) { IntArray(m) }. - Fill it with two nested loops,
iover the rows andjover the columns. Set each cell togrid[i][j]. - Add the total above. If
iis greater than 0, addcolumnPartialSums[i - 1][j]to the cell. Row 0 has nothing above it. - Choose the top row. Loop
x1over all rows. - Choose the bottom row. Inside it, loop
x2fromx1to the last row, since the bottom can't be above the top. - Create
numsas avalholding anIntArray(m). It receives the column sums between the two rows. - Loop
yover the columns. - Start from the total down to the bottom row. Set
nums[y]tocolumnPartialSums[x2][y]. - Cut off the part above the top row. If
x1is greater than 0, subtractcolumnPartialSums[x1 - 1][y]fromnums[y]. What remains is the sum of rowsx1tox2. - Solve the one-dimensional problem. Call
maxSumSubarray(nums), store the value in avalnamedsum, and updateresultwithmaxOf(result, sum). - Return
resultafter the loops. - Implement
maxSumSubarrayabovemaxSum. It keepsresult, the best sum so far, starting atnums[0], andmaxSum, the best sum of a subarray that ends at the current element, starting at 0. For eachvalue, setmaxSumtovalue + maxOf(maxSum, 0), then updateresultwithmaxOf(result, maxSum).
The helper uses Kadane's idea. A subarray that ends just before value and has a negative sum can only make the next subarray worse, so it is dropped by replacing it with 0 before value is added. A subarray that has a positive sum is kept and extended.
The solution in Kotlin
class Solution {
fun maxSumSubarray(nums: IntArray): Int {
var result = nums[0]
var maxSum = 0
for (value in nums) {
maxSum = value + maxOf(maxSum, 0)
result = maxOf(result, maxSum)
}
return result
}
fun maxSum(grid: Array<IntArray>): Int {
val n = grid.size
val m = grid[0].size
var result = grid[0][0]
val columnPartialSums = Array(n) { IntArray(m) }
for (i in 0 until n) {
for (j in 0 until m) {
columnPartialSums[i][j] = grid[i][j]
if (i > 0) {
columnPartialSums[i][j] += columnPartialSums[i - 1][j]
}
}
}
for (x1 in 0 until n) {
for (x2 in x1 until n) {
val nums = IntArray(m)
for (y in 0 until m) {
nums[y] = columnPartialSums[x2][y]
if (x1 > 0) {
nums[y] -= columnPartialSums[x1 - 1][y]
}
}
val sum = maxSumSubarray(nums)
result = maxOf(result, sum)
}
}
return result
}
}
The Kotlin details worth a closer look:
- Two functions in one class.
maxSumSubarrayandmaxSumare both members ofSolution. InsidemaxSum, the callmaxSumSubarray(nums)needs no receiver; it calls the method on the same object. Kotlin doesn't require a function to be declared before its use inside a class. - Same names, different scopes. Both functions have a local
result, andmaxSumSubarrayalso has amaxSumvariable whose name matches the other function. A local variable and a method live in different namespaces, so this is legal, though it reads confusingly. They never interfere. - Compound assignment on array cells.
columnPartialSums[i][j] += columnPartialSums[i - 1][j]reads the cell, adds, and writes it back.-=works the same way fornums[y]. valwith a mutable array.val nums = IntArray(m)fixes which array the name refers to; its elements are still assigned in the loop. A freshnumsis created for each pair of rows, so no values carry over.Array(n) { IntArray(m) }. The lambda runs once per row and creates a separate zero-filled row each time.
Complexity
Time: O(n² * m). Building the column table takes O(n * m). There are about n²/2 pairs of rows, and for each pair the function fills nums in O(m) and runs the linear subarray scan over it, also O(m). That is an improvement of a factor of about m over the four-loop version, which takes O(n² * m²).
Space: O(n * m). The column table has as many cells as the grid, and nums adds O(m).
Edge cases and common mistakes
- All negative numbers. For
[[-3, -1], [-2, -4]]the answer is-1. The helper startsresultatnums[0], and the reset to 0 happens before addingvalue, so a subarray is never empty. - A single cell or a single row.
[[5]]returns5. With one row,x1andx2are both 0, and the problem is the one-dimensional one. - Forgetting the
x1 > 0check. ReadingcolumnPartialSums[-1][y]throws anArrayIndexOutOfBoundsException. - Not subtracting the part above the top row. The column sums would then include rows above
x1and the results would be too large or too small, depending on the signs. - Using 0 as the helper's starting best. A start of 0 would report 0 for an all-negative row, which isn't the sum of any non-empty subarray.
Testing
- The grid from the example gives
38. [[-3, -1], [-2, -4]]gives-1.[[5]]gives5.[[-2, 3, -1], [4, -5, 6]]gives6.