DP — 1D Linear Pattern: Algorithmic Intuition, Sub-patterns & C++ Guide
The answer at position i is built from answers at a few earlier positions. Write that rule, then fill left to right.
3 sub-patterns · 8 questions
What is this pattern?
Dynamic Programming (DP) is recursion that remembers. You write the answer for position i using the answers for a few smaller positions, then fill those answers in order so each one is computed only once.
In 1D linear DP the state is one index:
dp[i]means one clear thing, e.g. "the number of ways to reach stepi" or "the most money from the firstihouses".- The rule (recurrence) builds
dp[i]fromdp[i − 1],dp[i − 2], … (or from every earlierdp[j]). - Base cases fill the first one or two cells by hand.
- Order: fill left to right, so every cell you read is already done.
The usual path is always the same: plain recursion (correct but exponential) → memoization (store each answer the first time) → bottom-up table → keep only the cells the rule still reads (often two variables).
Counting the ways up a staircase with a notebook. Without the notebook you recount the ways to reach step 3 every time someone asks, again and again. With it, you write each step's count once — step 5's count is just step 4's plus step 3's, already on the page.
See it move
Ways to climb 5 stairs with 1- or 2-steps. First the plain recursion (orange = a subproblem solved before), then the same answers filled left to right, each once.
n = 5step 1/21- calls
- 1
f(5) = f(4) + f(3): the last move was a 1-step or a 2-step.
House Robber. Purple = the two cells the rule reads, grey = cells the two-variable code has already dropped, green on nums = robbed, red = skipped.
nums = [5, 1, 1, 5, 3]step 1/7- prev2
- 0
- prev1
- 0
dp[i] = the most money from houses 0..i without robbing two neighbours. Before house 0 both kept values are 0.
How do I recognise it?
"Number of ways to reach / build / decode …" where each step has a few choices.
"Maximum / minimum total up to position i" and the choice at i depends on the previous one or two positions (take this one or skip it).
"Can the first i characters be split into valid pieces?" — try every last piece.
A brute-force recursion that calls itself on the same smaller input many times (draw the tree: the same
f(3)appears in several branches).The input is one array or string and the answer for a prefix helps the answer for a longer prefix.
Looks like DP — 1D Linear, but isn't
DP counts or optimises; it never lists. When the output itself is all the ways, you must generate them, and there can be exponentially many.
It is DP too, but the state is "best subarray ending at i", and you restart or extend. That is the Kadane pattern.
The state is the capacity / sum left, not the position in the array. Perfect Squares sits on the border: it is 1D over the number, but it is really an unbounded knapsack.
Greedy works only when a local choice can never hurt later (that's the Greedy pattern). House Robber, Min Cost Climbing Stairs and Perfect Squares all break greedy on small inputs.
How would I discover it from scratch?
Don't memorise “use dp — 1d linear”. Ask these questions in order — the pattern falls out.
Write the recursion that tries every choice: ways(n) = ways(n − 1) + ways(n − 2), or rob(i) = max(rob(i − 1), rob(i − 2) + nums[i]). It is correct, but every call branches, so it costs about O(2^N).
Sub-patterns
Same core idea, different rule for “valid”. Learn them in this order.
Fibonacci-style
3 questionsdp[i] is built from dp[i − 1] and dp[i − 2] only — the last step was a 1-step or a 2-step (or a 1-digit or 2-digit piece).
Take or skip
3 questionsAt each element choose: take it (and you may not take its neighbour) or skip it. dp[i] = max(dp[i − 1], dp[i − 2] + value[i]).
Prefix segmentation
2 questionsdp[i] asks about the whole prefix of length i, and the rule tries every last piece s[j..i), reading dp[j] for many j.
Don't mix these up
Same states, same rule, same time. Memo keeps the recursion and fills cells only when asked; tabulation fills every cell in order with a loop, which is what lets you shrink the table to a few variables.
If one small example shows the locally best choice losing (rob the biggest house, take the biggest square), you need DP. Greedy needs a proof that the local choice is always safe.
Both start from the same recursion. If the question asks for a count or a best value, the subproblems repeat → memoize (DP). If it asks you to list every answer, nothing repeats in the output → backtracking.
Same shape (last piece is 1 or 2 long), but in Decode Ways a piece is allowed only if it's a valid code: '0' alone is not, and two digits must be 10–26.
Code templates (C++)
Fibonacci-style (two previous cells)
The base cases and the combine line are the only parts that change between problems (+ for counting ways, min(...) + cost for cheapest path, validity checks for Decode Ways).
// Fibonacci-style DP: dp[i] is built from dp[i - 1] and dp[i - 2] only,
// so two variables are enough. Shown for "ways to climb n stairs (1 or 2 steps)".
int climbWays(int n) {
if (n <= 1) return 1;
int prev2 = 1, prev1 = 1; // dp[0], dp[1]
for (int i = 2; i <= n; i++) {
int cur = prev1 + prev2; // last move was a 1-step or a 2-step
prev2 = prev1;
prev1 = cur;
}
return prev1; // dp[n]
}Take or skip
skip keeps the previous best; take adds this element to the best that ends two back. For a circle (House Robber II), run it twice: without the first element, and without the last.
// Take or skip: the best total from a[0..i] where no two taken elements are neighbours.
// dp[i] = max(skip = dp[i - 1], take = dp[i - 2] + a[i]).
long long takeSkip(const vector<int>& a, int from, int to) { // inclusive range
long long prev2 = 0, prev1 = 0; // best up to i - 2, best up to i - 1
for (int i = from; i <= to; i++) {
long long cur = max(prev1, prev2 + a[i]);
prev2 = prev1;
prev1 = cur;
}
return prev1;
}
// Circle: first and last can't both be taken.
// n == 1 ? a[0] : max(takeSkip(a, 0, n - 2), takeSkip(a, 1, n - 1))Prefix segmentation
dp[i] = the first i characters can be split. Try every last piece s[j..i); limit j to the longest word to save time.
// Prefix segmentation: dp[i] = the first i characters can be split into words.
// Try every last piece s[j..i) and read dp[j].
bool canSplit(const string& s, const vector<string>& words) {
unordered_set<string> dict(words.begin(), words.end());
size_t longest = 0;
for (auto& w : words) longest = max(longest, w.size());
int n = s.size();
vector<bool> dp(n + 1, false);
dp[0] = true; // the empty prefix splits trivially
for (int i = 1; i <= n; i++)
for (int j = i - 1; j >= 0 && i - j <= (int)longest; j--)
if (dp[j] && dict.count(s.substr(j, i - j))) { dp[i] = true; break; }
return dp[n];
}