Skip to content
DSA dynamic programming 8 min read

DP on Subsequences & Strings: Patterns Recap

This page ties together the subsequence and string DP patterns from the rest of the course. Most of these problems reduce to a small handful of templates: take-or-skip over one sequence, two-pointer 2D DP over two sequences, and interval/palindrome DP within one string. Recognizing which template fits is the real skill — once you do, the recurrence is almost mechanical.

Pattern 1: Take-or-skip over one sequence

When each element offers a binary choice (include it or not) under some constraint, define dp over (index, accumulated-constraint). This is the shape of 0/1 knapsack and subset sum: can any subset hit a target sum?

  • State: dp[i][s] = can the first i numbers form sum s?
  • Transition: dp[i][s] = dp[i-1][s] || dp[i-1][s - num] (skip or take).
  • Base case: dp[0][0] = true.
bool subsetSum(const std::vector<int>& nums, int target) {
    std::vector<char> dp(target + 1, false);
    dp[0] = true;
    for (int x : nums)
        for (int s = target; s >= x; s--)        // reverse: each number once
            dp[s] = dp[s] || dp[s - x];
    return dp[target];
}

Pattern 2: Two-pointer 2D DP over two sequences

When the answer depends on comparing two strings/arrays, use dp[i][j] indexed by a prefix of each, and branch on whether the current characters match. This is Longest Common Subsequence, edit distance, and string-matching/regex problems.

The mental template:

if (a[i-1] == b[j-1])  dp[i][j] = f(dp[i-1][j-1])      // characters align
else                   dp[i][j] = g(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])

Whether f/g use +1, max, or min depends on whether you count, maximize, or minimize.

Pattern 3: Interval / palindrome DP within one string

Some problems consider substrings (i..j ranges). Palindrome DP is the classic: dp[i][j] is true when s[i..j] is a palindrome. The transition shrinks the interval from both ends.

  • State: dp[i][j] = is s[i..j] a palindrome?
  • Transition: dp[i][j] = (s[i] == s[j]) && (j - i < 2 || dp[i+1][j-1]).
  • Order: iterate i descending (or by increasing length) so dp[i+1][...] is ready.
std::string longestPalindrome(const std::string& s) {
    int n = s.size();
    if (n == 0) return "";
    std::vector<std::vector<char>> dp(n, std::vector<char>(n, false));
    int start = 0, best = 1;
    for (int i = n - 1; i >= 0; i--)
        for (int j = i; j < n; j++)
            if (s[i] == s[j] && (j - i < 2 || dp[i + 1][j - 1])) {
                dp[i][j] = true;
                if (j - i + 1 > best) { best = j - i + 1; start = i; }
            }
    return s.substr(start, best);
}

How to recognize the pattern

Clue in the problemLikely patternExample
”subset / pick some elements to hit a target”take-or-skipknapsack, subset sum
”two strings/arrays compared”2D two-pointerLCS, edit distance
”longest/contiguous within one string”interval/palindromelongest palindromic substring
”ways/min over linear choices”1D linear DPclimbing stairs, coin change
”ending at index i”per-index DPLIS

For beginners: Don’t memorize 30 problems — memorize these 5 shapes and the question each dp cell answers. New problems are almost always a remix of a template you already know.

Going deeper: The hardest step is choosing the state. Ask: “What’s the smallest set of parameters that fully describes a subproblem?” If two different paths reach the same parameters with the same future, they share a state — and that’s where caching pays off.

Ready to apply all of this? Head to the DP exercises and check your work on the solutions page.

Last updated June 25, 2026
Was this helpful?