2Brute2Best
🔥0

DP — Strings Pattern: Algorithmic Intuition, Sub-patterns & C++ Guide

Compare two strings (or one string with itself) with a table where dp[i][j] describes the first i and j characters.

4 sub-patterns · 17 questions

Remember this · DP — Strings
Trigger
Longest common subsequence, edit distance, palindromes, matching with wildcards, interleaving.
Think
Look at the last characters: if they match, what do I do? If not, what are my options?
Key question
What does dp[i][j] mean, and what are the empty-string base cases?
A

What is this pattern?

String DP compares prefixes. For two strings a and b, dp[i][j] describes the first i characters of a and the first j characters of b. Row 0 and column 0 are the empty prefix, and they are the base cases.

Every cell looks at the last characters a[i − 1] and b[j − 1]:

  • They match → use the diagonal dp[i − 1][j − 1] (and add 1 for LCS).
  • They don't → one of them is not used, or must be changed: take the best of dp[i − 1][j] (drop a's last), dp[i][j − 1] (drop b's last) and, for edit distance, dp[i − 1][j − 1] (replace).

Fill row by row; each cell reads only its top, left and top-left neighbours, so two rows (or one row plus a saved diagonal) are enough.

Two relatives use the same table with a twist:

  • Common substring: a mismatch resets the run to 0, and the answer is the biggest cell anywhere.
  • Palindromes: one string with itself, dp[i][j] about s[i..j], filled by increasing length (or by expanding around each centre).

Two people reading their strings from the end, one letter at a time. If the last letters agree, they both move on together and score a point. If not, one of them skips a letter — and they try both ways, keeping the better result. The table remembers every "you're at letter i, I'm at letter j" situation so nobody re-reads.

B

See it move

Purple = the cells the rule reads. Green = a match. Gold = the walk back that rebuilds the subsequence.

LCS: match → diagonal, else max(top, left)a = "abcde", b = "ace"step 1/21
dp[i][j] = LCS of the first i and first j letters
∅
a
c
e
∅
0
0
0
0
a
0
b
0
c
0
d
0
e
0
Row and column ∅ are 0

dp[i][j] = the LCS of the first i letters of a and the first j letters of b. With an empty prefix (∅) there is nothing in common, so row 0 and column 0 are 0.

Top = delete a's letter, left = insert b's letter, diagonal = replace (free on a match). Purple = the cells read.

Edit distance: cheapest of three movesa = "horse", b = "ros"step 1/16
dp[i][j] = fewest edits: first i of a → first j of b
∅
r
o
s
∅
0
1
2
3
h
1
o
2
r
3
s
4
e
5
Base row / column

Turning "" into j letters takes j inserts (row ∅); turning i letters into "" takes i deletes (column ∅).

C

How do I recognise it?

  • Two strings (or arrays) and "longest common subsequence / substring".

  • "Minimum operations to turn A into B" — insert, delete, replace (edit distance), or deletions with a cost.

  • Matching a string against a pattern with ?, * or . — the answer is true / false.

  • "Can s3 be formed by interleaving s1 and s2".

  • "Palindromic substrings / subsequence", "fewest insertions / cuts to make palindromes".

  • "Number of distinct subsequences of s equal to t".

Looks like DP — Strings, but isn't

Is t a subsequence of s? (yes / no)
→ Two pointers

A greedy walk with one pointer per string is enough — matching the first possible character is always safe. DP is for longest / count / cost.

Find all occurrences of a pattern without wildcards
→ Sliding window / string matching (KMP, hashing)

An exact pattern needs no choices, so no table of prefixes.

Longest substring without repeating characters
→ Sliding Window

One string and a validity rule on a window; nothing is compared prefix-to-prefix.

Group anagrams / check anagram
→ Hashing (counts)

Order doesn't matter there, and DP is all about order.

D

How would I discover it from scratch?

Don't memorise “use dp — strings”. Ask these questions in order — the pattern falls out.

Recurse on the last characters: lcs(i, j) = match ? 1 + lcs(i − 1, j − 1) : max(lcs(i − 1, j), lcs(i, j − 1)). Each mismatch branches in two → about O(2^(N+M)).

E

Sub-patterns

Same core idea, different rule for “valid”. Learn them in this order.

F

Don't mix these up

Longest common subsequence vs Longest common substring

Gaps allowed → mismatch takes max(top, left), answer in the corner. No gaps → mismatch is 0, answer is the max cell.

Edit distance vs n + m − 2·LCS

Without replace (only insert / delete) they are equal. With replace, one replace does the job of a delete plus an insert, so edit distance can be smaller.

Wildcard `*` vs Regex `*`

Wildcard * matches any sequence by itself. Regex x* means zero or more copies of x — the star belongs to the character before it.

Palindromic subsequence vs Palindromic substring

Subsequence = LCS(s, reverse s) works. Substring: a common substring of s and its reverse need not be a palindrome — use centres or the s[i..j] table.

G

Code templates (C++)

LCS (two rows)

Match → diagonal + 1, otherwise max(top, left). Variants change the base row and the combine: deletions, supersequence, counts (+).

lcs-template.cpp
// LCS with two rows: dp[i][j] = LCS of the first i chars of a and the first j of b.
int lcs(const string& a, const string& b) {
    int n = a.size(), m = b.size();
    vector<int> prev(m + 1, 0), cur(m + 1, 0);       // row 0 = empty prefix of a
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++)
            cur[j] = a[i - 1] == b[j - 1] ? prev[j - 1] + 1         // both last chars used
                                          : max(prev[j], cur[j - 1]); // drop one of them
        swap(prev, cur);
    }
    return prev[m];
}

Edit distance

Save the old diagonal before overwriting. Base row/column are j and i. Wildcard / regex matching use the same loop with booleans.

edit-distance-template.cpp
// Edit distance in one row: fewest insert / delete / replace to turn a into b.
int editDistance(const string& a, const string& b) {
    int n = a.size(), m = b.size();
    vector<int> dp(m + 1);
    for (int j = 0; j <= m; j++) dp[j] = j;          // "" -> b[0..j): j inserts
    for (int i = 1; i <= n; i++) {
        int diag = dp[0];                            // dp[i - 1][0]
        dp[0] = i;                                   // a[0..i) -> "": i deletes
        for (int j = 1; j <= m; j++) {
            int top = dp[j];                         // dp[i - 1][j]
            if (a[i - 1] == b[j - 1]) dp[j] = diag;  // last chars match: free
            else dp[j] = 1 + min({top,               // delete a[i - 1]
                                  dp[j - 1],         // insert b[j - 1]
                                  diag});            // replace
            diag = top;
        }
    }
    return dp[m];
}

Longest common substring

A mismatch resets to 0 and the answer is the best cell anywhere. One row works if j goes high → low.

common-substring-template.cpp
// Longest common substring: dp[j] = length of the common run ending at a[i - 1] and b[j - 1].
// A mismatch breaks the run (0). j goes high -> low so dp[j - 1] is still the previous row.
int longestCommonSubstring(const string& a, const string& b) {
    int m = b.size(), best = 0;
    vector<int> dp(m + 1, 0);
    for (size_t i = 1; i <= a.size(); i++)
        for (int j = m; j >= 1; j--) {
            dp[j] = a[i - 1] == b[j - 1] ? dp[j - 1] + 1 : 0;
            best = max(best, dp[j]);                 // the answer can end anywhere
        }
    return best;
}

Palindromes by expanding centres

2N − 1 centres (each character, and each gap). Count every expansion for "how many", keep the widest for "longest".

palindrome-template.cpp
// Expand around each of the 2N - 1 centres (a character, or the gap between two).
// Every successful expansion is one more palindromic substring.
int countPalindromes(const string& s, int& bestStart, int& bestLen) {
    int n = s.size(), count = 0;
    bestStart = 0; bestLen = n ? 1 : 0;
    for (int centre = 0; centre < 2 * n - 1; centre++) {
        int l = centre / 2, r = l + centre % 2;      // odd: l == r, even: r = l + 1
        while (l >= 0 && r < n && s[l] == s[r]) {
            count++;
            if (r - l + 1 > bestLen) { bestLen = r - l + 1; bestStart = l; }
            l--; r++;
        }
    }
    return count;                                    // longest = s.substr(bestStart, bestLen)
}