INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION
01
00/16
01 / COVER STEP 16 · DYNAMIC PROGRAMMING
INVARIANT · STEP 16 · DECK 4 OF 4
CHOOSE THE CUT

Two halves, and the second is the only genuinely new idea in the whole of step 16. LIS is still pick / not-pick — you just carry which element you last took, and one lecture replaces the whole table with a binary search. Partition DP is different in kind: you stop choosing what to take and start choosing where to cut, then solve both sides and combine. Every remaining hard DP an interviewer asks is one of these two.

16Problems
3Families
16Units
4Live benches
← → ↑ ↓  or  W A S D  navigate SPACE next   DOUBLE-CLICK to advance I index   G goto problem   P predict H hide solutions   T close video   F fullscreen
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
02 / START HERE READ THIS ONE FIRST

HOW THIS DECK WORKS

This is not a list of problems. It is 16 learning units, and every one runs the same four beats. Go in order and the problems stop feeling random.

ASSUMEDDecks I to III of this step. The LIS half is pick / not-pick with the PREVIOUS choice carried; the partition half is the one genuinely new idea in step 16 — you choose a split point rather than an element, and both sides recurse.

01 INTROWhat the concept is, and what to watch for
02 VIDEOThe lecture, full-width in theatre mode
03 DRILLS2–4 questions checking the lecture landed
04 PROBLEMSThe sheet problems that concept unlocks

Moving around

← ↑Back a slide — or A / W → ↓Forward — or D / S / Space 2×clickDouble-click the right side to advance, left to go back. A single click never moves the deck. IThe index: every problem, clickable, with your progress GJump straight to a problem by its number FFullscreen

While you study

HHide solutions — blurs code and steps so you try first PPredict mode: call the next step before the animation plays it TClose the video — Esc works too ☐ ★Mark solved, or star to revisit. Both are saved automatically.

16 PROBLEMS · EVERY ONE LINKS TO A JUDGE TO SOLVE

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
03 / SIGNALS WHEN YOU SEE X, REACH FOR Y

AN ELEMENT, OR A CUT

Seven of these rows are pick / not-pick with the previous choice carried. The other nine stop choosing elements altogether and choose a split point instead. Reading which of the two a statement is asking for, before writing anything, is what this whole deck is for.

“LONGEST INCREASING / DIVISIBLE / CHAIN”

pick / not-pick where the pick is conditional on what you last took

LIS · state carries the previous indexO(n²)
LIS AGAIN, BUT n IS 10⁵

the O(n²) table will not fit, so keep the smallest tail per length and binary search it

PATIENCE · tails array, O(n log n)O(n log n)
“PARENTHESISE”, “MULTIPLY IN SOME ORDER”, “BURST”

no element to pick — you choose a SPLIT, recurse both sides and combine at the cut

PARTITION DP · loop over every k in (i, j)O(n³)
“PARTITION THE STRING / ARRAY INTO PIECES”

take a valid prefix, pay for it, recurse on the remainder — one index, not two

FRONT PARTITION · O(n²)O(n²)
AN OPERATION THAT CHANGES ITS NEIGHBOURS

ask what happens LAST rather than first, so the sides stay independent

REVERSE THE QUESTION · burst balloonsO(n³)
A BINARY MATRIX AND “SQUARES” OR “RECTANGLE”

build each cell from the neighbours already computed, or carry heights row by row

GRID DP · min of three, or a histogram + stackO(n·m)
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
04 / CONSTRAINTS BEFORE YOU WRITE A LINE

READ THE ANSWER OFF THE CONSTRAINTS

Partition DP is O(n³) — n² ranges, each looping over n cut points — so it only survives while n is around a hundred. That is the tightest bound in the whole of step 16, and a small n beside a parenthesisation problem is itself a strong hint about which pattern is wanted.

n ≤
BUDGET
WHAT THAT BUYS YOU
n ≤ 10²
O(n³)
partition DP: n² ranges, each looping over n cut points. MCM, burst balloons, cut the stick
n ≤ 10³
O(n²)
the LIS table, and every front-partition problem
n ≤ 10⁵
O(n log n)
the n² table is impossible — LIS by binary search on tails is the only way up
n·m ≤ 10⁶
O(n·m)
the two grid rows: one pass per cell, with a stack riding along
n ≤ 20
O(2ⁿ)
small enough to enumerate — and the signal for bitmask DP, which is beyond this sheet

The LIS half is the opposite story: at n = 10⁵ the O(n²) table is impossible and the binary-search form is the only way through — which is why the sheet gives it a row.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
05 / WARMUP BEFORE ANY OF IT · 1 OF 2

AN ELEMENT, OR A CUT

DRILL 01 · RECALL

Every DP in the first three decks chose an ELEMENT — take it or skip it. What does partition DP choose instead?

A cut, not an element. That is the one genuinely new idea in step 16, and it is why the state becomes a RANGE (i, j) rather than a position. The loop over cut points inside a two-index state is the pattern's signature — see it and you know what you are looking at.

DRILL 02 · RECALL

LIS carries the previous index in its state, giving n × (n+1) states. The tabulated O(n²) form drops that. What does dp[i] mean there?

Ending exactly at i. That reformulation is what removes the second index — and it is why the answer is the MAXIMUM over the array rather than its last cell. The same 'the answer is not the corner' shape as longest common substring in deck III.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
06 / WARMUP BEFORE ANY OF IT · 2 OF 2

AN ELEMENT, OR A CUT

DRILL 01 · BUG

Matrix chain multiplication. It runs and returns a number that is too small. Which line?

int f(int i, int j, vector<int>& d, vector<vector<int>>& dp){
    if(i == j) return 0;
    if(dp[i][j] != -1) return dp[i][j];
    int best = INT_MAX;
    for(int k = i; k < j; k++){
        int cost = f(i, k, d, dp) + f(k+1, j, d, dp);
        best = min(best, cost);
    }
    return dp[i][j] = best;
}

The combine term is missing. Splitting is free in this code, so the answer is the sum of the sub-costs and nothing else — always too small, always a plausible number, and the recursion structure looks perfect. d[i-1]*d[k]*d[j] is what pays for joining the halves.

DRILL 02 · RECALL

Burst Balloons cannot be solved by asking which balloon to burst FIRST. Why not?

The sides stop being subproblems. Burst a middle balloon and its left and right neighbours become adjacent, so neither half can be solved alone. Asking which is burst LAST fixes it: its neighbours are then the range's fixed boundaries, and the halves are independent again.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
07 / ROADMAP WHAT YOU ARE ABOUT TO DO

THE WHOLE RUN, 16 UNITS

Sixteen lectures, 6h 06m, sixteen sheet rows. Units 01 to 07 are LIS — one table, one predicate swapped at a time, and one lecture that throws the table away entirely. Units 08 to 14 are partition DP, the one genuinely new idea in step 16. Units 15 and 16 close on two grid problems, the last deliberately easy.

UNIT 01

CARRY THE PREVIOUS

▶ 24:353 DRILLS1 PROBLEM
UNIT 02

PRINT IT

▶ 25:573 DRILLS1 PROBLEM
UNIT 03

THROW THE TABLE AWAY

▶ 16:273 DRILLS1 PROBLEM
UNIT 04

SORT, THEN LIS

▶ 14:393 DRILLS1 PROBLEM
UNIT 05

A PREDICATE ON STRINGS

▶ 16:573 DRILLS1 PROBLEM
UNIT 06

TWO DIRECTIONS

▶ 13:523 DRILLS1 PROBLEM
UNIT 07

COUNT THE LONGEST

▶ 20:463 DRILLS1 PROBLEM
UNIT 08

PARTITION DP BEGINS

▶ 53:414 DRILLS1 PROBLEM
UNIT 09

TABULATING BACKWARDS

▶ 9:083 DRILLS1 PROBLEM
UNIT 10

SENTINELS AT THE ENDS

▶ 30:023 DRILLS1 PROBLEM
UNIT 11

THINK IN REVERSE

▶ 34:003 DRILLS1 PROBLEM
UNIT 12

COUNT THE PARENTHESISATIONS

▶ 34:553 DRILLS1 PROBLEM
UNIT 13

FRONT PARTITION

▶ 23:173 DRILLS1 PROBLEM
UNIT 14

BOUNDED FRONT PARTITION

▶ 21:393 DRILLS1 PROBLEM
UNIT 15

STACK A HISTOGRAM

▶ 11:043 DRILLS1 PROBLEM
UNIT 16

THREE NEIGHBOURS, ONE MIN

▶ 15:593 DRILLS1 PROBLEM
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
08 / INDEX PRESS I FROM ANYWHERE

INDEX — ALL 16 PROBLEMS

Click any problem to jump straight to it. Solved ones turn green. Press I from anywhere to come back here. The number is the sheet's row, not your position — the deck teaches in the order that builds, so you will meet them slightly out of numerical sequence.

DP on LIS · 07
MCM DP | Partition DP · 07
DP on Squares · 02
SOLVED HAS A JUDGE LINK
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
09 / INTRO UNIT 01 · CARRY THE PREVIOUS

UNIT 01 — CARRY THE PREVIOUS

Pick / not-pick where the pick depends on what you last took

THE QUESTION THIS LECTURE ANSWERS

WHAT IS THE LONGEST RUN OF INCREASING VALUES YOU CAN PULL OUT, IN ORDER?

subsequenceprevious indexending-at reformulation
WHAT TO WATCH FOR
  • 01THE PICK IS CONDITIONAL, SO THE STATE CARRIES THE PREVIOUS INDEX
  • 02ORDER IS PRESERVED — [5, 2] IS NOT A SUBSEQUENCE OF [9, 2, 5]
  • 03dp[i] MEANS THE RUN ENDING AT i, WHICH IS WHY THE ANSWER IS THE MAXIMUM
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
10 / VIDEO UNIT 01 · CARRY THE PREVIOUS

DP 41 · Longest Increasing Subsequence

STRIVER A2Z
Carrying the previous index · the O(n²) reformulation · where the answer lives
RUNTIME 24:35
AFTER THIS → 3 DRILLS · PROBLEM #01
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
11 / DRILL UNIT 01 · CARRY THE PREVIOUS · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

Why does the state need a second index beyond the current position?

Pick / not-pick again, but the pick branch is CONDITIONAL — you may only take a[i] if it exceeds what you last took. So the state carries the previous index, which is why the classic formulation is f(i, prev) over n × (n+1) states.

DRILL 02 · RECALL

The lecture stresses what a subsequence is NOT. Which of these is not one of [9, 2, 5]?

Order is preserved in a subsequence; only skipping is allowed. He says it explicitly — 'five and two have swapped places, thereby it's not a subsequence'. That constraint is what makes the previous element the right thing to carry.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
12 / DRILL UNIT 01 · CARRY THE PREVIOUS · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

The O(n²) tabulated form uses dp[i] = the LIS ENDING at i. How is the answer read off?

A cell holds the best subsequence ending exactly there, and the best one may end anywhere — the same 'answer is not the corner' shape as longest common substring in deck III. Reading dp[n-1] gives only the subsequences forced to include the last element.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
13 / MECHANISM UNIT 01 · LIS · CODE MIRRORED

CARRY WHAT YOU LAST TOOK

[10, 9, 2, 5, 3, 7, 101, 18]. Each cell is the longest increasing run ending exactly there, built by looking back at every smaller element before it. Watch which earlier cell each answer extends, and note the answer is the largest cell, not the last one.

RECURRENCE
STATE
CODE MIRROR
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
14 / PROBLEM #01 · LIS · MED

Longest Increasing Subsequence

MED lis ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Longest increasing subsequence”. Pick / not-pick again — but whether you MAY pick depends on what you last took, which is what puts a second thing in the state.

INTUITION

Reformulate: let dp[i] be the longest increasing run ENDING exactly at i. Then every earlier element smaller than a[i] offers a chain to extend, and you take the best. That reformulation is what removes the 'previous index' from the state — and it is why the answer is the maximum over the array rather than its last cell.

STEPS
  1. State: dp[i] = longest increasing subsequence ending at index i.
  2. Initialise every dp[i] to 1 — each element is a chain by itself.
  3. For each i, scan every j < i with a[j] < a[i] and take max(dp[i], dp[j] + 1).
  4. The answer is the maximum over dp, not dp[n−1].
BRUTEO(2ⁿ)
OPTIMALO(n²)
↕ SCROLL
int lengthOfLIS(vector<int>& a) {
    int n = a.size();
    vector<int> dp(n, 1);                 // every element is a chain of one
    for (int i = 0; i < n; i++)
        for (int j = 0; j < i; j++)
            if (a[j] < a[i])              // may this chain be extended?
                dp[i] = max(dp[i], dp[j] + 1);
    return *max_element(dp.begin(), dp.end());   // the MAX, never dp[n-1]
}
TIMEO(n²)every pair of indices compared
SPACEO(n)one array of n
TRAP

Returning dp[n−1]. Each cell is the run ENDING there, so that answers 'the longest run that must include the last element' — a smaller number that happens to be right whenever the array ends on its own best chain.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
15 / INTRO UNIT 02 · PRINT IT

UNIT 02 — PRINT IT

One extra integer per cell buys the whole sequence back

THE QUESTION THIS LECTURE ANSWERS

THE LENGTH IS EASY. HOW DO YOU RECOVER THE SUBSEQUENCE ITSELF?

parent arrayargmaxreconstruction
WHAT TO WATCH FOR
  • 01RECORD WHICH j EACH i EXTENDED — ONE INTEGER, NOT A STRING
  • 02THE WALK STARTS AT THE INDEX HOLDING THE MAXIMUM, NOT AT n−1
  • 03YOU WALK BACKWARDS, SO REVERSE WHAT YOU COLLECT
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
16 / VIDEO UNIT 02 · PRINT IT

DP 42 · Printing Longest Increasing Subsequence

STRIVER A2Z
The parent array · starting the walk at the argmax · reversing at the end
RUNTIME 25:57
AFTER THIS → 3 DRILLS · PROBLEM #02
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
17 / DRILL UNIT 02 · PRINT IT · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

To reconstruct the actual subsequence, what does the O(n²) fill also record?

A parent array — one extra integer per cell. Then walk it backwards from the index holding the maximum and reverse. Storing whole subsequences in cells is O(n²) memory for information one integer already implies.

DRILL 02 · RECALL

Where does the backward walk START?

The chain ends where dp is largest, not at the array's end and not at the array's largest element. Track that index while filling; starting anywhere else reconstructs a shorter chain and prints it with total confidence.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
18 / DRILL UNIT 02 · PRINT IT · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

Why can the binary-search O(n log n) version NOT print the subsequence directly?

The tails array is the right LENGTH but its contents may never have appeared together as a subsequence. Reconstructing from it needs a separate parent array anyway — which is why the printing lecture stays on the O(n²) table.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
19 / PROBLEM #02 · LIS · MED

Print Longest Increasing Subsequence

MED lis ▶ SOLVE ON GEEKSFORGEEKS
SIGNAL — WHAT GIVES IT AWAY

“Print” or “return the subsequence”. The table holds the length; one extra integer per cell holds the route.

INTUITION

While filling, record which earlier index each cell extended. That parent array is a linked list running backwards through the best chain — walk it from the index holding the maximum and reverse. Storing whole subsequences in cells costs O(n²) memory for what one integer already implies.

STEPS
  1. Fill dp as in #01, and alongside it a parent array.
  2. Initialise parent[i] = i, meaning the chain starts here.
  3. When dp[i] improves via j, set parent[i] = j.
  4. Track the index holding the maximum as you go — the ARGMAX.
  5. Walk parents back from it until parent[at] == at, then reverse.
BRUTEO(2ⁿ)
OPTIMALO(n²)
↕ SCROLL
vector<int> printLIS(vector<int>& a) {
    int n = a.size(), best = 0, at = 0;
    vector<int> dp(n, 1), parent(n);
    for (int i = 0; i < n; i++) {
        parent[i] = i;                    // its own parent = chain starts here
        for (int j = 0; j < i; j++)
            if (a[j] < a[i] && dp[j] + 1 > dp[i]) {
                dp[i] = dp[j] + 1;
                parent[i] = j;            // remember who we extended
            }
        if (dp[i] > best) { best = dp[i]; at = i; }   // track the ARGMAX
    }
    vector<int> out;
    while (parent[at] != at) { out.push_back(a[at]); at = parent[at]; }
    out.push_back(a[at]);
    reverse(out.begin(), out.end());
    return out;
}
TIMEO(n²)the same fill plus a linear walk
SPACEO(n)two arrays of n
TRAP

Starting the backward walk at n−1. The chain ends wherever dp is largest, which is usually not the array's end and is never guaranteed to be its largest VALUE either.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
20 / INTRO UNIT 03 · THROW THE TABLE AWAY

UNIT 03 — THROW THE TABLE AWAY

Replacing the whole table with a binary search

THE QUESTION THIS LECTURE ANSWERS

n IS 10⁵ AND THE O(n²) TABLE IS IMPOSSIBLE. WHAT REPLACES IT?

tails arraypatience sortingbinary search
WHAT TO WATCH FOR
  • 01EACH CELL IS THE SMALLEST POSSIBLE TAIL OF A RUN OF THAT LENGTH
  • 02SMALLER IS NEVER WORSE — WHICH IS WHY THE ARRAY STAYS SORTED
  • 03A VALUE EITHER APPENDS OR OVERWRITES; ONLY APPENDING GROWS THE ANSWER
  • 04THE LENGTH IS THE ANSWER. THE CONTENTS ARE NOT THE SUBSEQUENCE
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
21 / VIDEO UNIT 03 · THROW THE TABLE AWAY

DP 43 · Longest Increasing Subsequence with Binary Search

STRIVER A2Z
The tails array · why smallest tails · append vs overwrite · what it is NOT
RUNTIME 16:27
AFTER THIS → 3 DRILLS · PROBLEM #03
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
22 / DRILL UNIT 03 · THROW THE TABLE AWAY · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

What does each cell of the maintained array actually hold?

The SMALLEST tail, because a smaller tail is never worse — anything that can extend a larger tail can extend a smaller one. That is the whole invariant, and it is why the array stays sorted and binary search applies.

DRILL 02 · RECALL

A new element arrives that is not larger than every tail. What happens?

Binary search for the first tail not less than it and overwrite. The array's LENGTH does not change — you have improved a run of that length without lengthening anything. Appending here is the bug that inflates the answer.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
23 / DRILL UNIT 03 · THROW THE TABLE AWAY · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

The lecture warns the array is not the answer. What is it good for?

Overwriting tails means the array can end up holding elements that never formed a subsequence together. The length is always right; the contents are working state. Printing it is the classic misuse of this algorithm.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
24 / MECHANISM UNIT 03 · LISTAILS · CODE MIRRORED

THE TABLE, REPLACED BY A BINARY SEARCH

The same array, O(n log n). This row holds the smallest possible tail of an increasing run of each length, so it stays sorted and binary search applies. A value either APPENDS. The longest run grew, or overwrites a tail, improving a run without lengthening it. Its length is the answer; its contents are working state.

RECURRENCE
STATE
CODE MIRROR
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
25 / PROBLEM #03 · LIS · MED

Longest Increasing Subsequence |(DP-43)

MED lis ▶ SOLVE ON LEETCODEThe same LeetCode problem as row 1, deliberately. Row 1 solves it with an O(n²) table; this row throws the table away and replaces it with a binary search for O(n log n). Two algorithms, one problem — and the sheet keeps them as separate rows because the second is what a follow-up question asks for.
SIGNAL — WHAT GIVES IT AWAY

LIS again, but with n up to 10⁵. The O(n²) table is 10¹⁰ cells — so the constraint itself is telling you to throw the table away.

INTUITION

Keep an array where cell k holds the SMALLEST possible tail of an increasing run of length k+1. A smaller tail is never worse, because anything that can extend a larger one can extend it too. That invariant keeps the array sorted, which makes binary search legal — and each element either appends (the longest run grew) or overwrites a tail (a run of that length just got easier to extend).

STEPS
  1. Maintain an initially empty tails array.
  2. For each element, binary search for the first tail not less than it.
  3. If there is none, append — the longest run has grown.
  4. Otherwise overwrite that tail. The length does not change.
  5. Return the array's LENGTH. Its contents are not the subsequence.
BRUTEO(n²)
OPTIMALO(n log n)
↕ SCROLL
int lengthOfLIS(vector<int>& a) {
    vector<int> tails;                    // smallest tail per run length
    for (int x : a) {
        auto it = lower_bound(tails.begin(), tails.end(), x);
        if (it == tails.end()) tails.push_back(x);   // longer: APPEND
        else                   *it = x;              // improve a tail
    }
    return tails.size();     // the LENGTH. the contents are NOT the answer
}
TIMEO(n log n)one binary search per element
SPACEO(n)the tails array, at most n
TRAP

Printing the tails array as the answer. Overwriting means it can end up holding elements that never appeared together as a subsequence — the LENGTH is always right and the sequence is fiction.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
26 / INTRO UNIT 04 · SORT, THEN LIS

UNIT 04 — SORT, THEN LIS

The same table with the comparison swapped

THE QUESTION THIS LECTURE ANSWERS

LARGEST SUBSET WHERE EVERY PAIR DIVIDES. HOW IS THAT AN LIS PROBLEM?

transitivitypredicate swapsorted chain
WHAT TO WATCH FOR
  • 01SORT FIRST — THEN DIVISIBILITY IS TRANSITIVE ALONG THE CHAIN
  • 02SO YOU ONLY CHECK AGAINST THE CHAIN'S LAST MEMBER, NOT ALL OF IT
  • 03ONE PREDICATE SWAPPED: a[i] % a[j] == 0 REPLACES a[i] > a[j]
  • 04THE SUBSET IS ASKED FOR, SO THE PARENT WALK COMES BACK
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
27 / VIDEO UNIT 04 · SORT, THEN LIS

DP 44 · Largest Divisible Subset

STRIVER A2Z
Sorting for transitivity · divisibility instead of ordering · the same reconstruction
RUNTIME 14:39
AFTER THIS → 3 DRILLS · PROBLEM #04
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
28 / DRILL UNIT 04 · SORT, THEN LIS · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

Why does sorting the array first make this an LIS problem?

Once sorted, if a divides b and b divides c then a divides c — so checking each new element against only the CHAIN'S LAST member is enough. Unsorted, you would have to check it against every member, and the LIS shape would not apply.

DRILL 02 · RECALL

What replaces the 'is greater than' test of ordinary LIS?

One predicate swapped and everything else — the table, the parent array, the backward walk — is unchanged. Recognising that the LIS SHAPE is separable from the 'increasing' test is what makes the next two rows free.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
29 / DRILL UNIT 04 · SORT, THEN LIS · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

The problem asks for the SUBSET, not its size. What does that require?

The same reconstruction technique, reused verbatim on a different predicate. Two units in and the LIS machinery is already being applied rather than derived — which is the point of teaching the pattern rather than the problems.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
30 / PROBLEM #04 · LIS · MED

Largest Divisible Subset

MED lis ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Every pair in the subset divides.” A pairwise condition that becomes a chain condition the moment the array is sorted.

INTUITION

Sort first. Divisibility is then transitive along the chain — if a divides b and b divides c then a divides c — so checking each new element against only the chain's LAST member is enough. That is exactly LIS's structure with a different predicate, and the parent-array reconstruction comes along unchanged.

STEPS
  1. Sort the array ascending.
  2. Run the LIS fill with a[i] % a[j] == 0 in place of a[j] < a[i].
  3. Carry a parent array, as in #02.
  4. Walk back from the argmax to build the subset.
BRUTEO(2ⁿ · n)
OPTIMALO(n²)
↕ SCROLL
vector<int> largestDivisibleSubset(vector<int>& a) {
    sort(a.begin(), a.end());             // sorting makes divisibility transitive
    int n = a.size(), best = 0, at = 0;
    vector<int> dp(n, 1), parent(n);
    for (int i = 0; i < n; i++) {
        parent[i] = i;
        for (int j = 0; j < i; j++)
            if (a[i] % a[j] == 0 && dp[j] + 1 > dp[i]) {   // the swapped test
                dp[i] = dp[j] + 1;
                parent[i] = j;
            }
        if (dp[i] > best) { best = dp[i]; at = i; }
    }
    vector<int> out;
    while (parent[at] != at) { out.push_back(a[at]); at = parent[at]; }
    out.push_back(a[at]);
    return out;
}
TIMEO(n²)every pair tested after the sort
SPACEO(n)two arrays of n
TRAP

Skipping the sort. Without it, divisibility is not transitive along the chain and you would have to test each candidate against every existing member — the LIS shape simply does not apply, and the answer comes out too large.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
31 / INTRO UNIT 05 · A PREDICATE ON STRINGS

UNIT 05 — A PREDICATE ON STRINGS

The predicate becomes a two-pointer scan

THE QUESTION THIS LECTURE ANSWERS

WORDS CHAIN WHEN ONE INSERTION MAKES THE NEXT. WHAT IS THE LONGEST CHAIN?

string chainpredecessorpredicate cost
WHAT TO WATCH FOR
  • 01SORT BY LENGTH, SO A PREDECESSOR HAS ALWAYS BEEN PROCESSED ALREADY
  • 02THE CHECK IS ONE INSERTION, NOT ONE CHARACTER OF DIFFERENCE
  • 03“bda” IS ONE LONGER THAN “bad” AND IS NOT A SUCCESSOR
  • 04COST BECOMES O(n²·L) — THE SHAPE IS LIS, THE PREDICATE IS THE PRICE
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
32 / VIDEO UNIT 05 · A PREDICATE ON STRINGS

DP 45 · Longest String Chain

STRIVER A2Z
Sorting by length · the is-predecessor check · what the predicate costs
RUNTIME 16:57
AFTER THIS → 3 DRILLS · PROBLEM #05
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
33 / DRILL UNIT 05 · A PREDICATE ON STRINGS · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

The words must be sorted before the LIS runs. Sorted by what?

A predecessor is exactly one character shorter, so sorting by length guarantees it has already been processed. Sorting alphabetically breaks that and the chain is built from words that have not been considered yet.

DRILL 02 · RECALL

What is the predicate now?

Length alone is not enough — 'bda' is one longer than 'bad' but is not formed by an insertion, because the order must be preserved. The check is a two-pointer scan, and getting it wrong makes the chain too long.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
34 / DRILL UNIT 05 · A PREDICATE ON STRINGS · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

What does this cost compared with plain LIS?

O(n² · L), because each of the n² pairs now runs a two-pointer scan over the characters. Worth stating out loud in an interview: the SHAPE is LIS, and the cost of the predicate is what makes the complexity differ.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
35 / PROBLEM #05 · LIS · MED

Longest String Chain

MED lis ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

Words, and “one insertion makes the next”. A chain condition on strings — LIS again, with the comparison replaced by a scan.

INTUITION

Sort by LENGTH, so every candidate predecessor has already been processed. Then it is the LIS fill with 'is b formed by inserting one character into a' as the predicate. That check is a two-pointer walk, so each of the n² comparisons costs O(L) — the shape is LIS and the predicate is what sets the complexity.

STEPS
  1. Sort the words by length.
  2. For each pair (j, i) with j before i, test isPredecessor(w[j], w[i]).
  3. The test: lengths differ by one, and a two-pointer scan consumes all of the shorter word.
  4. Standard LIS fill on top of that predicate; return the maximum.
BRUTEO(2ⁿ · L)
OPTIMALO(n²·L)
↕ SCROLL
bool isPredecessor(string& a, string& b) {   // b is a plus one char
    if (b.size() != a.size() + 1) return false;
    int i = 0, j = 0;
    while (j < (int)b.size()) {
        if (i < (int)a.size() && a[i] == b[j]) i++;
        j++;
    }
    return i == (int)a.size();
}
int longestStrChain(vector<string>& w) {
    sort(w.begin(), w.end(), [](const string& x, const string& y){
        return x.size() < y.size(); });   // sort by LENGTH, not alphabetically
    int n = w.size(), best = 1;
    vector<int> dp(n, 1);
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < i; j++)
            if (isPredecessor(w[j], w[i]))
                dp[i] = max(dp[i], dp[j] + 1);
        best = max(best, dp[i]);
    }
    return best;
}
TIMEO(n²·L)n² pairs, each an O(L) scan
SPACEO(n)one array of n
TRAP

Testing only that the lengths differ by one. 'bda' is one longer than 'bad' and is not formed by an insertion — order must be preserved. The chain comes out longer than it should, on inputs that look fine.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
36 / INTRO UNIT 06 · TWO DIRECTIONS

UNIT 06 — TWO DIRECTIONS

Two LIS runs, joined at every candidate peak

THE QUESTION THIS LECTURE ANSWERS

RISE THEN FALL. HOW LONG CAN THAT BE?

bitonicpeakdouble counting
WHAT TO WATCH FOR
  • 01RUN LIS FORWARDS AND AGAIN BACKWARDS INTO TWO ARRAYS
  • 02EVERY INDEX IS A CANDIDATE PEAK: lis[i] + lds[i] − 1
  • 03SUBTRACT ONE — THE PEAK IS COUNTED IN BOTH ARRAYS
  • 04CHECK THE STATEMENT: SOME JUDGES ALLOW A PURELY RISING SEQUENCE
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
37 / VIDEO UNIT 06 · TWO DIRECTIONS

DP 46 · Longest Bitonic Subsequence

STRIVER A2Z
LIS forwards and backwards · joining at the peak · the off-by-one
RUNTIME 13:52
AFTER THIS → 3 DRILLS · PROBLEM #06
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
38 / DRILL UNIT 06 · TWO DIRECTIONS · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

A bitonic subsequence rises then falls. How is it computed?

Run the O(n²) LIS forwards into one array and backwards into another. Each index is then a candidate PEAK, and the bitonic length through it is the two values added minus one — the peak counted twice.

DRILL 02 · RECALL

Why subtract one when combining the two arrays at index i?

lis[i] includes a[i] and lds[i] includes a[i], so adding them double-counts the peak. An off-by-one that inflates every answer by exactly one — a wrong number that is right-shaped and passes casual inspection.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
39 / DRILL UNIT 06 · TWO DIRECTIONS · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

Must a bitonic subsequence have a strictly rising AND a strictly falling part?

Different judges differ, and the answer changes by more than one. GfG's version permits a monotonic sequence to count; some others require a genuine peak. This is a read-the-statement problem, and the deck flags it rather than picking for you.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
40 / PROBLEM #06 · LIS · MED

Longest Bitonic Subsequence

MED lis ▶ SOLVE ON GEEKSFORGEEKS
SIGNAL — WHAT GIVES IT AWAY

“Rises then falls.” Two monotonic halves meeting at a peak — so run the LIS you already have, twice, in opposite directions.

INTUITION

lis[i] is the longest rise ending at i; lds[i] is the longest fall starting at i. Treat every index as a candidate peak: the bitonic length through it is the two added, minus one because the peak itself is in both.

STEPS
  1. Fill lis[] left to right, exactly as in #01.
  2. Fill lds[] right to left, comparing in the same direction.
  3. For every i, take lis[i] + lds[i] − 1.
  4. Return the maximum.
  5. Check the statement: some judges require both halves non-empty.
BRUTEO(2ⁿ)
OPTIMALO(n²)
↕ SCROLL
int longestBitonicSubsequence(vector<int>& a) {
    int n = a.size();
    vector<int> lis(n, 1), lds(n, 1);
    for (int i = 0; i < n; i++)                    // rising, from the left
        for (int j = 0; j < i; j++)
            if (a[j] < a[i]) lis[i] = max(lis[i], lis[j] + 1);
    for (int i = n - 1; i >= 0; i--)               // falling, from the right
        for (int j = n - 1; j > i; j--)
            if (a[j] < a[i]) lds[i] = max(lds[i], lds[j] + 1);
    int best = 0;
    for (int i = 0; i < n; i++)
        best = max(best, lis[i] + lds[i] - 1);     // -1: the peak counts twice
    return best;
}
TIMEO(n²)two LIS fills
SPACEO(n)two arrays of n
TRAP

Forgetting the −1. The peak is counted in both arrays, so every answer comes out exactly one too large — right-shaped, monotonic in the input, and wrong on every single test case.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
41 / INTRO UNIT 07 · COUNT THE LONGEST

UNIT 07 — COUNT THE LONGEST

Counting alongside the optimisation

THE QUESTION THIS LECTURE ANSWERS

HOW MANY DIFFERENT SUBSEQUENCES ACHIEVE THE LONGEST LENGTH?

countingreplace vs addties
WHAT TO WATCH FOR
  • 01TWO ARRAYS WALKED TOGETHER: THE LENGTH AND THE COUNT
  • 02A LONGER CHAIN ARRIVES → cnt[i] IS REPLACED BY cnt[j]
  • 03AN EQUAL CHAIN ARRIVES → cnt[j] IS ADDED TO cnt[i]
  • 04GET IT BACKWARDS AND THE LENGTH STAYS RIGHT WHILE THE COUNT IS WRONG
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
42 / VIDEO UNIT 07 · COUNT THE LONGEST

DP 47 · Number of Longest Increasing Subsequences

STRIVER A2Z
The count array · replace on longer, add on equal
RUNTIME 20:46
AFTER THIS → 3 DRILLS · PROBLEM #07
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
43 / DRILL UNIT 07 · COUNT THE LONGEST · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

What does the second array alongside dp[] hold?

Two arrays walked together: length and count. Every counting problem in this step has done the same thing — run the optimisation, then carry a tally beside it. What differs is the update rule, which is the next drill.

DRILL 02 · RECALL

Extending from j gives a chain LONGER than the best ending at i so far. What happens to cnt[i]?

A longer chain invalidates everything counted so far, so the tally starts again from cnt[j]. Adding instead of replacing is the bug — the count comes out too large while the LENGTH stays perfectly correct, so the failure is invisible in the easy half of the output.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
44 / DRILL UNIT 07 · COUNT THE LONGEST · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

Extending from j gives a chain EXACTLY as long as the best so far. Then?

Equal length means a genuinely different route to the same best, so the counts accumulate. Replace-on-longer, add-on-equal is the whole rule, and mixing the two up is the entire difficulty of this row.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
45 / PROBLEM #07 · LIS · MED

Number of Longest Increasing Subsequences

MED lis ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“How many” longest increasing subsequences. Counting alongside an optimisation — the same pairing deck II used, one dimension along.

INTUITION

Carry two arrays: the length ending at i, and how many chains achieve it. When a longer chain arrives, everything counted so far is no longer longest, so the tally is REPLACED. When an equal-length chain arrives, it is a genuinely different route to the same best, so the tallies ADD. Replace-on-longer, add-on-equal is the whole rule.

STEPS
  1. State: dp[i] = longest run ending at i; cnt[i] = how many achieve it.
  2. If dp[j] + 1 > dp[i]: dp[i] = dp[j] + 1 and cnt[i] = cnt[j].
  3. If dp[j] + 1 == dp[i]: cnt[i] += cnt[j].
  4. Find the best length, then sum cnt over every i achieving it.
BRUTEO(2ⁿ)
OPTIMALO(n²)
↕ SCROLL
int findNumberOfLIS(vector<int>& a) {
    int n = a.size(), best = 0, total = 0;
    vector<int> dp(n, 1), cnt(n, 1);
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (a[j] >= a[i]) continue;
            if (dp[j] + 1 > dp[i]) {
                dp[i] = dp[j] + 1;
                cnt[i] = cnt[j];          // LONGER: replace the tally
            } else if (dp[j] + 1 == dp[i]) {
                cnt[i] += cnt[j];         // EQUAL: another route, so add
            }
        }
        best = max(best, dp[i]);
    }
    for (int i = 0; i < n; i++) if (dp[i] == best) total += cnt[i];
    return total;
}
TIMEO(n²)every pair, with two updates
SPACEO(n)two arrays of n
TRAP

Adding where you should replace. The LENGTH stays perfectly correct, so half the output looks right — only the count is inflated, and only on inputs with more than one optimal chain.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
46 / INTRO UNIT 08 · PARTITION DP BEGINS

UNIT 08 — PARTITION DP BEGINS

The one genuinely new idea in step 16

THE QUESTION THIS LECTURE ANSWERS

MULTIPLY A CHAIN OF MATRICES. WHICH ORDER COSTS LEAST?

partition DPrange statecut pointcombine term
WHAT TO WATCH FOR
  • 01A NEW PATTERN, AND HE SAYS SO: YOU CHOOSE A SPLIT, NOT AN ELEMENT
  • 02n MATRICES HAVE n+1 DIMENSIONS — MATRIX i IS d[i−1] × d[i], SO i STARTS AT 1
  • 03THE COMBINE AT THE CUT IS d[i−1] × d[k] × d[j]. FORGETTING IT IS THE BUG
  • 04k LOOPS OVER EVERY CUT POINT — THERE IS NO LOCAL RULE FOR THE BEST ONE
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
47 / VIDEO UNIT 08 · PARTITION DP BEGINS

DP 48 · Matrix Chain Multiplication

STRIVER A2Z
Choosing a cut instead of an element · the dimension indexing · looping every k
RUNTIME 53:41
AFTER THIS → 4 DRILLS · PROBLEM #08
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
48 / DRILL UNIT 08 · PARTITION DP BEGINS · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

The lecturer calls this a new PATTERN, not just a new problem. What is new about it?

Every DP before this chose an ELEMENT — take it or skip it. Partition DP chooses a CUT POINT, recurses on both sides, and combines at the cut. It is the one genuinely new idea in step 16, and he warns it is the hard pattern.

DRILL 02 · RECALL

Why does the recursion start at i = 1 rather than i = 0?

n matrices are described by n+1 dimensions, so matrix i is arr[i-1] × arr[i]. The chain of matrices runs 1..n-1 in that array. Getting this indexing wrong is the single most common way MCM goes wrong, and it produces a plausible number.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
49 / DRILL UNIT 08 · PARTITION DP BEGINS · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

What is the cost combined AT the split point k?

Both sides collapse to single matrices — one is arr[i-1] × arr[k], the other arr[k] × arr[j] — and multiplying those costs their product of dimensions. Forgetting the combine term entirely is the other classic MCM bug.

DRILL 02 · RECALL

Why does k loop over every value from i to j−1 rather than a chosen split?

There is no greedy choice of split, which is exactly why this is a DP. The loop over k is the partition pattern's signature — see a loop over cut points inside a two-index state and you are looking at partition DP.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
50 / MECHANISM UNIT 08 · MCM · CODE MIRRORED

A TRIANGULAR TABLE, AND A LOOP OVER CUTS

Matrices with dimensions [10, 20, 30, 40, 30]. Every earlier bench in this step filled a rectangle; this one fills half of one, because f(i, j) only means anything for i ≤ j. The diagonal is the base case, the fill runs by increasing range length, and each cell tries every cut point. There is no local rule for which is best.

RECURRENCE
STATE
CODE MIRROR
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
51 / PROBLEM #08 · PARTITION · HARD

Matrix chain multiplication

HARD partition ▶ SOLVE ON GEEKSFORGEEKS
SIGNAL — WHAT GIVES IT AWAY

“In which order should these be multiplied?” No element to take or skip — you are choosing where to split, which is a different pattern from everything before it.

INTUITION

For a range of matrices, the last multiplication happens at some split k: the left side has collapsed to one matrix, so has the right, and joining them costs the product of three dimensions. There is no local rule for the best k, so you try every one. The state is a RANGE rather than a position, which is what makes the table triangular.

STEPS
  1. n matrices have n+1 dimensions; matrix i is d[i−1] × d[i], so ranges start at 1.
  2. State: f(i, j) = cheapest way to multiply matrices i through j.
  3. Base: f(i, i) = 0. One matrix needs no multiplication.
  4. For every k in i..j−1: f(i, k) + f(k+1, j) + d[i−1]·d[k]·d[j].
  5. Take the minimum over k.
BRUTEO(4ⁿ)
OPTIMALO(n³)
↕ SCROLL
int f(int i, int j, vector<int>& d, vector<vector<int>>& dp) {
    if (i == j) return 0;                 // one matrix: nothing to multiply
    if (dp[i][j] != -1) return dp[i][j];
    int best = INT_MAX;
    for (int k = i; k < j; k++) {         // EVERY cut point
        int cost = f(i, k, d, dp) + f(k+1, j, d, dp)
                 + d[i-1] * d[k] * d[j];  // the COMBINE term
        best = min(best, cost);
    }
    return dp[i][j] = best;
}
TIMEO(n³)n² ranges, each looping n cut points
SPACEO(n²)the triangular table
TRAP

Omitting the d[i−1]·d[k]·d[j] combine term. Splitting becomes free, so the answer is just the sum of the sub-costs — always too small, always plausible, and the recursion structure looks textbook-correct.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
52 / INTRO UNIT 09 · TABULATING BACKWARDS

UNIT 09 — TABULATING BACKWARDS

The one place in step 16 where the loops run backwards

THE QUESTION THIS LECTURE ANSWERS

SAME PROBLEM, TABULATED. WHY IS THAT WORTH ITS OWN LECTURE?

tabulation orderdiagonal baserange length
WHAT TO WATCH FOR
  • 01i RUNS DOWNWARD AND j UPWARD — THE ONLY REVERSAL IN THE WHOLE STEP
  • 02BECAUSE f(i, j) READS RANGES INSIDE ITSELF, WHICH MUST EXIST FIRST
  • 03THE BASE CASE IS THE DIAGONAL: A RANGE OF ONE COSTS NOTHING
  • 04SAME O(n³) EITHER WAY — THE LECTURE IS ABOUT THE ORDER, NOT THE SPEED
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
53 / VIDEO UNIT 09 · TABULATING BACKWARDS

DP 49 · Matrix Chain Multiplication, Bottom-Up

STRIVER A2Z
i downward, j upward · the diagonal base case · why the order is forced
RUNTIME 9:08
AFTER THIS → 3 DRILLS · PROBLEM #09
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
54 / DRILL UNIT 09 · TABULATING BACKWARDS · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

In what order must the two loops run when tabulating MCM?

f(i, j) reads f(i, k) and f(k+1, j) — smaller ranges inside the current one. Only i-down, j-up guarantees those are filled first. This is why the sheet gives the tabulated form its own row: the loop order is genuinely counter-intuitive.

DRILL 02 · RECALL

What does the base case become in the tabulated form?

One matrix needs no multiplication, so the diagonal is 0. Every partition DP has this shape: the base is the diagonal, because that is where the range has collapsed to a single item.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
55 / DRILL UNIT 09 · TABULATING BACKWARDS · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

Why is tabulating this worth a separate nine-minute lecture at all?

Same O(n³) either way. The lecture exists because every earlier tabulation in this step ran its loops upward and this one does not — and the deck ships it as a diff so the reversal is the only thing on screen.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
56 / PROBLEM #09 · PARTITION · HARD

Matrix Chain Multiplication | Bottom-Up|(DP-49)

HARD partition ▶ SOLVE ON GEEKSFORGEEKSThe same judge problem as row 8 — this row is the tabulated form of it. Partition DP tabulates awkwardly (the loops run i downward and j upward, which is not the usual order), and that is exactly why the sheet gives it a row of its own.
SIGNAL — WHAT GIVES IT AWAY

The same problem, tabulated. The sheet gives it a row of its own because partition DP is the one place in step 16 where the loop order is not the usual one.

INTUITION

f(i, j) reads f(i, k) and f(k+1, j) — ranges strictly INSIDE itself. So those must already be filled, which forces i to run downward while j runs upward. Every earlier tabulation in this step ran both loops upward; this is the exception, and it is genuinely easy to get wrong.

STEPS
  1. Initialise the diagonal to 0 — a range of one costs nothing.
  2. Loop i from n down to 1.
  3. Loop j from i+1 up to n.
  4. Inside, loop k over every cut and take the minimum, as in #08.
  5. The answer is dp[1][n].
BRUTEO(4ⁿ)
OPTIMALO(n³)
↕ SCROLL
int mcm(vector<int>& d) {
    int n = d.size() - 1;                 // n matrices, n+1 dimensions
    vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));
    for (int i = n; i >= 1; i--)          // i DOWNWARD
        for (int j = i + 1; j <= n; j++) {  // j UPWARD
            dp[i][j] = INT_MAX;
            for (int k = i; k < j; k++) {   // EVERY cut point
                int cost = dp[i][k] + dp[k+1][j]
                         + d[i-1] * d[k] * d[j];   // the COMBINE term
                dp[i][j] = min(dp[i][j], cost);
            }
        }
    return dp[1][n];
}
TIMEO(n³)identical to the memoized form
SPACEO(n²)the triangular table
TRAP

Running both loops upward out of habit. The cells it reads are still zero, so the answer comes out far too small with no error and no crash — the same failure as a missing combine term, from a different cause.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
57 / INTRO UNIT 10 · SENTINELS AT THE ENDS

UNIT 10 — SENTINELS AT THE ENDS

Sentinels that give every range a length

THE QUESTION THIS LECTURE ANSWERS

CUT A STICK AT GIVEN POSITIONS. EACH CUT COSTS THE PIECE'S LENGTH.

sentinelrange boundariesorder dependence
WHAT TO WATCH FOR
  • 01SORT THE CUTS AND ADD 0 AND n AT THE ENDS AS SENTINELS
  • 02THE COST OF A CUT IS THE CURRENT PIECE'S LENGTH — WHICH THE SENTINELS GIVE YOU
  • 03CUT EARLY AND YOU PAY FOR A LONG STICK; CUT LATE AND IT IS ALREADY SHORT
  • 04SO THE ORDER MATTERS, WHICH IS EXACTLY WHY THIS IS AN OPTIMISATION
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
58 / VIDEO UNIT 10 · SENTINELS AT THE ENDS

DP 50 · Minimum Cost to Cut the Stick

STRIVER A2Z
Sorting the cuts · the 0 and n sentinels · why order changes the total
RUNTIME 30:02
AFTER THIS → 3 DRILLS · PROBLEM #10
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
59 / DRILL UNIT 10 · SENTINELS AT THE ENDS · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

Two things are done to the cuts array before the DP runs. Which?

Sorting makes the cuts within a range contiguous; the sentinels give every range a concrete left and right boundary so the cost of a cut is a simple subtraction. Skip either and the recurrence cannot be written cleanly.

DRILL 02 · RECALL

What is the cost of making a cut at position k inside the range (i, j)?

The cost is the CURRENT stick's length, which the sentinels make readable as the gap between the boundaries just outside the range. This is why they were added — without them the boundary ranges have no length to read.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
60 / DRILL UNIT 10 · SENTINELS AT THE ENDS · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

Why does the order of cuts matter at all — why is this not just a sum?

Cut early and you pay for a long stick; cut late and the piece is already short. Different orders give genuinely different totals, which is what makes it an optimisation — and it is the same reason Burst Balloons needs a table.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
61 / PROBLEM #10 · PARTITION · HARD

Minimum cost to cut the stick

HARD partition ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Each cut costs the length of the piece being cut.” A cost that depends on what is left — so the ORDER matters, and order-dependence means partition DP.

INTUITION

Sort the cut positions and add 0 and n as sentinels. A range of cuts then has a concrete left and right boundary, so the cost of cutting anywhere inside it is just the distance between those boundaries. Choose which cut in the range happens FIRST, pay that length, and both sides become independent ranges.

STEPS
  1. Push 0 and n into the cuts array and sort it.
  2. State: f(i, j) = cheapest way to make cuts i through j.
  3. Cost of any cut in that range is cuts[j+1] − cuts[i−1] — the piece's length.
  4. For each k in i..j: f(i, k−1) + f(k+1, j) + that length.
  5. Take the minimum; the answer is f(1, m−2).
BRUTEO(m!)
OPTIMALO(m³)
↕ SCROLL
int minCost(int n, vector<int>& cuts) {
    cuts.push_back(0);
    cuts.push_back(n);                    // SENTINELS: every range gets ends
    sort(cuts.begin(), cuts.end());
    int m = cuts.size();
    vector<vector<int>> dp(m + 1, vector<int>(m + 1, 0));
    for (int i = m - 2; i >= 1; i--)
        for (int j = i; j <= m - 2; j++) {
            int best = INT_MAX;
            for (int k = i; k <= j; k++)
                best = min(best, dp[i][k-1] + dp[k+1][j]
                               + cuts[j+1] - cuts[i-1]);   // the piece's LENGTH
            dp[i][j] = best;
        }
    return dp[1][m - 2];
}
TIMEO(m³)m² ranges over m cut points
SPACEO(m²)the triangular table
TRAP

Skipping the sentinels. Without a boundary just outside the range there is nothing to subtract, and the boundary ranges have no length at all — so the recurrence cannot even be written, and improvised substitutes are wrong at the edges.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
62 / INTRO UNIT 11 · THINK IN REVERSE

UNIT 11 — THINK IN REVERSE

Reversing the question so the sides stay independent

THE QUESTION THIS LECTURE ANSWERS

BURST BALLOONS FOR COINS. WHICH ORDER MAXIMISES WHAT YOU COLLECT?

reverse the questionindependencesentinels
WHAT TO WATCH FOR
  • 01BURSTING A MIDDLE BALLOON JOINS ITS NEIGHBOURS — THE SIDES STOP BEING SUBPROBLEMS
  • 02SO ASK WHICH IS BURST LAST: ITS NEIGHBOURS ARE THEN THE RANGE'S FIXED ENDS
  • 03THE GAIN IS a[i−1] × a[k] × a[j+1], NOT THE IMMEDIATE NEIGHBOURS
  • 04REVERSING THE QUESTION IS THE WHOLE TRICK, AND IT IS WORTH STEALING
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
63 / VIDEO UNIT 11 · THINK IN REVERSE

DP 51 · Burst Balloons

STRIVER A2Z
Why 'first' fails · asking 'last' instead · the gain at the boundaries
RUNTIME 34:00
AFTER THIS → 3 DRILLS · PROBLEM #11
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
64 / DRILL UNIT 11 · THINK IN REVERSE · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

Why can this not be solved by asking which balloon to burst FIRST?

Burst a middle balloon first and the two sides become adjacent — so they are no longer independent subproblems and the recursion cannot split. This is the obstacle the whole lecture is built around.

DRILL 02 · RECALL

What is the fix?

If k is burst LAST in a range, its neighbours at that moment are exactly the range's boundaries — which are fixed and known. The two sides become independent again, and the standard partition recurrence applies. Reversing the question is the entire trick.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
65 / DRILL UNIT 11 · THINK IN REVERSE · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

What does the gain at the split k evaluate to?

Because k is burst last, everything strictly inside the range is already gone and its neighbours are the cells just OUTSIDE it. Using the immediate neighbours is the reflex, and it is what the reversal exists to avoid.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
66 / PROBLEM #11 · PARTITION · HARD

Burst balloons

HARD partition ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

An operation that changes its neighbours. That is the tell for reversing the question — and it is why the obvious recursion does not work.

INTUITION

Bursting a middle balloon makes its neighbours adjacent, so the two sides are no longer independent and cannot be recursed on. Ask instead which balloon is burst LAST in a range: at that moment everything else inside is gone, so its neighbours are exactly the range's fixed boundaries. The sides become independent again and the standard partition recurrence applies.

STEPS
  1. Pad the array with 1 at both ends — the sentinels the gain term reads.
  2. State: f(i, j) = best coins from bursting everything in i..j.
  3. For each k in i..j, treat k as burst LAST.
  4. Gain: a[i−1]·a[k]·a[j+1] + f(i, k−1) + f(k+1, j).
  5. Take the maximum over k.
BRUTEO(n!)
OPTIMALO(n³)
↕ SCROLL
int maxCoins(vector<int>& nums) {
    vector<int> a;
    a.push_back(1);
    for (int x : nums) a.push_back(x);
    a.push_back(1);                       // sentinels of 1 at both ends
    int n = a.size();
    vector<vector<int>> dp(n, vector<int>(n, 0));
    for (int i = n - 2; i >= 1; i--)
        for (int j = i; j <= n - 2; j++) {
            int best = 0;
            for (int k = i; k <= j; k++)  // k is burst LAST in this range
                best = max(best, a[i-1] * a[k] * a[j+1]
                               + dp[i][k-1] + dp[k+1][j]);
            dp[i][j] = best;
        }
    return dp[1][n - 2];
}
TIMEO(n³)n² ranges, each looping n choices
SPACEO(n²)the triangular table
TRAP

Using the immediate neighbours a[k−1] and a[k+1] in the gain. Because k is burst LAST, everything strictly inside the range has already gone — its neighbours are the cells outside it. Using the immediate ones is the reflex the reversal exists to avoid.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
67 / INTRO UNIT 12 · COUNT THE PARENTHESISATIONS

UNIT 12 — COUNT THE PARENTHESISATIONS

A partition where the counts multiply instead of adding

THE QUESTION THIS LECTURE ANSWERS

HOW MANY WAYS CAN YOU PARENTHESISE THIS EXPRESSION SO IT EVALUATES TO TRUE?

boolean parenthesisationoutcome indexmultiplying counts
WHAT TO WATCH FOR
  • 01A THIRD INDEX: WHETHER THIS RANGE MUST COME OUT TRUE OR FALSE
  • 02k LANDS ONLY ON OPERATORS, SO BOTH SIDES ARE WHOLE EXPRESSIONS
  • 03AN AND NEEDS BOTH SIDES TRUE, SO THE COUNTS MULTIPLY — NOT ADD
  • 04EVERY EARLIER COUNTING DP ADDED, WHICH IS WHY THIS ONE CATCHES PEOPLE
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
68 / VIDEO UNIT 12 · COUNT THE PARENTHESISATIONS

DP 52 · Evaluate Boolean Expression to True

STRIVER A2Z
The third state index · splitting on operators · why AND multiplies
RUNTIME 34:55
AFTER THIS → 3 DRILLS · PROBLEM #12
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
69 / DRILL UNIT 12 · COUNT THE PARENTHESISATIONS · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

What does the state carry beyond the two range endpoints?

A third index for the required outcome, because an AND needs both sides TRUE while an OR needs either — so you must be able to ask for FALSE counts too. Partition DP with one extra bit of state, and that bit is the whole problem.

DRILL 02 · RECALL

The split point k in this problem lands on what?

k steps by two, landing only on operators, so each side is a well-formed expression. Splitting on operands produces fragments that cannot be evaluated — which is what makes this partition variant look harder than it is.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
70 / DRILL UNIT 12 · COUNT THE PARENTHESISATIONS · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

Counting ways for an AND to be TRUE combines the sides how?

Every left parenthesisation pairs with every right one, so counts MULTIPLY. Every earlier counting problem in step 16 added its branches, because they were alternatives; here they are simultaneous. Adding is the natural reflex and it is wrong.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
71 / PROBLEM #12 · PARTITION · MED

Different Ways to Evaluate a Boolean Expression

MED partition ▶ SOLVE ON LEETCODEThe lecture solves “count the ways to parenthesise so the expression evaluates to TRUE” — a partition DP. LeetCode 1106, which the sheet links, asks you to evaluate one fully-parenthesised expression, which is a parsing exercise with no DP in it. Watch the lecture for the partition; the link is the sheet's, and it does not exercise it.
SIGNAL — WHAT GIVES IT AWAY

“In how many ways can it be parenthesised so it evaluates to true?” Partition DP with a third index — and the one place counts multiply rather than add.

INTUITION

Split at an operator so both sides are whole expressions. An AND is true only when both sides are, so its count is leftTrue × rightTrue — every left parenthesisation pairs with every right one. Because an OR needs either side true, you also have to be able to ask for FALSE counts, which is what the third state index is for.

STEPS
  1. State: f(i, j, wantTrue) — the required outcome is part of the state.
  2. Base: a single character; it is T or F.
  3. Loop k over operator positions only, stepping by two.
  4. AND true: lT·rT. OR true: lT·rT + lT·rF + lF·rT. XOR true: lT·rF + lF·rT.
  5. Sum over k, modulo whatever the judge asks for.
BRUTECatalan — O(4ⁿ)
OPTIMALO(n³)
↕ SCROLL
int countWays(string& s) {
    int n = s.size();
    const int MOD = 1003;
    // dp[i][j][isTrue] - the third index is the required OUTCOME
    vector<vector<array<long long,2>>> dp(n, vector<array<long long,2>>(n, {0,0}));
    for (int i = 0; i < n; i += 2) {      // operands sit at even indices
        dp[i][i][1] = (s[i] == 'T');
        dp[i][i][0] = (s[i] == 'F');
    }
    for (int i = n - 1; i >= 0; i -= 2)
        for (int j = i + 2; j < n; j += 2)
            for (int t = 0; t < 2; t++) {
                long long ways = 0;
                for (int k = i + 1; k < j; k += 2) {   // k lands on OPERATORS
                    long long lT = dp[i][k-1][1], lF = dp[i][k-1][0];
                    long long rT = dp[k+1][j][1], rF = dp[k+1][j][0];
                    if (s[k] == '&')
                        ways += t ? lT*rT : lT*rF + lF*rT + lF*rF;  // MULTIPLY
                    else if (s[k] == '|')
                        ways += t ? lT*rT + lT*rF + lF*rT : lF*rF;
                    else
                        ways += t ? lT*rF + lF*rT : lT*rT + lF*rF;
                }
                dp[i][j][t] = ways % MOD;
            }
    return (int)dp[0][n-1][1];
}
TIMEO(n³)n² ranges × 2 outcomes × n splits
SPACEO(n²)the triangular table, doubled
TRAP

Adding the two sides' counts instead of multiplying them. Every earlier counting DP in step 16 added, because its branches were alternatives; here both sides happen at once and their choices compose. The number comes out far too small.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
72 / INTRO UNIT 13 · FRONT PARTITION

UNIT 13 — FRONT PARTITION

Cutting from the front instead of the middle

THE QUESTION THIS LECTURE ANSWERS

SPLIT THE STRING SO EVERY PIECE IS A PALINDROME. WHAT IS THE FEWEST CUTS?

front partitionoff-by-oneprecomputation
WHAT TO WATCH FOR
  • 01A DIFFERENT FORM OF PARTITION, AND HE SAYS SO — CUT FROM THE FRONT
  • 02ONLY THE RIGHT REMAINDER RECURSES, SO IT IS O(n²), NOT O(n³)
  • 03THE BASE CASE RETURNS −1, CANCELLING THE CUT CHARGED FOR THE LAST PIECE
  • 04PRECOMPUTE THE PALINDROME TABLE AND O(n³) BECOMES O(n²)
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
73 / VIDEO UNIT 13 · FRONT PARTITION

DP 53 · Palindrome Partitioning II

STRIVER A2Z
Front partition vs MCM · the −1 base case · precomputing the palindrome check
RUNTIME 23:17
AFTER THIS → 3 DRILLS · PROBLEM #13
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
74 / DRILL UNIT 13 · FRONT PARTITION · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

The lecture calls this a DIFFERENT form of partition from MCM. What changes?

Front partition: take a valid prefix, pay for it, recurse on what is left. One index instead of two, so it is O(n²) rather than O(n³) — and it is the shape of most partition problems that are not MCM.

DRILL 02 · RECALL

The recursion returns the number of CUTS, but the base case is subtle. Why?

Every recursive call charges 1 for the cut it makes, including the last piece — which needs no cut after it. Returning −1 at the end cancels that overcharge exactly. An answer one too large is exactly what you get without it.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
75 / DRILL UNIT 13 · FRONT PARTITION · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

The naive version checks isPalindrome inside the loop. What does precomputing it buy?

A palindrome table filled once in O(n²) makes every check O(1), so the whole thing is O(n²). This is the standard follow-up after you produce the working version, and it is worth volunteering before being asked.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
76 / PROBLEM #13 · PARTITION · HARD

Palindrome partitioning II

HARD partition ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Partition the string so every piece is a palindrome, minimise the cuts.” A different partition shape: you cut from the FRONT and only the remainder recurses.

INTUITION

Take a prefix that is a palindrome, charge one cut, and recurse on what is left. Only one side recurses, so the state is a single position rather than a range — which makes this O(n²) rather than O(n³). The base case returns −1 to cancel the cut charged for the final piece, which needs no cut after it.

STEPS
  1. Precompute a palindrome table in O(n²) so every check is O(1).
  2. State: f(i) = fewest cuts for the suffix starting at i.
  3. For every j ≥ i with s[i..j] a palindrome: 1 + f(j+1).
  4. Take the minimum.
  5. Base: f(n) = −1, cancelling the last piece's phantom cut.
BRUTEO(2ⁿ)
OPTIMALO(n²)
↕ SCROLL
int minCut(string s) {
    int n = s.size();
    vector<vector<bool>> pal(n, vector<bool>(n, false));
    for (int i = n - 1; i >= 0; i--)      // precompute: O(n^3) -> O(n^2)
        for (int j = i; j < n; j++)
            pal[i][j] = (s[i] == s[j]) && (j - i < 2 || pal[i+1][j-1]);
    vector<int> dp(n + 1, 0);
    dp[n] = -1;                           // -1 cancels the last piece's cut
    for (int i = n - 1; i >= 0; i--) {
        int best = INT_MAX;
        for (int j = i; j < n; j++)       // FRONT partition: take a prefix
            if (pal[i][j]) best = min(best, 1 + dp[j+1]);
        dp[i] = best;
    }
    return dp[0];
}
TIMEO(n²)n positions × n prefixes, O(1) each
SPACEO(n²)the palindrome table
TRAP

Returning 0 from the base case. Every recursive call charges 1, including the one that consumes the final piece — so the answer comes out exactly one too high on every input, which is the easiest kind of wrong to ship.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
77 / INTRO UNIT 14 · BOUNDED FRONT PARTITION

UNIT 14 — BOUNDED FRONT PARTITION

Front partition with a bound on the piece

THE QUESTION THIS LECTURE ANSWERS

SPLIT INTO PIECES OF AT MOST k, EACH BECOMING ITS MAXIMUM. MAXIMISE THE SUM.

bounded partitionrunning maximumpiece value
WHAT TO WATCH FOR
  • 01THE LOOP RUNS 1 TO k, NOT 1 TO n — WHICH IS WHY THIS IS O(n·k)
  • 02THE MAXIMUM IS CARRIED AS THE PIECE GROWS, NEVER RE-SCANNED
  • 03A PIECE OF LENGTH L WITH MAXIMUM m IS WORTH L × m, NOT m
  • 04SAME FRONT-PARTITION SHAPE AS THE PREVIOUS UNIT, WITH A CAP
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
78 / VIDEO UNIT 14 · BOUNDED FRONT PARTITION

DP 54 · Partition Array for Maximum Sum

STRIVER A2Z
The bounded loop · carrying the running maximum · what a piece is worth
RUNTIME 21:39
AFTER THIS → 3 DRILLS · PROBLEM #14
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
79 / DRILL UNIT 14 · BOUNDED FRONT PARTITION · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

Each subarray may be at most k long and becomes k copies of its maximum. What does the front-partition loop do?

The loop is bounded by k rather than by n, which is what makes this O(n·k). Same front-partition shape as palindrome partitioning, with a cap on how far the first piece may extend.

DRILL 02 · RECALL

How is the running maximum handled inside that loop?

Extending the piece by one element can only raise the maximum, so carrying it costs nothing. Re-scanning makes it O(n·k²) — the same 'the loop already has what you need' observation that keeps MCM's combine step cheap.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
80 / DRILL UNIT 14 · BOUNDED FRONT PARTITION · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

The value contributed by a piece of length L with maximum m is what?

The statement replaces every element of the subarray with the maximum, so a piece of length L is worth L × m. Reading it as m alone makes long pieces worthless and biases every answer toward tiny partitions.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
81 / PROBLEM #14 · PARTITION · MED

Partition Array for Maximum Sum

MED partition ▶ SOLVE ON LEETCODE
SIGNAL — WHAT GIVES IT AWAY

“Subarrays of at most k, each replaced by its maximum.” Front partition again, with a bound on how far the first piece may reach.

INTUITION

Same shape as the previous problem — take a prefix, pay for it, recurse on the rest — but the prefix may only be up to k long, so the loop is bounded by k rather than by n. The maximum of the growing piece is carried as the loop extends, because adding an element can only raise it.

STEPS
  1. State: f(i) = best total for the suffix starting at i.
  2. For L from 1 to k, while i + L ≤ n:
  3. update the running maximum with a[i + L − 1],
  4. candidate = L × max + f(i + L).
  5. Take the best; the answer is f(0).
BRUTEO(2ⁿ)
OPTIMALO(n·k)
↕ SCROLL
int maxSumAfterPartitioning(vector<int>& a, int k) {
    int n = a.size();
    vector<int> dp(n + 1, 0);
    for (int i = n - 1; i >= 0; i--) {
        int mx = 0, best = 0;
        for (int L = 1; L <= k && i + L <= n; L++) {
            mx = max(mx, a[i + L - 1]);   // carried, never re-scanned
            best = max(best, L * mx + dp[i + L]);   // a piece is worth L * max
        }
        dp[i] = best;
    }
    return dp[0];
}
TIMEO(n·k)n positions, k lengths each
SPACEO(n)one array of n+1
TRAP

Valuing a piece at its maximum rather than at length × maximum. Every element of the subarray becomes the maximum, so a piece of length L is worth L times it — reading it as m alone makes long pieces worthless and biases every answer toward tiny partitions.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
82 / INTRO UNIT 15 · STACK A HISTOGRAM

UNIT 15 — STACK A HISTOGRAM

A DP that hands its state to a stack

THE QUESTION THIS LECTURE ANSWERS

LARGEST ALL-ONES RECTANGLE IN A BINARY MATRIX.

histogrammonotonic stackcarried heights
WHAT TO WATCH FOR
  • 01EACH ROW BECOMES A HISTOGRAM OF THE 1s STACKED ABOVE IT
  • 02A ZERO RESETS THE HEIGHT TO 0 — THE HEIGHTS ARE THE REMEMBERED STATE
  • 03THE HISTOGRAM HALF IS A MONOTONIC STACK, FROM STEP 09
  • 04HE EXPLAINS OUT LOUD WHY A STACK PROBLEM IS IN A DP PLAYLIST
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
83 / VIDEO UNIT 15 · STACK A HISTOGRAM

DP 55 · Maximum Rectangle Area with all 1's

STRIVER A2Z
Rows as histograms · the monotonic stack · why this sits in a DP playlist
RUNTIME 11:04
AFTER THIS → 3 DRILLS · PROBLEM #15
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
84 / DRILL UNIT 15 · STACK A HISTOGRAM · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

A binary matrix, largest all-ones rectangle. How is it reduced?

Row by row, a cell's height is the run of consecutive 1s ending at it vertically — reset to 0 on a 0. Every row is then a histogram, and the answer is the best largest-rectangle-in-a-histogram over all n of them.

DRILL 02 · RECALL

What solves the histogram half?

Largest rectangle in a histogram is the classic monotonic-stack problem. He says out loud why a stack problem sits in a DP playlist: the carried heights are the remembered state, and the stack is what runs on top of them.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
85 / DRILL UNIT 15 · STACK A HISTOGRAM · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

What is the complexity?

n rows, each O(m) to build and O(m) to run the stack over. Optimal, and the reason the reduction is worth knowing rather than just the brute force over all rectangle corners.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
86 / PROBLEM #15 · SQUARES · HARD

Maximum Rectangle Area with all 1's|(DP-55)

HARD squares ▶ SOLVE ON LEETCODEThe lecturer says out loud why this sits in a DP playlist: the heights carried down from row to row are the remembered state, and everything on top of that is the largest rectangle in a histogram — a monotonic stack, from step 09.
SIGNAL — WHAT GIVES IT AWAY

A binary matrix and “largest rectangle”. The sheet files it under DP, and the DP part is smaller than it looks — most of the work is a stack.

INTUITION

Walk the matrix row by row, carrying for each column the run of 1s ending at that row — reset to 0 on a 0. Every row is then a histogram, and the answer is the best 'largest rectangle in a histogram' over all n of them. The carried heights are the remembered state; the monotonic stack is what runs on top.

STEPS
  1. Maintain a heights array, one entry per column.
  2. For each row: heights[j] = grid[i][j] ? heights[j] + 1 : 0.
  3. Run largest-rectangle-in-histogram over that array.
  4. The stack pops while the top is not shorter than the current bar, and each pop measures a rectangle.
  5. Keep the best across every row.
BRUTEO(n²·m²)
OPTIMALO(n·m)
↕ SCROLL
int largestRectangleArea(vector<int>& h) {
    int n = h.size(), best = 0;
    stack<int> st;                        // MONOTONIC stack, from step 09
    for (int i = 0; i <= n; i++) {
        int cur = (i == n) ? 0 : h[i];
        while (!st.empty() && h[st.top()] >= cur) {
            int height = h[st.top()]; st.pop();
            int left = st.empty() ? -1 : st.top();
            best = max(best, height * (i - left - 1));
        }
        st.push(i);
    }
    return best;
}
int maximalRectangle(vector<vector<char>>& g) {
    int n = g.size(), m = g[0].size(), best = 0;
    vector<int> heights(m, 0);
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++)       // the carried heights ARE the DP
            heights[j] = (g[i][j] == '1') ? heights[j] + 1 : 0;
        best = max(best, largestRectangleArea(heights));
    }
    return best;
}
TIMEO(n·m)each row built and scanned linearly
SPACEO(m)the heights array and the stack
TRAP

Rebuilding the heights from scratch for each row. It is O(n·m) per row and turns the whole thing into O(n²·m) — the carried heights ARE the dynamic programming here, and recomputing them throws away the only DP in the problem.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
87 / INTRO UNIT 16 · THREE NEIGHBOURS, ONE MIN

UNIT 16 — THREE NEIGHBOURS, ONE MIN

Three neighbours, one min, and a sum at the end

THE QUESTION THIS LECTURE ANSWERS

HOW MANY ALL-ONES SQUARE SUBMATRICES ARE THERE ALTOGETHER?

min of threecounting by summinggrid DP
WHAT TO WATCH FOR
  • 011 + min OF UP, LEFT AND DIAGONAL — THE MIN ENFORCES ALL THREE
  • 02A CELL HOLDING 3 CONTAINS SQUARES OF SIDE 1, 2 AND 3
  • 03SO THE ANSWER IS THE SUM OF THE WHOLE TABLE, NOT ITS MAXIMUM
  • 04THE ONLY EASY ROW IN THE DECK — A GRID DP FROM DECK I IN NEW CLOTHES
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
88 / VIDEO UNIT 16 · THREE NEIGHBOURS, ONE MIN

DP 56 · Count Square Submatrices with All Ones

STRIVER A2Z
The min of three · why a cell of s holds s squares · the gentlest row in the deck
RUNTIME 15:59
AFTER THIS → 3 DRILLS · PROBLEM #16
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
89 / DRILL UNIT 16 · THREE NEIGHBOURS, ONE MIN · 1 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

dp[i][j] is the side of the largest all-ones square with its bottom-right corner at (i, j). How is it computed?

A square of side s here needs squares of side s−1 at all three neighbours — the MIN is what enforces 'all three', because the weakest one limits you. Taking the max would claim squares that contain a zero.

DRILL 02 · RECALL

The question asks for the COUNT of all square submatrices. How does the table answer it?

A cell holding 3 means squares of side 1, 2 AND 3 all end there — three of them. So the total is the sum of the whole table. Counting non-zero cells instead counts only the 1×1 squares and undercounts badly.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
90 / DRILL UNIT 16 · THREE NEIGHBOURS, ONE MIN · 2 OF 2

CHECK — CAN YOU ANSWER IT WITHOUT LOOKING BACK?

DRILL 01 · RECALL

This is the only EASY row in the deck. What makes it easy after seven partition problems?

It is a grid DP from deck I wearing new clothes — read three neighbours, take a min, add one. After Burst Balloons and MCM the sheet is ending step 16 gently on purpose, and the deck ends on it too.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
91 / MECHANISM UNIT 16 · SQUARES · CODE MIRRORED

THREE NEIGHBOURS, ONE MIN

Each cell is the side of the largest all-ones square with its bottom-right corner there, 1 + min of up, left and diagonal. The min is what enforces all three. And the answer is the sum of every cell, because a cell holding 3 contains squares of side 1, 2 and 3.

RECURRENCE
STATE
CODE MIRROR
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
92 / PROBLEM #16 · SQUARES · EASY

Count Square Submatrices with All Ones|(DP-56)

EASY squares ▶ SOLVE ON LEETCODEThe sheet marks this EASY, and it is the only EASY row in the deck — three neighbours, one min, no partitioning. A deliberately gentle close after seven partition problems.
SIGNAL — WHAT GIVES IT AWAY

A binary matrix and “count all square submatrices”. No partitioning, no stack — three neighbours and a min, which is deck I's grid DP in new clothes.

INTUITION

Let dp[i][j] be the side of the largest all-ones square whose bottom-right corner is at (i, j). A square of side s there needs squares of side s−1 at the cells above, left and diagonally up-left — so the MIN of those three, plus one, is what this corner supports. And a cell holding 3 contains squares of side 1, 2 and 3, so the total count is the sum of the whole table.

STEPS
  1. dp[i][j] = 0 wherever the grid is 0.
  2. On the top row or left column, dp = 1 if the cell is 1.
  3. Otherwise dp[i][j] = 1 + min(up, left, diagonal).
  4. Add every dp value into a running total.
  5. Return that total.
BRUTEO(n·m·min(n,m)²)
OPTIMALO(n·m)
↕ SCROLL
int countSquares(vector<vector<int>>& g) {
    int n = g.size(), m = g[0].size(), total = 0;
    vector<vector<int>> dp(n, vector<int>(m, 0));
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++) {
            if (!g[i][j])          dp[i][j] = 0;
            else if (!i || !j)     dp[i][j] = 1;    // on an edge: 1x1 only
            else dp[i][j] = 1 + min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]});
            total += dp[i][j];     // a cell holding s contains s squares
        }
    return total;
}
TIMEO(n·m)one pass, three reads per cell
SPACEO(m)one row, rolled
TRAP

Counting the cells that are non-zero, or reading the table's maximum. A cell of value s contains s squares, so the answer is the SUM — counting non-zero cells returns only the number of 1×1 squares, which is a plausible undercount.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
93 / RECALL RETRIEVAL, NOT RECOGNITION · 1 OF 3

NAME THE PATTERN FROM WHAT IS ASKED

DRILL 01 · RECALL

A partition DP's base case sits where in the table?

The diagonal. f(i, i) is a range of one — one matrix costs nothing to multiply, one character is already a palindrome. Every partition problem has this shape, and the triangular table is what makes it visible.

DRILL 02 · RECALL

Tabulating a partition DP, which way must the loops run?

i down, j up. f(i, j) reads ranges INSIDE itself, so those must already exist. This is the only place in step 16 where the loop order is reversed, which is why the sheet gives the tabulated MCM its own row.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
94 / RECALL RETRIEVAL, NOT RECOGNITION · 2 OF 3

NAME THE PATTERN FROM WHAT IS ASKED

DRILL 01 · RECALL

Front partition — palindrome partitioning, partition array for maximum sum — differs from MCM how?

Only the right side recurses. The prefix is finished and paid for, so the state is a single position — O(n²) rather than O(n³). Most partition problems that are not MCM have this shape, and recognising which you have decides the complexity before you write anything.

DRILL 02 · RECALL

Counting parenthesisations that evaluate to true, an AND node combines its sides by…

Multiply. Every counting problem before this one ADDED, because its branches were alternatives — pick or skip, one or the other. Here both sides happen at once and their choices compose, so the counts multiply. Adding is the reflex and it is wrong.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
95 / RECALL RETRIEVAL, NOT RECOGNITION · 3 OF 3

NAME THE PATTERN FROM WHAT IS ASKED

DRILL 01 · RECALL

LIS by binary search: what is in the maintained array?

Smallest tails. A smaller tail is never worse, which keeps the array sorted and makes binary search legal. Its LENGTH is the answer; its contents may never have co-occurred as a subsequence, so printing it is the classic misuse.

DRILL 02 · RECALL

Counting all-ones square submatrices, the answer is…

Sum the table. A cell holding 3 means squares of side 1, 2 and 3 all end there. Counting non-zero cells counts only the 1×1s; reading the corner answers a different question entirely — the same 'where does the answer live' problem this step keeps posing.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
96 / TRAPS PLAUSIBLE, WRONG, AND SILENT

SIX WAYS TO BE PLAUSIBLY WRONG

Partition DP fails quietly. A missing combine term, a loop running the wrong way, a count added where it should have multiplied — every one of these returns a number, and the recursion above it looks exactly like the textbook.

FORGETTING THE COMBINE TERM IN A PARTITION DP

dp[i][k] + dp[k+1][j] with nothing added for joining them. Splitting becomes free, the answer is always too small, and the recursion looks textbook-perfect.

SPLITTING BURST BALLOONS ON THE FIRST BURST

Bursting a middle balloon joins its neighbours, so the halves are not independent and the recurrence is simply invalid. Ask which is burst LAST instead.

TABULATING A PARTITION DP WITH BOTH LOOPS UPWARD

f(i, j) reads ranges inside itself, so i must run DOWNWARD. Upward reads cells that are still zero and produces a confident, much-too-small answer.

ADDING WHERE A PARTITION COUNT SHOULD MULTIPLY

In boolean parenthesisation an AND's sides happen together, so their counts compose multiplicatively. Every earlier counting DP added, which is exactly why this gets missed.

PRINTING THE TAILS ARRAY AS THE LIS

Overwriting tails leaves an array of the right LENGTH whose contents may never have formed a subsequence. The number is right and the sequence is fiction.

READING dp[n-1] FOR LIS

Each cell is the run ENDING there, so the answer is the maximum over the array. Reading the last cell answers 'the longest run that must include the final element'.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
97 / CHEATSHEET REVISION SURFACE

ONE SCREEN, THE WHOLE DECK

Sixteen rows and the last page of step 16. The top seven are LIS with one thing swapped each time; the middle seven are partition DP; the last two are grids.

ALGORITHM
TIME
SPACE
REACH FOR IT WHEN
LIS · table
O(n²)
O(n)
dp[i] = max(dp[j]+1) for j
LIS · binary search
O(n log n)
O(n)
smallest tail per length; append or overwrite
Print LIS
O(n²)
O(n)
carry a parent array; walk back from the argmax
Largest divisible subset
O(n²)
O(n)
sort, then LIS with a[i] % a[j] == 0
Longest string chain
O(n²·L)
O(n)
sort by length, then LIS with an is-predecessor check
Longest bitonic
O(n²)
O(n)
LIS from the left + LIS from the right − 1 at each peak
Number of LIS
O(n²)
O(n)
carry counts: REPLACE on longer, ADD on equal
MCM · memoized
O(n³)
O(n²)
min over k of dp[i][k]+dp[k+1][j]+d[i-1]·d[k]·d[j]
MCM · tabulated
O(n³)
O(n²)
same, but i runs DOWNWARD and j upward
Cut the stick
O(n³)
O(n²)
sort the cuts, add 0 and n as sentinels; cost is the piece length
Burst balloons
O(n³)
O(n²)
ask which is burst LAST; gain is a[i-1]·a[k]·a[j+1]
Boolean parenthesisation
O(n³)
O(n²)
third index for the required result; counts MULTIPLY
Palindrome partitioning II
O(n²)
O(n)
front partition; base returns −1 for the last piece
Partition array for max sum
O(n·k)
O(n)
front partition capped at k; value is L × max
Maximal rectangle
O(n·m)
O(m)
per-row histogram heights + a monotonic stack
Count square submatrices
O(n·m)
O(m)
1 + min of three neighbours; SUM the table
INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
98 / CLOSE STEP 16 · DECK 4 OF 4

CHOOSE THE CUT

Seven problems that carried the last thing you took — and one that threw the table away for a binary search on tails. Then the pattern that is different in kind: stop choosing elements, choose a split, solve both sides, and pay something at the join. Matrix chains, sticks, balloons burst in reverse, boolean expressions whose counts multiply, and two front partitions where only the remainder recurses. That completes step 16 — fifty-five rows, fifty-six lectures, four decks. Every one of them was the same three sentences: express it by index, do everything possible there, then take the max, the min or the sum.

00%
OF THIS DECK SOLVED
← ALL TOPICSTHE SHELFDECK III · STRINGS & STOCKS

Deck 4 of 4, and the last of step 16. Lectures DP 41-56 of 56, sixteen against sixteen sheet rows. With this deck the playlist is fully used and all 55 rows of the sheet have a slide. Every drill cites its lecture transcript; every bench fills in numbers the build re-derived; and every code listing was executed against the judge's own examples before it shipped.

INVARIANT · DYNAMIC PROGRAMMING · LIS & PARTITION · DECK 4 OF 4
■ TOO SMALL TO READ
1280×720
INVARIANT · STEP 16 · DECK 4 OF 4

This one needs a laptop

Not a preference — an arithmetic one. Every slide is a fixed 1280 × 720 stage: a graph animating beside the code that drives it, with the problems laid out two columns wide. It scales as a single piece, so on a screen this size the body text comes out around 4px tall.

Shrinking it further would not help, and rebuilding it to reflow would mean losing the thing that makes it worth reading.

YOUR SCREEN0 × 0
NEEDED1060 × 610
WHAT IS WAITING ON THE LAPTOP
  • Algorithms you can step through, forwards and back
  • Predict mode — call the next step before it is shown
  • Spaced repetition, so the sheet stops decaying
WHERE TO CUT · NOT WHAT TO TAKE
Your progress is saved per device, so anything you tick on the laptop will be waiting there.