2370. Longest Ideal Subsequence - Explanation
Description
Choose a subsequence of s such that adjacent chosen letters differ by at most k positions in the alphabet. Return the greatest possible length. A subsequence preserves the original order but may skip characters.
Alphabet distance does not wrap around: the distance from a to z is 25.
Example 1:
Input: s = "azaz"
k = 0
Output: 2Explanation: When k is zero, adjacent chosen letters must be equal. Choosing either the two a characters or the two z characters gives length 2.
Example 2:
Input: s = "bdac"
k = 1
Output: 2Explanation: A subsequence such as "ba" or "dc" has length 2 and adjacent distance 1. No three characters in their original order satisfy the distance limit.
Example 3:
Input: s = "az"
k = 25
Output: 2Explanation: With k = 25, a and z may be adjacent. Keeping both characters gives length 2.
Constraints:
1 <= s.length <= 100000scontains only lowercase English letters.0 <= k <= 25
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Dynamic Programming (1D) - The core approach involves building optimal subsequences by tracking states for each character position
- Recursion with Memoization - Understanding how to convert brute-force recursion into efficient top-down DP
- ASCII Character Manipulation - Working with character codes to calculate alphabetical distances between letters
1. Recursion
Intuition
An "ideal" subsequence requires that consecutive characters differ by at most k in their alphabetical positions. For each character in the string, we have two choices: skip it or include it (if it satisfies the constraint with the previous character). This decision tree naturally maps to a recursive approach where we try both options and take the maximum.
Algorithm
- Define
dfs(i, prev)whereiis the current index andprevis the last included character (or a sentinel for none). - Base case: If
ireaches the end, return0. - Option 1: Skip the current character and recurse with
dfs(i + 1, prev). - Option 2: If
previs empty or the absolute difference between current and previous character is at mostk, include it with1+dfs(i + 1, s[i]). - Return the maximum of both options.
class Solution:
def longestIdealString(self, s: str, k: int) -> int:
def dfs(i, prev):
if i == len(s):
return 0
skip = dfs(i + 1, prev)
include = 0
if prev == "" or abs(ord(s[i]) - ord(prev)) <= k:
include = 1 + dfs(i + 1, s[i])
return max(skip, include)
return dfs(0, "")Time & Space Complexity
- Time complexity:
- Space complexity: for recursion stack.
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution has overlapping subproblems since the same (index, previous character) pair can be reached through different paths. By caching results in a 2D table indexed by position and the previous character, we avoid redundant computation. Since there are only 26 possible previous characters (plus a "none" state), the state space is manageable.
Algorithm
- Create a cache of size
nx27(26letters plus one for "no previous"). - Convert the previous character to an index (
0to25, or use an offset for the "none" case). - Before computing, check if the result is cached.
- Compute using the same logic as plain recursion, store in cache, and return.
class Solution:
def longestIdealString(self, s: str, k: int) -> int:
cache = [[-1] * 27 for _ in range(len(s))]
def dfs(i, prev):
if i == len(s):
return 0
if cache[i][prev + 1] != -1:
return cache[i][prev + 1]
skip = dfs(i + 1, prev)
include = 0
c = ord(s[i]) - ord('a')
if prev == -1 or abs(c - prev) <= k:
include = 1 + dfs(i + 1, c)
cache[i][prev + 1] = max(skip, include)
return cache[i][prev + 1]
return dfs(0, -1)Time & Space Complexity
- Time complexity:
- Space complexity:
3. Dynamic Programming (Bottom-Up)
Intuition
We can fill a table iteratively instead of recursively. For each position i and each possible "last character" prev, we compute the longest ideal subsequence. We propagate values forward: either keep the previous state unchanged (skip current character) or extend it if the current character is within k of prev.
Algorithm
- Create a 2D array
dp[i][prev]of size(n+1)x26. - For each index
ifrom1ton:- Get the current character as an index
curr. - For each
prevfrom0to25:- Carry forward
dp[i-1][prev]todp[i][prev]. - If
|curr - prev|<=k, updatedp[i][curr]=max(dp[i][curr], 1 + dp[i-1][prev]).
- Carry forward
- Get the current character as an index
- Return the maximum value in
dp[n].
class Solution:
def longestIdealString(self, s: str, k: int) -> int:
dp = [[0] * 26 for _ in range(len(s) + 1)]
for i in range(1, len(s) + 1):
curr = ord(s[i - 1]) - ord('a')
for prev in range(26):
dp[i][prev] = max(dp[i][prev], dp[i - 1][prev])
if abs(curr - prev) <= k:
dp[i][curr] = max(dp[i][curr], 1 + dp[i - 1][prev])
return max(dp[len(s)])Time & Space Complexity
- Time complexity:
- Space complexity:
4. Dynamic Programming (Space Optimized)
Intuition
Since we only need to know the longest subsequence ending at each character, we can use a single array of size 26. For each character in the string, we look at all characters within distance k and take the maximum length, then update the entry for the current character. This reduces space from O(n) to O(1) (constant 26 entries).
Algorithm
- Initialize an array
dpof size 26 with zeros. - For each character
cin the string:- Convert
cto indexcurr. - Find the maximum value in
dp[prev]for allprevwhere|curr - prev| <= k. - Set
dp[curr] = max(dp[curr], 1 + maxFound).
- Convert
- Return the maximum value in
dp.
class Solution:
def longestIdealString(self, s: str, k: int) -> int:
dp = [0] * 26
for c in s:
curr = ord(c) - ord('a')
longest = 1
for prev in range(26):
if abs(curr - prev) <= k:
longest = max(longest, 1 + dp[prev])
dp[curr] = max(dp[curr], longest)
return max(dp)Time & Space Complexity
- Time complexity:
- Space complexity: since we have at most 26 different characters.
Common Pitfalls
Confusing Subsequence with Substring
A subsequence does not require elements to be contiguous in the original string. Some solutions incorrectly enforce that characters must be adjacent in the input, which is the definition of a substring. Remember that you can skip characters while maintaining relative order.
Off-by-One Errors in Character Distance Calculation
When checking if the difference between two characters is at most k, ensure you use the absolute value of the difference between their positions in the alphabet. A common mistake is comparing ASCII values directly without converting to 0-indexed positions, or forgetting to take the absolute value when the current character is alphabetically before the previous one.
Not Considering All Valid Previous Characters
When computing the longest subsequence ending at a character, you must consider all previous characters within distance k, not just the immediately preceding one in the string. The optimal extension might come from any character in the range [curr - k, curr + k], and failing to check the entire range leads to suboptimal answers.
Sign in to join the discussion