Longest Palindromic Subsequence
Given a string
s, the Longest Palindromic Subsequence (LPS) problem asks for the length of the longest subsequence ofsthat reads the same forward and backward. A subsequence — unlike a substring — is not required to be contiguous; it is any sequence obtained by deleting zero or more characters fromswhile preserving the relative order of the rest. For example, in"bbbab"the LPS has length4(the subsequence"bbbb"), even though no length-4 palindromic substring exists. The problem is the canonical interval-DP example for strings (LeetCode 516) and is structurally identical to a Longest Common Subsequence problem betweensand its reverse — a non-obvious reduction worth knowing because it lets you reuse a single 2D-DP implementation for both. The straightforward 2D DP runs inO(n²)time andO(n²)space, with a textbook one-row optimization toO(n)extra space. The defining subtlety is the iteration order: the recurrencedp[i][j]readsdp[i+1][j-1],dp[i+1][j], anddp[i][j-1], so a naive row-by-row fill reads cells before they are computed. The fix — fill by increasing interval length — is the same fix as Matrix Chain Multiplication and Burst Balloons and is the recurring theme of every interval DP.
1. Intuition — Two Ends At a Time
A palindrome reads the same forward and backward. A natural way to think about palindromes is to consider the two endpoints of the candidate string. If s[i] == s[j], then those two characters can serve as the outermost pair of a palindrome whose interior is the LPS of s[i+1 .. j-1]. If s[i] != s[j], then at least one of those two characters cannot be the outermost character of any palindromic subsequence using both ends — so the best we can do is to drop one of them and recurse on the smaller interval. This single observation produces the two cases of the recurrence:
if s[i] == s[j]: dp[i][j] = dp[i+1][j-1] + 2
else: dp[i][j] = max(dp[i+1][j], dp[i][j-1])
The “+2” in the matching case accounts for the two endpoints we just consumed; the inner DP dp[i+1][j-1] is the LPS of the strict interior. In the mismatch case, neither s[i] nor s[j] can simultaneously be the leftmost and rightmost character of a single subsequence (because they differ), so we must commit to dropping one — and we try both, taking the better.
A useful real-world analogy is a zipper: imagine zipping a palindrome closed from both ends inward. At every step you check the two facing teeth (s[i] and s[j]); if they match, you zip them together and shrink the window. If they don’t match, you snip off whichever tooth gives you a longer palindrome later — and the only honest way to know which is to try both and recurse.
The reduction to LCS (Longest Common Subsequence) is the second elegant framing: the longest palindromic subsequence of s is exactly the longest common subsequence of s and reverse(s). The reason: a palindromic subsequence of s is a sequence of characters that, when read forward, equals itself read backward. Equivalently, it is a subsequence appearing both in s and in reverse(s) — which is the definition of an LCS between the two strings. This reduction is conceptually beautiful and operationally useful because every implementation of Longest Common Subsequence immediately solves LPS without further code.
2. Tiny Worked Example
Take s = "bbbab" (length n = 5). Index from 0 to 4. We will fill the upper-triangular part of dp[i][j] for 0 ≤ i ≤ j ≤ 4.
The recurrence:
- Base case:
dp[i][i] = 1for everyi(a single character is a palindrome of length 1). - Length 2 base case: if
s[i] == s[j]thendp[i][j] = 2, elsedp[i][j] = 1. (Equivalent to the general recurrence withdp[i+1][j-1]interpreted as 0 for the empty interior.) - General: as in §1.
Length 1 (the diagonal). dp[0][0] = dp[1][1] = dp[2][2] = dp[3][3] = dp[4][4] = 1.
Length 2 (super-diagonal). Compare adjacent pairs in s = b b b a b:
dp[0][1]:s[0]='b', s[1]='b', match →2.dp[1][2]:s[1]='b', s[2]='b', match →2.dp[2][3]:s[2]='b', s[3]='a', no match →1.dp[3][4]:s[3]='a', s[4]='b', no match →1.
| j=0 | j=1 | j=2 | j=3 | j=4 | |
|---|---|---|---|---|---|
| i=0 | 1 | 2 | |||
| i=1 | 1 | 2 | |||
| i=2 | 1 | 1 | |||
| i=3 | 1 | 1 | |||
| i=4 | 1 |
Length 3.
dp[0][2]:s[0]=s[2]='b', match →dp[1][1] + 2 = 1 + 2 = 3.dp[1][3]:s[1]='b' ≠ s[3]='a', mismatch →max(dp[2][3], dp[1][2]) = max(1, 2) = 2.dp[2][4]:s[2]='b' = s[4]='b', match →dp[3][3] + 2 = 1 + 2 = 3.
| j=0 | j=1 | j=2 | j=3 | j=4 | |
|---|---|---|---|---|---|
| i=0 | 1 | 2 | 3 | ||
| i=1 | 1 | 2 | 2 | ||
| i=2 | 1 | 1 | 3 | ||
| i=3 | 1 | 1 | |||
| i=4 | 1 |
Length 4.
dp[0][3]:s[0]='b' ≠ s[3]='a', mismatch →max(dp[1][3], dp[0][2]) = max(2, 3) = 3.dp[1][4]:s[1]='b' = s[4]='b', match →dp[2][3] + 2 = 1 + 2 = 3.
| j=0 | j=1 | j=2 | j=3 | j=4 | |
|---|---|---|---|---|---|
| i=0 | 1 | 2 | 3 | 3 | |
| i=1 | 1 | 2 | 2 | 3 | |
| i=2 | 1 | 1 | 3 | ||
| i=3 | 1 | 1 | |||
| i=4 | 1 |
Length 5.
dp[0][4]:s[0]='b' = s[4]='b', match →dp[1][3] + 2 = 2 + 2 = 4.
| j=0 | j=1 | j=2 | j=3 | j=4 | |
|---|---|---|---|---|---|
| i=0 | 1 | 2 | 3 | 3 | 4 |
| i=1 | 1 | 2 | 2 | 3 | |
| i=2 | 1 | 1 | 3 | ||
| i=3 | 1 | 1 | |||
| i=4 | 1 |
Answer: dp[0][n-1] = dp[0][4] = 4. The LPS is "bbbb" (taking indices 0, 1, 2, 4).
Sanity check: scanning s = "bbbab" we can pick the four bs at positions 0, 1, 2, 4 — the resulting subsequence is "bbbb", a palindrome of length 4. No five-character palindromic subsequence exists because there is only one 'a' and four 'b's, and any five-character subsequence must include the 'a', which then must appear at the center and somewhere flanking it — impossible with only one 'a'.
3. The Iteration-Order Hazard
The recurrence reads dp[i+1][j-1], dp[i+1][j], and dp[i][j-1]. Visualize the dependency: cell (i, j) reads its bottom-left, down, and left neighbors. If we fill the table row-by-row top-to-bottom (the natural for i in range(n): for j in range(n)), we read dp[i+1][j-1] before it has been computed — i+1 is a row we have not visited yet.
The correct fill order is by increasing interval length: first all length-1 cells (the diagonal), then length-2, then length-3, etc. Every cell of length L reads cells of length L-1 and shorter, all of which are guaranteed to be filled. This is the same ordering used in Matrix Chain Multiplication and Burst Balloons and is the structural fingerprint of interval DP.
An equivalent alternative is to fill rows bottom-up (for i in range(n-1, -1, -1)) with j going left-to-right (for j in range(i, n)). Under this order, dp[i+1][...] is already filled (we are below row i+1) and dp[i][j-1] is filled (we are to the right of column j-1). This works because (i+1, j-1) and (i+1, j) and (i, j-1) all sit to the lower-left, lower, and left of (i, j) in our access pattern. Many published implementations use this form because it allows a simpler doubly-nested loop. We will show both.
4. Pseudocode
4.1 Length-Outer Form (Canonical Interval-DP Signature)
LongestPalindromicSubsequence(s):
n := length(s)
dp := 2D array of size n × n, initialized 0
for i := 0 to n - 1:
dp[i][i] := 1
for length := 2 to n:
for i := 0 to n - length:
j := i + length - 1
if s[i] == s[j]:
if length == 2: dp[i][j] := 2
else: dp[i][j] := dp[i+1][j-1] + 2
else:
dp[i][j] := max(dp[i+1][j], dp[i][j-1])
return dp[0][n-1]
4.2 Bottom-Up-Row Form (Equivalent, Slightly More Compact)
for i := n - 1 down to 0:
dp[i][i] := 1
for j := i + 1 to n - 1:
if s[i] == s[j]:
dp[i][j] := dp[i+1][j-1] + 2 # dp[i+1][j-1] is 0 when j == i+1, treat as such
else:
dp[i][j] := max(dp[i+1][j], dp[i][j-1])
return dp[0][n-1]
The second form is what most LeetCode editorials show; both compute the same table.
5. Python Implementation
5.1 Bottom-Up Tabulation, O(n²) Space
def longest_palindrome_subseq(s: str) -> int:
n = len(s)
if n == 0:
return 0
dp = [[0] * n for _ in range(n)]
for i in range(n):
dp[i][i] = 1
# Iterate by increasing interval length.
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j]:
# When length == 2, dp[i+1][j-1] = dp[i+1][i] which is below the diagonal:
# the interior is empty, conventionally length 0. Our 0-initialization handles it.
dp[i][j] = dp[i+1][j-1] + 2
else:
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
return dp[0][n-1]
print(longest_palindrome_subseq("bbbab")) # 4
print(longest_palindrome_subseq("cbbd")) # 2 ("bb")The dp[i+1][j-1] access when length == 2 resolves to dp[i+1][i] — a cell below the diagonal, which we never explicitly set. Because we initialized the array with zeros and we only ever read this cell in the matching branch (where the contribution is 0 + 2 = 2), the answer is correct. Some implementations explicitly seed the second super-diagonal with 2 for matches and 1 for mismatches; this is a stylistic choice and produces the same result.
5.2 Top-Down Memoization
from functools import lru_cache
def longest_palindrome_subseq_memo(s: str) -> int:
n = len(s)
@lru_cache(maxsize=None)
def helper(i: int, j: int) -> int:
if i > j:
return 0
if i == j:
return 1
if s[i] == s[j]:
return helper(i + 1, j - 1) + 2
return max(helper(i + 1, j), helper(i, j - 1))
return helper(0, n - 1)The recursive version sidesteps iteration-order traps entirely — see Memoization vs Tabulation. Recursion naturally evaluates smaller intervals first because they sit deeper in the call tree. The trade-off is recursion overhead and Python’s default 1000-frame stack limit; for n up to ~500 it is fine in practice.
5.3 Space-Optimized O(n) Version
The recurrence reads only dp[i+1][j-1], dp[i+1][j], and dp[i][j-1]. When iterating i from n-1 down to 0, only two rows are live at any moment: the current i’th row and the previous (already-computed) (i+1)’th row. We can compress to two 1D arrays. With one more piece of bookkeeping (saving dp[i+1][j-1] before overwriting), we can collapse to a single 1D array.
def longest_palindrome_subseq_o_n_space(s: str) -> int:
n = len(s)
if n == 0:
return 0
dp = [0] * n
for i in range(n - 1, -1, -1):
prev = 0 # holds dp[i+1][j-1] from the previous iteration of j
dp[i] = 1 # diagonal
for j in range(i + 1, n):
tmp = dp[j] # save dp[i+1][j] before overwrite
if s[i] == s[j]:
dp[j] = prev + 2
else:
dp[j] = max(dp[j], dp[j-1])
prev = tmp # now `prev` holds what was dp[i+1][j], which becomes dp[i+1][(j+1)-1] in the next j-iteration
return dp[n-1]The prev variable carries the diagonal-step dependency between iterations. This is the standard 1D-rolling-array trick for any 2D DP whose recurrence reads only the previous row plus the previous column entry. Saves a factor of n in memory; identical asymptotics in time.
5.4 Reduction to LCS
def longest_palindrome_subseq_via_lcs(s: str) -> int:
return longest_common_subsequence(s, s[::-1])
def longest_common_subsequence(a: str, b: str) -> int:
m, n = len(a), len(b)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if a[i-1] == b[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]This implementation does not require thinking about interval-DP iteration order at all — dp[i][j] reads cells with strictly smaller indices, so the natural row-major fill works. This is one reason the LCS reduction is operationally attractive even though it doubles the constant factor.
The reduction recovers the length, not always a palindromic string
LPS(s) = LCS(s, reverse(s))holds for the length, and this is rigorously proven (the matching-character count ofsagainsts^Ris exactly the longest palindromic-subsequence length). But the LCS string ofsands^Ris not always a palindrome. Brodal & Fagerberg (ESA 2024) give the counterexamples = "abccab", where a longest common subsequence ofsands^Rcan come out non-palindromic (ESA 2024). To recover an actual palindrome from the reduction you take the LCStand “reflect” it — replace the last⌊k/2⌋symbols by the reverse of the first⌊k/2⌋symbols oft. So for the length (LeetCode 516, the common interview ask) the one-liner above is exact; if the problem asks for the palindrome string itself, use the native reconstruction in §7 or apply the reflect step. The two problems are otherwise computationally equivalent — you can also reduce LCS back to LPS, confirming they are equally hard (ESA 2024).
6. Complexity
Time: O(n²). There are O(n²) interval-DP cells (i, j). Each cell does O(1) work (one character comparison plus a max of two cells or one addition). Total: Θ(n²). The exact count is n(n-1)/2 interior cells + n diagonal cells.
Space: O(n²) for the standard DP, reducible to O(n) with the rolling-array trick (§5.3). Reconstruction of the actual subsequence requires O(n²) because we need the full table to backtrack.
Conditional lower bound. Like Longest Common Subsequence, LPS has a SETH-based conditional lower bound, and the bound is Ω(n^{2-ε}) for any ε > 0 — not n²/log²n (an earlier draft of this note stated n²/log²n and mis-attributed it; that was wrong). Bringmann & Künnemann (FOCS 2015) explicitly “prove quadratic-time hardness for longest palindromic subsequence and longest tandem subsequence via reductions from longest common subsequence” (Bringmann-Künnemann 2015). The reduction is exactly the LPS(s) = LCS(s, reverse(s)) equivalence of §1: since LCS over a constant alphabet has no O(n^{2-ε}) algorithm under SETH (Abboud-Backurs-Williams and Bringmann-Künnemann, both FOCS 2015), neither does LPS. A 2024 survey of the reduction states it cleanly: “finding a longest palindromic subsequence requires worst-case time Ω(n^{2-ε}), for any ε > 0, under the strong exponential time hypothesis (SETH), by the above reduction and the conditional lower bound for the LCS problem” (Brodal, Fagerberg et al., ESA 2024). Note the contrast with the substring problem, which is O(n) via Manacher’s Algorithm — there is no log factor in the LPS bound and no subquadratic algorithm in general. In practice, O(n²) is the answer.
7. Reconstructing the Subsequence
The DP table tells you the length; to recover the actual characters, walk the table from dp[0][n-1] toward the diagonal:
def reconstruct_lps(s: str) -> str:
n = len(s)
dp = [[0]*n for _ in range(n)]
for i in range(n): dp[i][i] = 1
for length in range(2, n+1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j]:
dp[i][j] = dp[i+1][j-1] + 2
else:
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
# Walk back
left, right = [], []
i, j = 0, n - 1
while i < j:
if s[i] == s[j]:
left.append(s[i])
right.append(s[j])
i, j = i + 1, j - 1
elif dp[i+1][j] >= dp[i][j-1]:
i += 1
else:
j -= 1
middle = s[i] if i == j else ""
return ''.join(left) + middle + ''.join(reversed(right))The reconstruction is O(n) because each step shrinks the window by at least one cell.
8. Comparison with Palindromic Substrings
Beginners often conflate substring and subsequence. They are different in a way that changes the algorithm fundamentally.
| Aspect | Palindromic Substring | Palindromic Subsequence |
|---|---|---|
| Contiguity | Required (must be a contiguous slice s[i..j]) | Not required (any indices, in order) |
Example in "bbbab" | Longest is "bbb" (length 3) | Longest is "bbbb" (length 4) |
| DP state | is_pal[i][j] (boolean) or dp[i][j] = length-of-pal-substring | dp[i][j] = length of LPS in s[i..j] |
| Recurrence (substring) | is_pal[i][j] = (s[i]==s[j]) AND is_pal[i+1][j-1] | n/a |
| Recurrence (subseq) | n/a | if s[i]==s[j]: dp[i][j]=dp[i+1][j-1]+2 else max(dp[i+1][j], dp[i][j-1]) |
| Better algorithm | Manacher’s Algorithm gives O(n) for longest palindromic substring | LCS-based O(n²) is best known for LPS |
| LeetCode | LC 5 (longest), LC 647 (count) | LC 516 (longest), LC 730 (distinct count) |
The key contrast: for substrings, Manacher’s algorithm exploits palindrome reuse (a palindrome at center c partly determines palindromes at nearby centers) to break the O(n²) barrier. For subsequences, no such center-symmetry exploit is known because the indices are non-contiguous; the LCS lower bound applies. This is why LPS is “stuck” at O(n²) while longest palindromic substring is O(n) via Manacher’s.
A second contrast: substring problems often have exponential counts of palindromes (every center generates several); subsequence problems often have exponential counts of distinct subsequences (LC 730 counts them mod 10^9 + 7). Don’t confuse “longest” with “count” — the DPs are different.
9. Variants and Related Problems
9.1 Minimum Insertions to Make a Palindrome (LC 1312)
Given s, find the minimum insertions to turn s into a palindrome. The answer is n − LPS(s). Reasoning: the LPS already reads as a palindrome; the remaining n − LPS characters need to be mirrored across the center via insertions. Same DP, different final read.
9.2 Minimum Deletions to Make a Palindrome
Same answer: n − LPS(s). The deletion view: keep the LPS, delete everything else.
9.3 Edit Distance Between s and reverse(s)
The edit distance with only insertions and deletions (no substitutions) is 2 × (n − LPS(s)). The minimum-edits view connects LPS to Edit Distance.
9.4 Two-Player Stone Game on the LPS DP
Some adversarial-DP problems use the same dp[i][j] interval skeleton with a min/max alternation. The LPS DP is the structural ancestor.
9.5 LPS in a Tree’s Path Strings
A non-trivial extension on trees: for each path from root to leaf, what is the LPS? Combines LPS with Tree DP. Rarely seen in interviews.
10. Diagram — Dependency Pattern of the Recurrence
flowchart TD A["dp[i][j]"] --> B["dp[i+1][j-1]<br/>(diagonal: matching case)"] A --> C["dp[i+1][j]<br/>(below: drop s[i])"] A --> D["dp[i][j-1]<br/>(left: drop s[j])"]
What this diagram shows. Each cell dp[i][j] reads exactly three other cells: the diagonal-down-left dp[i+1][j-1] (interior of the current interval, used when endpoints match), the directly-below dp[i+1][j] (the same interval after dropping its left endpoint), and the directly-left dp[i][j-1] (after dropping the right endpoint). Visualizing the table with rows indexed by i (increasing downward) and columns by j (increasing rightward), every dependency arrow points to a cell that is either below or to the left. The only safe iteration orders are therefore (a) by increasing interval length, sweeping diagonals (the canonical interval-DP order), or (b) bottom-up rows with left-to-right columns within each row. The natural top-down row-major order does not work — it would try to read row i+1 before row i+1 exists. This dependency picture is identical to the one for Matrix Chain Multiplication (the cell-dependency shape, not the formula) and is the most efficient mental hook for spotting interval DPs in disguise.
11. Common Interview Problems
| LC # | Problem | Connection |
|---|---|---|
| 516 | Longest Palindromic Subsequence | The base problem |
| 1312 | Minimum Insertion Steps to Make a String Palindrome | n − LPS(s) |
| 5 | Longest Palindromic Substring | Palindromic Substrings; expand-around-center or Manacher |
| 647 | Palindromic Substrings (count) | Different DP, substring not subsequence |
| 730 | Count Different Palindromic Subsequences | Counting variant; harder DP with character bookkeeping |
| 1143 | Longest Common Subsequence | LPS reduces to LCS(s, reverse(s)) |
| 583 | Delete Operation for Two Strings | LCS variant |
| 1216 | Valid Palindrome III | LPS-flavored: can s become palindrome with ≤ k deletions? |
| 132 | Palindrome Partitioning II | Interval DP, palindrome substring oracle + cuts DP |
| 131 | Palindrome Partitioning | Backtracking + palindrome check |
| 1771 | Maximize Palindrome Length From Subsequences | Two-string LPS variant |
| 906 | Super Palindromes | Number theory, not the LPS DP |
12. Pitfalls
12.1 Iteration Order
The headline pitfall, repeated for emphasis. A row-major fill silently produces wrong answers because dp[i+1][j-1] is read before being written. The bug is silent — no exception, no obvious sign — just a wrong number returned. Always use the length-outer or bottom-up-row form. See Matrix Chain Multiplication §11.1 for the same issue.
12.2 Confusing Subsequence and Substring
The single most-common interview confusion. The two problems share a name pattern but have different recurrences, different optimal complexities (O(n²) vs O(n) via Manacher’s), and different pitfalls. Always re-read the problem statement to confirm which is being asked. See §8.
12.3 Length-2 Boundary
When length == 2 and s[i] == s[j], the recurrence wants dp[i+1][j-1] + 2. But dp[i+1][j-1] for length == 2 is dp[i+1][i], a cell below the diagonal that is conventionally interpreted as “empty interval, length 0”. If you initialize the array with zeros, this works out automatically. If you initialize with -1 or any non-zero sentinel, you get garbage. Be explicit about your sentinel.
12.4 Off-by-One in the length Loop Bound
The loop is for length in range(2, n + 1) (Python: range(2, n+1) produces 2, 3, ..., n). Off-by-one (range(2, n) or range(1, n+1) mistakenly seeded for length 1 again) skips the full-string case. Test on n = 1 and n = 2 to catch this.
12.5 Forgetting to Initialize the Diagonal
Without dp[i][i] = 1, every cell defaults to 0, and the entire DP returns 0 for any non-empty string. A trivial bug but very common in fresh implementations.
12.6 The LCS Reduction’s Hidden Constant Factor
s[::-1] is O(n) to build but allocates a new string of size n. For very long inputs this matters. The native LPS DP avoids the reversal entirely.
12.7 Reconstructing the Wrong Palindrome
When two characters don’t match, the DP picks max(dp[i+1][j], dp[i][j-1]). Reconstruction must follow whichever side achieved the max — using >= versus > causes off-by-one in the recovered palindrome (you may emit a longer-but-invalid sequence). See the >= choice in §7 and audit it.
12.8 Memory Blowup on Long Strings
For n = 10^4, the 2D DP table is 10^8 cells — about 400 MB if stored as Python ints. Either use the O(n) rolling-array form, switch to a more compact representation (numpy’s int16), or recognize that LPS is genuinely O(n²) and not viable for the longest inputs.
12.9 Treating Palindromicity as “Mirror Across Center”
Beginners sometimes try to find the palindromic subsequence by walking from the center outward, mirroring as they go. This greedy approach fails because the optimal center is not known a priori — the LPS may have its center at any position, and even with the center known, the choice of which non-center characters to keep is itself a DP. The interval-DP framing avoids the trap.
12.10 Recursion Depth in the Memoized Version
For n up to ~500 the default Python recursion limit holds; beyond that, set sys.setrecursionlimit(10**5) or switch to bottom-up. This is the same caveat as in Tree DP and most other recursive-DP implementations.
13. Open Questions
- Is there a sub-quadratic algorithm for LPS on a binary alphabet? No — Bringmann-Künnemann’s SETH hardness for LCS already holds for binary strings, and their LPS reduction inherits it, so even binary LPS has no
O(n^{2-ε})algorithm under SETH (Bringmann-Künnemann 2015). Run-length-encoding tricks help when the string is highly compressible but do not break the worst-case quadratic barrier. - Does the
LPS = LCS(s, reverse(s))reduction also recover the LPS string, or only its length? Only the length transfers cleanly. The length of an LCS ofsands^Requals the length of an LPS ofs, but an arbitrary LCS string ofsands^Rneed not be a palindrome — Brodal-Fagerberg give the counterexamples = abccab, where an LCS ofsands^Rcan be non-palindromic — so reconstructing the palindrome needs the extra “reflect the first half onto the second half” step (ESA 2024). See §5.4.
14. See Also
- Longest Common Subsequence — the equivalent reduction; same recurrence shape on different inputs
- Palindromic Substrings — the contiguous sibling problem, with Manacher’s Algorithm as the linear-time specialist
- Matrix Chain Multiplication — the canonical interval DP; same iteration-order discipline
- Burst Balloons — interval DP with reversed reasoning direction
- Edit Distance — different fill order (row-major works); useful contrast
- Memoization vs Tabulation — the recursive form sidesteps iteration-order bugs
- DP State Identification — recognizing interval DP from the problem
- Manacher’s Algorithm — the linear-time substring counterpart
- Big-O Notation
- SWE Interview Preparation MOC