Maximum sum rectangle
Given a 2D matrix of integers that may be positive or negative, find the axis-aligned rectangular submatrix whose entries add up to the largest possible total.
Note. This is typically solved by collapsing pairs of rows into a running column sum and applying Kadane's algorithm to it, rather than by one recurrence over a table.
Do this lesson first: house robberExample input
the 2 by 12 matrix [[-3, -4, 4, -6, -4, 1, -6, 6, 0, 5, -2, 2], [-4, -2, 1, -2, -6, 6, -5, 5, 3, 2, 5, -5]]
Expected output
24, from rows 0 to 1 and columns 7 to 10
Break it down
Answer each question out loud before you open it. Getting it wrong here is the useful part. A revealed answer you never guessed at teaches you nothing.
Fill the table
The table pauses before each cell you have to supply. Type the value the recurrence gives, and the write animation confirms it.
Press start. The animation stops at every cell YOUR recurrence must fill.
TWO ROWS AND THREE CELLS ARE BASES, 27 of the 72 cells at the full 12 columns, and each of the two kinds is a base for a different reason. THE FIRST KIND IS THE TWO SINGLE-ROW C ROWS. The column totals of a band that is one row deep are that row of the matrix, unchanged: there is nothing to add up. So C[0-0] IS the matrix's first row and C[1-1] IS its second, and the whole input is visible on the table before a single cell is computed. THE SECOND KIND IS THE FIRST COLUMN OF EACH K ROW, three cells in all. Kadane genuinely starts there. The only rectangle in a band whose right edge is column 0 is column 0 itself, since there is nothing to its left to extend, so K[t-b][0] = C[t-b][0] with no decision to make and no cell to compare against. That is the same reason longest-increasing-subsequence declares dp[0] = 1 rather than computing it. WHAT IS NOT A BASE IS THE POINT OF THE PAGE. The C row of the two-row band, 12 of the 45 computed cells, is COMPUTED, from the two single-row C rows one column at a time. It would have been easy to hand that row over as data, since the matrix is right there, and the page would then have started from three collapsed bands and only ever shown Kadane. Computing it puts the row collapse the problem is named for inside the trace, with its own reads and its own gate, instead of in the setup. ORDER is row-major and it needs exactly two properties, both of which the layout was arranged to give it. A K cell at column j reads its own band's C cell in the row directly above and its own K cell one column to the left, so it needs the row above finished and the current row filled left to right. The C cell of the two-row band reads the two single-row C cells in the same column, which sit in table rows 0 and 2, so it needs the two single-row bands laid out FIRST. That is why the band order is 0-0, 1-1, 0-1 and not, say, 0-0, 0-1, 1-1: the enumeration order of the bands is free in the algorithm, and here it is spent on making the collapse a computed row.
- 1
function maxSumRectangle(matrix) { - 2
const rows = matrix.length, cols = matrix[0].length; - 3
let best = -Infinity; - 4
for (let top = 0; top < rows; top++) { - 5
const colTotal = new Array(cols).fill(0); - 6
for (let bottom = top; bottom < rows; bottom++) { - 7
for (let j = 0; j < cols; j++) colTotal[j] += matrix[bottom][j]; - 8
let running = colTotal[0]; - 9
best = Math.max(best, running); - 10
for (let j = 1; j < cols; j++) { - 11
running = Math.max(colTotal[j], running + colTotal[j]); - 12
best = Math.max(best, running); - 13
} - 14
} - 15
} - 16
return best; - 17
}
The code, the trap, the variations
- 1
function maxSumRectangle(matrix) { - 2
const rows = matrix.length, cols = matrix[0].length; - 3
let best = -Infinity; - 4
for (let top = 0; top < rows; top++) { - 5
const colTotal = new Array(cols).fill(0); - 6
for (let bottom = top; bottom < rows; bottom++) { - 7
for (let j = 0; j < cols; j++) colTotal[j] += matrix[bottom][j]; - 8
let running = colTotal[0]; - 9
best = Math.max(best, running); - 10
for (let j = 1; j < cols; j++) { - 11
running = Math.max(colTotal[j], running + colTotal[j]); - 12
best = Math.max(best, running); - 13
} - 14
} - 15
} - 16
return best; - 17
}
Where people go wrong
Reading the end of the last K row as the answer. The answer is the largest K cell ANYWHERE, and on this table it is not the last one: at 12 columns K[0-1][11] holds 21 while the answer, 24, sits one column to its left. That is not a near miss engineered for the page either. The last cell of a K row is the best rectangle whose right edge is the last column, and there is no reason for the best rectangle in the matrix to end there; whenever the columns after the true right edge total negative, as column 11 totals -3 here, the last cell is strictly smaller. The same mistake in the code is initialising `best` after the loops instead of updating it inside them. THE SECOND MISTAKE IS SKIPPING THE SINGLE-ROW BANDS, and it is the one the picture encourages. The problem says rectangle, the method says collapse a band of rows, and it is easy to hear both as meaning the rectangle must be more than one row tall. It need not be: a one-row rectangle is a rectangle, and the bands with top equal to bottom are exactly the ones that find them. Drop them here and the answer on this matrix does not move, because the two-row band wins at every slider size, which is precisely why the failure is dangerous rather than obvious. Change the matrix and it bites, though it takes more than a single-row optimum to make it bite, and the near miss is worth seeing first. On [[1, -3, 4, 2, -1, 5], [-2, 6, -4, 3, 1, -3]] the best rectangle IS a single row, columns 2 to 5 of row 0 summing to 10, and a solver skipping the single-row bands still returns 10, because that 10 is TIED: columns 1 to 5 of both rows reach it too. A tie rescues the broken solver. On [[-5, 3, 1, -2, 6, -6], [-5, -4, 1, 1, -4, -1]] nothing rescues it. The best rectangle there is columns 1 to 4 of row 0, summing to 8, it is the unique rectangle reaching 8, and the best rectangle using both rows is only 3, so a solver that skipped the single-row bands returns 3 against a true 8. Check that by brute force over the four corners rather than taking it from here, and check the uniqueness too, since it is the half that makes the example work. THE THIRD IS SEEDING THE RUNNING TOTAL WITH ZERO, which is the same bug wearing a rectangle costume. Write `running = Math.max(0, running + colTotal[j])` and you have allowed the EMPTY rectangle, worth 0, as a candidate. On a matrix with a positive answer nothing goes wrong and the bug ships. On an all-negative matrix every answer comes back 0, which is not the sum of any rectangle at all. On this very matrix, take the leftmost 2 columns: every rectangle inside them is negative, the true answer is -2, the single cell in row 1 column 1, and the zero-seeded version returns 0. The slider starts at 5 columns so the page never shows that case; the bug is still there at 5 columns, waiting for a different matrix. THE FOURTH IS COLLAPSING THE BAND FROM SCRATCH FOR EVERY BAND, which costs correctness nothing and time a whole factor of m. Adding the band rows up afresh for each of the m(m + 1) / 2 bands is O(m) per column per band; carrying the previous band's totals and adding one matrix row is O(1). This table does the cheap version in plain sight: C[0-1] is C[0-0] plus C[1-1], read off the table, not re-added from the matrix.
Report the rectangle's four corners, not just its total.
The recurrence does not change and the fill order does not change. What changes is what a K cell remembers: alongside its value it carries the LEFT edge of the rectangle that achieved it, which is column j itself when the cell restarts and the previous cell's remembered left edge when it extends. The band supplies the top and bottom, the cell's own column supplies the right edge, so the argmax cell then names all four. At 12 columns the answer cell K[0-1][10] would carry left = 7, and rows 0 to 1, columns 7 to 10 is the rectangle. Note that ties become visible in a way the total hides: at 12 columns the best rectangle inside row 0 alone is 11 and TWO different rectangles reach it, columns 7 to 9 and columns 7 to 11, so a corner-reporting version has to say which one it means.
Allow the empty rectangle, so the answer can never be below 0.
One line, and it is the line the third pitfall warns about, so it is worth seeing it work correctly for once. Take the answer as max(0, largest K cell) at the END, rather than folding a 0 into the running total inside Kadane's step. The two agree on the ANSWER, and they agree for a reason rather than by luck: a Kadane scan that clamps its running value at 0 reports max(0, the largest rectangle sum), which is what max(0, largest K cell) reports too. Where they differ is what the CELLS mean. A clamped cell holds the best rectangle-or-nothing ending at that column, so it reads 0 wherever every rectangle ending there is negative, and the state paragraph on this page would stop describing it. On this matrix the distinction never reaches the answer at any slider position, since the answer is 5 or more at all eight; on the leftmost 2 columns the table's answer is -2 and the empty-rectangle answer is 0, and both are right answers to their own question.
Give the matrix more rows.
Nothing about the recurrence changes and everything about the size does. The band count goes as m(m + 1) / 2, so 3 rows give 6 bands and 4 give 10, and this table's two rows per band would need 12 and 20 table rows against a 10-row budget. That is the honest reason the matrix here is 2 rows deep. The general method does not care: it walks the bands top by top, keeping one array of column totals per top and adding one matrix row as the bottom descends, which is the same arithmetic as this page's computed C row generalised. Total time O(m² × n), and note which dimension is squared. For a tall thin matrix, transpose first: bands over the SHORT side and Kadane along the long one turns O(m² × n) into O(n² × m), which on a 1000 by 10 matrix is a factor of 100 either way round, since the two costs differ by m over n and not by its square.