Context:FIT2004_MOC · the credit-level classical DP problem, and the vault’s standing counterexample to “the answer is the last cell”Parent Framework:Dynamic Programming
Quick Revision
🎯 Objective:DP[i]= the length of the longest increasing subsequence ending at position i ➔ answer =maxiDP[i], notDP[n].
📦 Core Components:n subproblems ➔ each scans all earlier positions O(n) ⟹ Θ(n2) time, Θ(n) space.
⚡ Key Constraint: the subproblem must be anchored — “ending at i” — because a free-floating “LIS of a[1..i]” cannot be extended, since it does not say what the last element is.
📝 How It Works
1. Why the Subproblem Must Be Anchored
The observation ➔ take an LIS aj1,…,ajk; every prefix of it aj1,…,ajk−1 is itself a longest increasing subsequence among those ending at position jk−1.
The proof shape ➔ if it were not longest, swapping in the longer one would give a longer LIS overall — contradiction. That is the optimal-substructure argument, and it is what forces the anchor.
What the anchor buys ➔ knowing the last element lets you test extendability with a single comparison a[j]<a[i]; without it there is nothing to compare against.
What the anchor costs ➔ the answer is no longer a single cell. DP[n] is the LIS ending at the last element, and the true LIS need not include it ➔ scan the table.
2. The Recurrence
DP[i]=1+⎩⎨⎧01≤j<ia[j]<a[i]maxDP[j]a[i]≤a[j]for all j<iotherwise
Read it aloud ➔ “element i itself, plus the best chain I can hang it onto.”
The base is inside the general case ➔ the first clause is the edge case: no smaller predecessor exists, so DP[i]=1(the singleton subsequence). Writing it as DP[1]=1 only is wrong — any i can be a minimum-so-far.
Strict <, not ≤ ➔ the problem says strictly increasing; ≤ silently solves the non-decreasing problem and duplicates get chained.
Answer ➔ 1≤i≤nmaxDP[i].
3. Complexity, and Where Each Factor Comes From
Subproblems ➔ n, one per position ⟹ auxiliary space Θ(n)(1-D MEMO ➔ Dynamic Programming §5).
Work per subproblem ➔ the inner max scans every j<i ⟹ O(n).
Total ➔ n×O(n)=Θ(n2) time. The final max scan adds Θ(n) and is dominated.
No best/worst split ➔ both loops are input-independent, so best = average = worst =Θ(n2); only the reconstruction length varies.
4. Reconstruction — Which Elements, Not Just How Many
Store the argmax ➔ pred[i] = the j that achieved the maximum, None when the edge case fired.
Start at the argmax of the whole table, not at n — the single most common LIS bug.
Ties give different, equally valid answers ➔ state your tie-break rule (e.g. smallest j) before hand-tracing, exactly as for a MST trace.
5. LIS in Disguise — the Box-Stacking Reduction (applied P17)
Problem ➔ n box types, each rectangular; a box may sit on another iff both base dimensions are strictly smaller; boxes may be rotated and reused. Maximise tower height.
Kill the rotation first ➔ make six copies of every box, one per orientation ⟹ 6n boxes and no rotation logic. The constant 6 vanishes into the bound.
Reuse becomes impossible for free ➔ base dimensions must strictly decrease going up, so no box type can recur; the copies make that automatic.
The recurrence is LIS with a 2-D order
DP[i]=hi+⎩⎨⎧01≤j≤6nwj<wi,lj<limaxDP[j]no box j with wj<wi and lj<liotherwise
Same shape, three substitutions ➔ “length +1” becomes “height +hi”; the scalar test a[j]<a[i] becomes the pair test wj<wi∧lj<li; the answer is still maxiDP[i], never DP[6n].
Cost ➔ O(n) subproblems ×O(n) scan =Θ(n2).
The transferable move (LO1) ➔ longest chain under any strict partial order is LIS; recognising the order is the whole problem.
⚙️ Core Implementation
🔹 LIS with reconstruction
Code
def lis(a): # a[0..n-1], strictly increasing n = len(a) DP = [1] * n # edge case: every element alone pred = [None] * n for i in range(1, n): for j in range(0, i): # O(n) work per subproblem if a[j] < a[i] and DP[j] + 1 > DP[i]: DP[i] = DP[j] + 1 pred[i] = j best = 0 # SCAN - the answer is not DP[n-1] for i in range(1, n): if DP[i] > DP[best]: best = i seq = [] # backtrack, then reverse k = best while k is not None: seq.append(a[k]) k = pred[k] lo, hi = 0, len(seq) - 1 while lo < hi: seq[lo], seq[hi] = seq[hi], seq[lo] lo += 1 hi -= 1 return DP[best], seq
💡 Common Mistake:Returning DP[n-1] ➔ that is the LIS ending at the last element. On {1,2,3,0} it returns 1 instead of 3.
💡 Common Mistake:Using a[j] <= a[i] ➔ solves the non-decreasing problem; on {5,5,5} it answers 3 where the strict answer is 1.
🔭 Beyond the lecture(not in the slides): LIS also has an O(nlogn) solution that keeps the smallest possible tail for each achievable length and places each element by binary search. FIT2004's applied sheet asks for O(n2); do not offer the faster one as the unit's method.
⚖️ Complexity
Case
Time
Auxiliary space
Trigger
Best = Average = Worst
Θ(n2)
Θ(n)
two nested loops, no early exit — input-independent
Reconstruction pass
O(n)
Θ(n) for pred
the chain is at most n long
Box stacking (§5)
Θ(n2)
Θ(n)
6n boxes ⟹ the 6 is a constant
📊 Exam Execution Trace & Applied Exercises
Manual Execution Trace
a={0,8,4,12,2,10,6,14,1,9,5,13,3,11,7,15}(the applied sheet’s instance). Tie-break: smallest j.
i
ai
Candidates j with aj<ai(best DP[j])
DP[i]
pred[i]
1
0
—
1
—
2
8
1:0(1)
2
1
3
4
1:0(1)
2
1
4
12
2:8(2)
3
2
5
2
1:0(1)
2
1
6
10
2:8(2)
3
2
7
6
3:4(2)
3
3
8
14
4:12(3)
4
4
9
1
1:0(1)
2
1
10
9
7:6(3)
4
7
11
5
3:4(2)
3
3
12
13
10:9(4)
5
10
13
3
5:2(2)
3
5
14
11
10:9(4)
5
10
15
7
7:6(3)
4
7
16
15
12:13(5)
6
12
Final Extracted Output:maxiDP[i]=6 at i=16; backtracking 16→12→10→7→5→1 yields {0,2,6,9,13,15}. The sheet’s {0,2,6,9,11,15} is equally valid — the tie at i=16 between j=12(13) and j=14(11) is broken by the stated rule, and both give length 6.
Read the trap ➔ DP[13]=3 while DP[12]=5: the table is not monotone, so the final max scan is mandatory. Here the argmax happens to land on i=n; do not let that coincidence become a habit.
Read the shape ➔ every row is “my own element, plus the tallest chain among strictly smaller earlier elements” — the same sentence as box stacking with the order swapped.
Applied Exercise — turn a stacking problem into LIS (P17)
Problem: show that 6 copies per box type make the “no reuse” rule automatic.
Stacking rule⇒⇒⇒wabove<wbelowandlabove<lbelowthe base dimensions are strictly decreasing up the towerno orientation can appear twice, so "as many as you like" is unreachable.
Final Extracted Output: the 6n-copy formulation needs no reuse bookkeeping, and Θ((6n)2)=Θ(n2). The reusable move: enumerate the finitely many configurations of an object up front, and a constraint that looked stateful becomes a property of the ordering.
⚠️ Common Mistakes
💡 Answer =DP[n] ➔ the anchored subproblem forces a max scan; this is the single most-marked LIS error and the wrap-up flags it explicitly.
💡 Defining DP[i] as “the LIS of a[1..i]” ➔ unanchored, so the recurrence cannot test extendability and the definition is unusable. Fix the definition, not the code.
💡 Non-strict comparison ➔ answers a different problem; state strictly increasing in the subproblem sentence.
💡 Quoting O(n) space as O(n2) ➔ the MEMO is 1-D; the n2 is the inner scan, i.e. time ➔ Dynamic Programming §5.
💡 Reconstructing from DP alone without pred ➔ possible but fiddly; store the argmax, it is Θ(n) and free within the existing bound.
🧠 Active Recall
Why must the LIS subproblem be "ending at i", and what does that cost you when reading the answer?
Answer
Short answer: the anchor names the last element, which is the only thing that makes a chain extendable; the cost is that the answer becomes maxiDP[i] instead of a fixed cell.
Why:Optimal substructure needs the prefix to be optimal for its endpoint ➔ a prefix of an LIS is a longest increasing subsequence ending at that earlier element, else it could be swapped for a longer one. Extendability is a comparison ➔ DP[i] can build on DP[j] only when a[j]<a[i], which requires knowing a[j] is the chain’s last element. The unanchored definition is not wrong, it is unusable ➔ “the LIS of a[1..i]” gives no way to decide whether a[i] can be appended.
You are given boxes with widths, lengths and heights and asked for the tallest legal tower. Reduce it.
Hint: what is being made longest, and under which order?
Answer
Short answer: it is LIS with the scalar order replaced by strict dominance on (w,l) and the +1 replaced by +hi; make 6 oriented copies of each box first.
Why:The rotation is finite ➔ six orientations per box, so 6n items and the recurrence never mentions rotation ➔ the constant is absorbed. Reuse solves itself ➔ dimensions strictly decrease up the tower, so no orientation recurs. The recurrence is unchanged in shape ➔ DP[i]=hi+max{DP[j]:wj<wi,lj<li}, answer maxiDP[i], cost Θ(n2) — recognising longest chain under a strict partial order is the LO1 skill being examined.