Start learning free

LESSON 242 OF 415 · PARTIAL SUMS #2

Maximum Sum Submatrix in Swift: Column Sums and Kadane's Algorithm

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 21
    Ctrl + Enter to check

    SWIFT · PARTIAL SUMS #2

    Maximum Sum Submatrix in Swift, explained

    Problem overview

    The method maxSum receives a grid of integers and returns the largest sum of any submatrix: a rectangle made of consecutive rows and consecutive columns. The numbers can be negative, so the whole grid is not always the best choice.

    For the grid

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

    the answer is 38, from the block [[10, 10], [9, 9]].

    In this lesson's code, n is the number of rows and m is the number of columns.

    Approach: fix two rows, turn the rest into a 1D problem

    Pick a top row x1 and a bottom row x2. Any rectangle between those two rows takes all the rows from x1 to x2 and a stretch of consecutive columns. Add up each column over just those rows, and you get one number per column: an array of m values. A rectangle between the two rows is then a contiguous subarray of that array, and its sum is the sum of the subarray.

    So for each pair of rows, the question becomes: what is the largest sum of a contiguous subarray of this array? That is the Maximum Sum Subarray problem, which Kadane's algorithm solves in O(m).

    Building the column sums for each pair of rows by adding up the rows would be slow. Instead, precompute columnPartialSums[i][j]: the sum of column j from row 0 down to row i. Then the sum of column y between rows x1 and x2 is columnPartialSums[x2][y] minus columnPartialSums[x1 - 1][y], the part above row x1, which only exists when x1 > 0.

    In the example, take x1 = 1 and x2 = 2. The column sums over rows 1 and 2 are [-1, 19, 19, -8]. The best subarray of that is 19 + 19 = 38, which is the answer. The other row pairs give smaller values.

    Step by step

    1. Store the sizes in n (rows) and m (columns), both as let, since they never change. Then create result as a var, starting from grid[0][0]. A single cell is a submatrix, so this works even if every number is negative. Starting at 0 would not.
    2. Create columnPartialSums, a [[Int]] of n rows of m zeros, using repeating:count: at both levels.
    3. Visit every cell with nested loops: i over the rows, j over the columns.
    4. Copy the cell into columnPartialSums[i][j].
    5. Add the cell above when i > 0, so each entry includes everything above it in the same column.
    6. Choose the top row x1 with a loop over all rows.
    7. Choose the bottom row x2 from x1 to the last row. The bottom cannot be above the top.
    8. Create nums, an array of m zeros, to hold the column sums between those two rows.
    9. Loop over each column y.
    10. Set nums[y] to the column's sum from row 0 to row x2.
    11. Subtract the part above when x1 > 0: remove columnPartialSums[x1 - 1][y]. What is left is only rows x1 to x2.
    12. Call maxSumSubarray(nums) to get the best contiguous sum, keep it in a constant called sum, and update result with max.
    13. Return result after the loops.
    14. Write maxSumSubarray as the 1D solution: result starts at nums[0], a running maxSum starts at 0, and for each val, maxSum = val + max(maxSum, 0) and result = max(result, maxSum). A negative running sum can only make the next subarray smaller, so it is replaced by 0 before val is added.

    The solution in Swift

    class Solution {
        func maxSumSubarray(_ nums: [Int]) -> Int {
            var result = nums[0]
            var maxSum = 0
            for val in nums {
                maxSum = val + max(maxSum, 0)
                result = max(result, maxSum)
            }
            return result
        }
    
        func maxSum(_ grid: [[Int]]) -> Int {
            let n = grid.count
            let m = grid[0].count
            var result = grid[0][0]
    
            var columnPartialSums = [[Int]](repeating: [Int](repeating: 0, count: m), count: n)
            for i in 0..<n {
                for j in 0..<m {
                    columnPartialSums[i][j] = grid[i][j]
                    if i > 0 {
                        columnPartialSums[i][j] += columnPartialSums[i - 1][j]
                    }
                }
            }
    
            for x1 in 0..<n {
                for x2 in x1..<n {
                    var nums = [Int](repeating: 0, count: m)
                    for y in 0..<m {
                        nums[y] = columnPartialSums[x2][y]
                        if x1 > 0 {
                            nums[y] -= columnPartialSums[x1 - 1][y]
                        }
                    }
                    let sum = maxSumSubarray(nums)
                    result = max(result, sum)
                }
            }
            return result
        }
    }
    

    The Swift details worth knowing:

    • Two methods, one class. maxSum calls maxSumSubarray by its plain name. Inside a method, Swift looks the name up on self, so no prefix is needed, and the order in which the methods are written does not matter.
    • The same name at two levels. Both maxSumSubarray and maxSum use local variables called result. Each belongs to its own function and they do not interfere. Inside maxSumSubarray, maxSum is a local Int, not the method.
    • [[Int]](repeating:count:). The inner [Int](repeating: 0, count: m) is copied into each of the n rows. Arrays are value types, so the rows are independent and writing to one row does not change another.
    • Compound assignment. += and -= work directly on an element reached through two subscripts, like columnPartialSums[i][j] += ....
    • A fresh nums each time. Declaring it with var inside the loops makes a new array for every pair of rows. Its cells are all rewritten before use.
    • Empty input crashes. grid[0] and nums[0] stop the program on an empty array. The lesson's tests use non-empty grids.

    Complexity

    Time: O(n² * m). There are about n² / 2 pairs of rows. For each pair, building nums costs O(m) and so does Kadane's algorithm. The prefix table costs O(n * m), which is smaller. Compared with enumerating all four corners, which takes O(n² * m²), this saves a whole factor of m.

    Space: O(n * m). The table columnPartialSums has as many cells as the grid. The array nums adds only O(m).

    Edge cases and common mistakes

    • All negative numbers. For [[-3, -1], [-2, -5]] the answer is -1, the best single cell. Both result variables start from an actual element, so they are never stuck at 0.
    • A single row. With [[1, 2, 3]], the only pair of rows is x1 = x2 = 0. The result is 6.
    • A single column. [[1], [-2], [3]] gives 3. Each nums has one element.
    • Forgetting the subtraction when x1 > 0. Without it, nums would hold the sums from row 0 down to x2, and the rows above x1 would count too.
    • Reading columnPartialSums[x1 - 1] when x1 is 0. That index is -1 and crashes the program, which is what the if x1 > 0 guard prevents.
    • Starting the inner search at row 0. The bottom row must run from x1, otherwise the code would try rectangles with the bottom above the top.

    Testing

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