Context:FIT2004_MOC · the applied sheet’s cleanest Θ(n2)→Θ(n) upgrade — and the vault’s home for prefix sums, which several other DPs depend on
Parent Framework:Dynamic Programming
Quick Revision
🎯 Objective: find the contiguous non-empty a[i..j] of maximum sum ➔ Θ(n2) by prefix sums over all intervals, Θ(n) by anchoring the subproblem at its right end.
📦 Core Components: prefix sums DP[i]=∑k≤iak ➔ any interval in O(1) | anchored DP′[j] ➔ “best sum ending atj” ➔ the greedy restart.
⚡ Key Constraint: the subarray is non-empty, so an all-negative input answers with its largest single element, never 0. Initialising the running maximum to 0 is the marked error.
📝 How It Works
1. Prefix Sums — the Θ(n2) Solution (applied P6a)
The identity ➔ sum(a[i..j])=sum(a[1..j])−sum(a[1..i−1]), so every interval sum is one subtraction once the prefixes are known.
The subproblem ➔ DP[i] = {the sum of the elements in a[1..i]} for 0≤i≤n:
DP[i]={0DP[i−1]+aii=0otherwise
The answer ➔ 0≤i<j≤nmax(DP[j]−DP[i]); the strict i<j is what keeps the subarray non-empty.
Cost ➔ Θ(n) to build the table +Θ(n2) intervals at O(1) each =Θ(n2). Naively this is Θ(n3) — recomputing each interval sum by a scan — so the prefix table alone removes a whole factor of n.
Reuse this table everywhere ➔ the ferry problem needs ∑j<iℓj in O(1), and text justification needs the running line length; both precompute prefix sums exactly like this ➔ Dynamic Programming §5.
2. Anchoring the Right End — the Θ(n) Solution (applied P6b)
The greedy observation ➔ looking at index j, if the best sum finishing at j−1 is positive it is worth extending; if it is ≤0 it can only hurt, so restart at aj.
The subproblem must force aj in ➔ DP'[j] = {the maximum sum of a subarray ENDING at position j}, i.e. 1≤i≤jmaxk=i∑jak — aj is a member by construction.
The answer is a scan ➔ 1≤j≤nmaxDP′[j], not DP′[n] — the anchor forces it, exactly as in LIS.
Cost ➔ n subproblems at O(1)=Θ(n) time.
Θ(1) auxiliary space ➔ each subproblem reads only its predecessor, so keep prev and best in two variables and never build the array. That space-saving trick applies to any DP whose recurrence looks back a constant distance.
Where the greed lives ➔ this is DP with a greedy element bolted on: the restart decision is made locally and never revisited, which is legal here because a non-positive prefix provably cannot help any later subarray ➔ Greedy Algorithm §2.
3. The 2-D Generalisation — Maximum Sum Submatrix (applied P14, [D])
O(n4) by 2-D prefix sums ➔ DP[i][j] = {the sum of the submatrix A[1..i][1..j]}, built by inclusion–exclusion:
DP[i][j]={0A[i][j]+DP[i−1][j]+DP[i][j−1]−DP[i−1][j−1]i=0 or j=0otherwise
Why the subtraction ➔ the two rectangles [1..i−1][1..j] and [1..i][1..j−1] overlap in [1..i−1][1..j−1], which would otherwise be counted twice. Draw the four rectangles — the picture is the derivation.
Any submatrix in O(1) ➔ sum(A[i..j][k..l])=DP[j][l]−DP[i−1][l]−DP[j][k−1]+DP[i−1][k−1]; there are O(n4) submatrices ⟹ Θ(n2)+O(n4)=O(n4).
O(n3) by reduction to the 1-D case ➔ a maximum-sum submatrix starts at some row i and has some height h. Fix both, compress rows i..i+h−1 into a single row of column sums, and run §2’s Θ(n) algorithm on it.
The compression is incremental ➔ growing the height by one costs O(n)(add the next row to the previous compressed row), so the whole sweep is O(n) starting rows ×O(n) heights ×O(n) scan =O(n3).
The pattern generalises ➔ the k-D problem compresses to (k−1)-D, giving O(n2k−1): O(n) in 1-D, O(n3) in 2-D, O(n5) in 3-D.
⚙️ Core Implementation
🔹 Θ(n), Θ(1) auxiliary, with the index range
Code
def max_subarray(a): # a[0..n-1], non-empty subarray required best = a[0] # NOT 0 - handles the all-negative case best_lo = best_hi = 0 cur = a[0] # DP'[j] for the current j cur_lo = 0 for j in range(1, len(a)): if cur > 0: cur = cur + a[j] # extending still pays else: cur = a[j] # restart: the prefix can only hurt cur_lo = j if cur > best: # SCAN - the answer is not DP'[n-1] best = cur best_lo, best_hi = cur_lo, j return best, best_lo, best_hi
💡 Common Mistake:best = 0 and cur = 0 ➔ on an all-negative array this returns the empty subarray. The spec says non-empty, so seed both from a[0].
💡 Common Mistake:Testing cur >= 0 ➔ a zero prefix is neutral; the sheet’s rule is DP'[j-1] > 0, and a non-strict test just lengthens the reported range without changing the sum. Harmless for the value, wrong for the indices.
🔭 Beyond the lecture(not in the slides): §2 is commonly called Kadane's algorithm; the applied sheet derives it from scratch and does not name it, so derive it rather than citing it.
⚖️ Complexity
Approach
Time
Auxiliary space
Answer location
Naive all-intervals, sum by scan
Θ(n3)
Θ(1)
—
Prefix sums + all intervals (P6a)
Θ(n2)
Θ(n)
max(DP[j]−DP[i]), i<j
Anchored right end (P6b)
Θ(n)
Θ(1)
maxjDP′[j] — scan
Submatrix, 2-D prefix sums (P14.1)
O(n4)
Θ(n2)
max over all O(n4) rectangles
Submatrix, row compression + 1-D (P14.2)
O(n3)
Θ(n)
max over start row × height
k-D generalisation
O(n2k−1)
Θ(nk−1)
recursive compression
All cases coincide ➔ every version has fixed loop bounds and no early exit, so best = average = worst.
📊 Exam Execution Trace & Applied Exercises
Manual Execution Trace
a=[−2,1,−3,4,−1,2,1,−5,4].
j
aj
DP′[j−1]
>0?
Rule fired
DP′[j]
best so far
1
−2
0
no
restart
−2
−2
2
1
−2
no
restart
1
1
3
−3
1
yes
extend
−2
1
4
4
−2
no
restart
4
4
5
−1
4
yes
extend
3
4
6
2
3
yes
extend
5
5
7
1
5
yes
extend
6
6
8
−5
6
yes
extend
1
6
9
4
1
yes
extend
5
6
Final Extracted Output: maximum sum 6, achieved by a[4..7]=[4,−1,2,1](the restart at j=4 fixes the left end).
Read the trap ➔ DP′[9]=5=6: the anchored subproblem means the last cell is not the answer. The running best is mandatory.
Read the left endpoint ➔ it is the position of the most recent restart, which is why cur_lo is updated only in the else branch.
Read the negative case ➔ on [−3,−1,−7] every step restarts, DP′=[−3,−1,−7] and the answer is −1 — the largest single element, exactly as the spec demands.
Applied Exercise — from Θ(n2) to Θ(n) in one sentence (P6)
Problem: name the change of subproblem that removes the factor of n, and justify the greedy step.
P6a: DP[i]P6b: DP′[j]Greedy step: =sum of a[1..i]=best sum ending at jDP′[j−1]≤0⇒∀i≤j−1,∑k=ijak≤aj⇒answer needs a pair(i,j)⇒O(n2)combinations⇒answer needs a singlej⇒O(n)subproblems⇒restarting loses nothing.
Final Extracted Output:Θ(n) time, Θ(1) auxiliary. The reusable move (LO1): when the answer is indexed by a pair, try re-defining the subproblem so one of the two indices is implied — here, by forcing aj to be the last element.
⚠️ Common Mistakes
💡 Initialising the maximum to 0 ➔ silently allows the empty subarray and returns 0 on all-negative input.
💡 Answer =DP′[n] ➔ anchored subproblem ⟹ scan. Same failure mode as LIS.
💡 Quoting Θ(n) space for the fast version ➔ the recurrence looks back one cell, so it is Θ(1) auxiliary; quoting the array you did not need is a lost mark.
💡 Forgetting the −DP[i−1][k−1] term in 2-D ➔ inclusion–exclusion has four terms; three of them is the classic off-by-a-rectangle.
💡 Calling the O(n3) submatrix method “just Kadane in 2-D” ➔ the work is the row compression(and its O(n) incremental update); the 1-D routine is the black box being reused.
🧠 Active Recall
The Θ(n2) version already computes every interval in O(1). Where does the remaining n go, and how is it removed?
Answer
Short answer: it is in the pair(i,j) — O(n2) intervals to try. Re-anchoring the subproblem at its right end makes i implicit, leaving O(n) subproblems.
Why:Prefix sums fix the cost per interval, not the number of intervals ➔ they take Θ(n3) down to Θ(n2) and stop there. Forcing aj into the subarray removes the left index ➔ DP′[j] is a single-parameter subproblem, and the left endpoint is recovered later from where the restart happened. The greedy step is what makes it O(1) per subproblem ➔ a non-positive best-ending-at-(j−1) can never help, so exactly one comparison decides extend-vs-restart ➔ Dynamic Programming §5.
You have an O(n) maximum-subarray routine. Get the maximum-sum submatrix of an n×n matrix in O(n3).
Hint: what does the answer’s shape let you fix?
Answer
Short answer: fix the start row and the height, compress those rows into one row of column sums, and run the 1-D routine on it.
Why:The answer has a top and a bottom ➔ O(n) choices of start row ×O(n) choices of height covers every possible vertical extent, and the remaining freedom is purely horizontal — which is exactly the 1-D problem. Compression is incremental ➔ growing the height by one adds the next row to the running compressed row in O(n), so no height is recomputed from scratch. Multiply the three factors ➔ O(n)⋅O(n)⋅O(n)=O(n3), beating the O(n4) prefix-sum enumeration, and the same “compress one dimension away” trick gives O(n2k−1) in k dimensions.