Maximize Palindrome Length From Subsequences
You are given two strings word1 and word2. You want to construct a string by selecting a subsequence from word1 and a subsequence from word2, then concatenating them. The goal is to make that concatenated string a palindrome. You must use at least one character from word1 and at least one from word2. Return the length of the longest palindrome you can build this way, or 0 if it is impossible.
This is LeetCode 1771: Maximize Palindrome Length From Subsequences. It looks like a brand-new problem at first, but the core is something you already know: the Longest Palindromic Subsequence. The twist is that your palindrome must span two strings, pulling characters from both.
Why this problem matters
LPS on a single string is a classic interval DP exercise. This problem forces you to extend that technique to a setting with constraints. You cannot just find the longest palindromic subsequence of the combined string and call it done. You need to guarantee that the palindrome actually uses characters from both halves. That extra constraint is what makes this a hard problem instead of a medium one.
In interviews, this pattern shows up whenever you need to run a known DP algorithm on a modified input while enforcing an external condition. If you can handle LPS with a boundary constraint, you can handle similar variations on LCS, edit distance, and other two-string DP problems.
The key insight
Concatenate word1 and word2 into a single string s = word1 + word2. Now run the standard LPS interval DP on s. The DP table dp[i][j] gives the longest palindromic subsequence of s[i..j].
But you do not just want dp[0][len(s)-1]. You need the palindrome to include at least one character from word1 (the first n1 characters of s) and at least one from word2 (the last n2 characters of s). The trick is to track the answer during the DP fill. Whenever you find a match where s[i] == s[j] with i in the word1 range (i < n1) and j in the word2 range (j >= n1), you know that palindrome uses at least one character from each word. Update the result with dp[i][j].
You do not need a separate pass to check the boundary condition. Just update your answer inside the DP loop whenever the matching pair crosses the word1/word2 boundary. If s[i] == s[j] with i in the word1 portion and j in the word2 portion, the palindromic subsequence from i to j is guaranteed to use at least one character from each word.
The solution
def longest_palindrome(word1, word2):
s = word1 + word2
n = len(s)
n1 = len(word1)
dp = [[0] * n for _ in range(n)]
result = 0
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
if i < n1 and j >= n1:
result = max(result, dp[i][j])
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
return result
The outer loop iterates over substring lengths from 2 up to n. For each length, the inner loop considers every starting index i. When s[i] == s[j], we extend the palindrome by wrapping those two characters around dp[i+1][j-1]. When they do not match, we take the better of dropping the left character or dropping the right character. The result variable only gets updated when a matching pair crosses the boundary between word1 and word2.
When length == 2 and s[i] == s[j], the formula gives dp[i+1][j-1] + 2 = dp[i+1][i] + 2. Since i+1 > i, that cell is 0 (never filled), which is correct: two matching characters form a palindrome of length 2, and 0 + 2 = 2.
Visual walkthrough
Step 1: Concatenate and initialize. s = word1 + word2 = "ab" + "ba" = "abba"
Base case: every single character is a palindrome of length 1. dp[i][i] = 1 for all i. Green labels are word1 indices, orange are word2.
Step 2: Fill length-2 substrings. Check adjacent pairs.
dp[0][1]: s[0]="a" != s[1]="b", so max(dp[1][1], dp[0][0]) = 1. dp[1][2]: s[1]="b" == s[2]="b", so dp[2][1]+2 = 0+2 = 2. This pair crosses the boundary!
Step 3: Fill length-3 and length-4 substrings.
dp[0][2]: s[0]="a" != s[2]="b", max(dp[1][2], dp[0][1]) = 2. dp[1][3]: s[1]="b" != s[3]="a", max(dp[2][3], dp[1][2]) = 2. dp[0][3]: s[0]="a" == s[3]="a", dp[1][2]+2 = 4. This crosses the boundary!
Step 4: Find the best cross-boundary palindrome.
Track max dp[i][j] whenever s[i] == s[j], i is in the word1 range (i < 2), and j is in the word2 range (j >= 2). dp[1][2] = 2 and dp[0][3] = 4 both qualify. The answer is 4, corresponding to the palindrome "abba".
In this example, word1 = "ab" and word2 = "ba", so s = "abba". The standard LPS of "abba" is 4 (the entire string). The cross-boundary check catches this at dp[0][3] because s[0] = 'a' is in word1 and s[3] = 'a' is in word2. It also catches dp[1][2] where s[1] = 'b' is in word1 and s[2] = 'b' is in word2, giving a palindrome of length 2. The answer is 4.
Complexity analysis
| Approach | Time | Space |
|---|---|---|
| LPS DP on concatenation | O((n+m)^2) | O((n+m)^2) |
Where n = len(word1) and m = len(word2). The DP table is (n+m) x (n+m), and we fill roughly half of it (the upper triangle).
The building blocks
1. Longest Palindromic Subsequence (interval DP)
The core algorithm that makes everything work. Given a string s, dp[i][j] stores the LPS of s[i..j].
def lps(s):
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])
return dp[0][n - 1]
This is the standard LPS you should know cold. The only change for our problem is concatenating two strings first and tracking cross-boundary matches.
2. Cross-boundary tracking
The constraint that the palindrome must use characters from both words translates to: when s[i] == s[j], check if i is in the word1 range and j is in the word2 range.
if s[i] == s[j]:
dp[i][j] = dp[i + 1][j - 1] + 2
if i < len(word1) and j >= len(word1):
result = max(result, dp[i][j])
This is the only addition to the standard LPS algorithm. It costs nothing in terms of time or space complexity, just a single comparison and a max update.
Edge cases
- No common character: if
word1andword2share no character in common, no cross-boundary palindrome exists. The result stays 0. For example,word1 = "aa"andword2 = "bb"returns 0. - Single character each:
word1 = "a"andword2 = "a"givess = "aa", anddp[0][1] = 2. Sinces[0]is inword1ands[1]is inword2, the answer is 2. - One string empty in spirit: the problem guarantees both strings have length at least 1, so you do not need to handle truly empty inputs.
- Entire concatenation is a palindrome:
word1 = "ab"andword2 = "ba"givess = "abba". The full LPS is 4, and the outermost matching pair crosses the boundary. Answer is 4.
From understanding to recall
The pattern here is clean: take a known algorithm (LPS), run it on a modified input (concatenated strings), and add a constraint check during the fill (cross-boundary match). Recognizing that pattern is the hard part. Once you see that concatenation turns two strings into one, the rest is standard interval DP with a single if guard.
To solidify this, practice identifying when a problem can be reduced to a known DP by reshaping the input. LeetCode 1771 is a concatenation reduction. Other problems use reversal (LPS as LCS of s and reverse(s)), interleaving, or padding. The underlying DP is always something you already know.
Related posts
- Longest Palindromic Subsequence - the foundational interval DP that this problem extends
- Longest Palindromic Substring - contiguous variant, different approach but same palindrome intuition
- Edit Distance - another classic 2D string DP with a similar table structure
If you want to build lasting fluency with palindrome DP and other string problems, CodeBricks uses spaced repetition to move these patterns from short-term cramming into long-term recall. Instead of solving LeetCode 1771 once and forgetting the boundary trick, you revisit it at optimally spaced intervals until the reduction is automatic.