2370. Longest Ideal Subsequence
Problem Description
You are given a string s
containing only lowercase letters and an integer k
. Your task is to find the length of the longest "ideal string" that can be formed.
An ideal string t
must satisfy two conditions:
t
must be a subsequence ofs
(you can obtaint
by deleting some or no characters froms
without changing the order of remaining characters)- For every pair of adjacent letters in
t
, their absolute difference in alphabetical position must be at mostk
The alphabetical position difference is calculated as the absolute difference between the positions of letters in the alphabet. For example:
- The difference between 'a' (position 1) and 'c' (position 3) is
|1 - 3| = 2
- The difference between 'b' (position 2) and 'e' (position 5) is
|2 - 5| = 3
- The difference between 'a' (position 1) and 'z' (position 26) is
|1 - 26| = 25
Note that the alphabet is not cyclic - 'a' and 'z' are 25 positions apart, not 1.
Example:
If s = "acfgbd"
and k = 2
:
- "acf" is NOT ideal because 'c' and 'f' have a difference of 3 (> 2)
- "ab" is ideal because 'a' and 'b' have a difference of 1 (≤ 2)
- "acd" is ideal because 'a' and 'c' have difference 2, and 'c' and 'd' have difference 1
The goal is to return the length of the longest possible ideal string.
Intuition
This problem asks for the longest subsequence with a constraint on adjacent elements, which immediately suggests dynamic programming. The key insight is that we need to track the longest ideal string ending at each position.
For each character in the string, we need to decide: should we start a new ideal string from this character, or extend an existing ideal string? To extend an existing ideal string, the current character must be within k
distance from the last character of that string.
The challenge is efficiently finding which previous characters can connect to our current character. Since we're dealing with lowercase letters (only 26 possible values), we can check all possible previous characters that are within k
distance. For the current character with ASCII value a
, any character with ASCII value in the range [a-k, a+k]
can be its predecessor in an ideal string.
However, when multiple positions have the same character, we only need to remember the most recent one, as it will have the maximum dp
value among all occurrences of that character (since dp
values are non-decreasing as we process the string left to right).
This leads to the approach:
- Use
dp[i]
to store the length of the longest ideal string ending at positioni
- Maintain a dictionary
d
that maps each character to its most recent position in the string - For each position
i
, check all 26 lowercase letters to see which ones are withink
distance and have appeared before - Update
dp[i]
based on the best predecessor found - Update the dictionary with the current character's position
The time complexity becomes O(26 * n)
which is effectively O(n)
since 26 is a constant, making this solution efficient even for large strings.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses dynamic programming with a character-to-position mapping optimization. Here's how the implementation works:
Data Structures:
dp[i]
: Stores the length of the longest ideal string ending at positioni
d
: Dictionary mapping each character to its most recent position in the string
Algorithm Steps:
-
Initialize the base case:
dp = [1] * n # Each character can start an ideal string of length 1 d = {s[0]: 0} # Store the first character's position
-
Process each character from position 1 to n-1:
for i in range(1, n): a = ord(s[i]) # Get ASCII value of current character
-
Check all possible predecessor characters:
for b in ascii_lowercase: if abs(a - ord(b)) > k: continue # Skip if distance exceeds k
For the current character
s[i]
, we check all 26 lowercase letters. If a letterb
has an absolute difference greater thank
froms[i]
, it cannot be a valid predecessor. -
Update dp[i] if valid predecessor exists:
if b in d: dp[i] = max(dp[i], dp[d[b]] + 1)
If character
b
has appeared before (exists in dictionaryd
), we can extend the ideal string ending atd[b]
by adding the current character. We take the maximum to ensure we get the longest possible ideal string. -
Update the dictionary with current character's position:
d[s[i]] = i
We always update to the most recent position because it will have the best (largest)
dp
value for that character. -
Return the maximum value in dp array:
return max(dp)
The answer is the maximum length among all ideal strings ending at different positions.
Example Walkthrough:
For s = "acbd"
and k = 2
:
- Position 0 ('a'):
dp[0] = 1
,d = {'a': 0}
- Position 1 ('c'): Check 'a', 'b', 'c', 'd', 'e' (within distance 2)
- 'a' exists in
d
,dp[1] = dp[0] + 1 = 2
- Update
d = {'a': 0, 'c': 1}
- 'a' exists in
- Position 2 ('b'): Check 'a', 'b', 'c', 'd' (within distance 2)
- 'a' exists:
dp[2] = max(1, dp[0] + 1) = 2
- 'c' exists:
dp[2] = max(2, dp[1] + 1) = 3
- Update
d = {'a': 0, 'c': 1, 'b': 2}
- 'a' exists:
- Result:
max(dp) = 3
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with s = "acfbd"
and k = 2
.
We'll track our dp
array and dictionary d
at each step:
Initial Setup:
dp = [1, 1, 1, 1, 1]
(each character can start an ideal string of length 1)d = {'a': 0}
(first character 'a' at position 0)
Position 1 - character 'c':
- Current character 'c' has ASCII value 99
- Check all 26 letters to find valid predecessors (within distance k=2):
- 'a' (ASCII 97): |99-97| = 2 ≤ 2 ✓ and 'a' exists in d at position 0
- 'b' (ASCII 98): |99-98| = 1 ≤ 2 ✓ but 'b' not in d yet
- 'c' (ASCII 99): |99-99| = 0 ≤ 2 ✓ but 'c' not in d yet
- 'd' (ASCII 100): |99-100| = 1 ≤ 2 ✓ but 'd' not in d yet
- 'e' (ASCII 101): |99-101| = 2 ≤ 2 ✓ but 'e' not in d yet
- All other letters: distance > 2, skip
- Update:
dp[1] = max(1, dp[0] + 1) = 2
(extend from 'a') - Update dictionary:
d = {'a': 0, 'c': 1}
Position 2 - character 'f':
- Current character 'f' has ASCII value 102
- Valid predecessors (within distance 2):
- 'd' (ASCII 100): |102-100| = 2 ≤ 2 ✓ but not in d
- 'e' (ASCII 101): |102-101| = 1 ≤ 2 ✓ but not in d
- 'f' (ASCII 102): |102-102| = 0 ≤ 2 ✓ but not in d
- 'g' (ASCII 103): |102-103| = 1 ≤ 2 ✓ but not in d
- 'h' (ASCII 104): |102-104| = 2 ≤ 2 ✓ but not in d
- No valid predecessors exist in d
dp[2] = 1
(start new ideal string)- Update dictionary:
d = {'a': 0, 'c': 1, 'f': 2}
Position 3 - character 'b':
- Current character 'b' has ASCII value 98
- Valid predecessors (within distance 2):
- 'a' (ASCII 97): |98-97| = 1 ≤ 2 ✓ and exists at position 0
- 'b' (ASCII 98): |98-98| = 0 ≤ 2 ✓ but not in d yet
- 'c' (ASCII 99): |98-99| = 1 ≤ 2 ✓ and exists at position 1
- 'd' (ASCII 100): |98-100| = 2 ≤ 2 ✓ but not in d
- Update:
dp[3] = max(1, dp[0]+1, dp[1]+1) = max(1, 2, 3) = 3
- Update dictionary:
d = {'a': 0, 'c': 1, 'f': 2, 'b': 3}
Position 4 - character 'd':
- Current character 'd' has ASCII value 100
- Valid predecessors (within distance 2):
- 'b' (ASCII 98): |100-98| = 2 ≤ 2 ✓ and exists at position 3
- 'c' (ASCII 99): |100-99| = 1 ≤ 2 ✓ and exists at position 1
- 'd' (ASCII 100): |100-100| = 0 ≤ 2 ✓ but not in d yet
- 'e' (ASCII 101): |100-101| = 1 ≤ 2 ✓ but not in d
- 'f' (ASCII 102): |100-102| = 2 ≤ 2 ✓ and exists at position 2
- Update:
dp[4] = max(1, dp[3]+1, dp[1]+1, dp[2]+1) = max(1, 4, 3, 2) = 4
- Update dictionary:
d = {'a': 0, 'c': 1, 'f': 2, 'b': 3, 'd': 4}
Final Result:
dp = [1, 2, 1, 3, 4]
max(dp) = 4
The longest ideal string has length 4, which corresponds to the subsequence "acbd" where:
- 'a' to 'c': difference = 2 ≤ 2 ✓
- 'c' to 'b': difference = 1 ≤ 2 ✓
- 'b' to 'd': difference = 2 ≤ 2 ✓
Solution Implementation
1class Solution:
2 def longestIdealString(self, s: str, k: int) -> int:
3 """
4 Find the longest subsequence where consecutive characters have
5 ASCII values within distance k of each other.
6
7 Args:
8 s: Input string
9 k: Maximum allowed ASCII distance between consecutive characters
10
11 Returns:
12 Length of the longest ideal subsequence
13 """
14 from string import ascii_lowercase
15
16 n = len(s)
17
18 # dp[i] stores the length of longest ideal subsequence ending at index i
19 dp = [1] * n
20
21 # last_index tracks the most recent index where each character appears
22 last_index = {s[0]: 0}
23
24 # Process each character starting from index 1
25 for i in range(1, n):
26 current_char_ascii = ord(s[i])
27
28 # Check all lowercase letters to find valid predecessors
29 for candidate_char in ascii_lowercase:
30 candidate_ascii = ord(candidate_char)
31
32 # Skip characters outside the allowed ASCII distance
33 if abs(current_char_ascii - candidate_ascii) > k:
34 continue
35
36 # If this character appeared before, try extending its subsequence
37 if candidate_char in last_index:
38 prev_index = last_index[candidate_char]
39 dp[i] = max(dp[i], dp[prev_index] + 1)
40
41 # Update the last occurrence index for current character
42 last_index[s[i]] = i
43
44 # Return the maximum length found
45 return max(dp)
46
1class Solution {
2 public int longestIdealString(String s, int k) {
3 int length = s.length();
4 int maxLength = 1;
5
6 // dp[i] stores the length of longest ideal string ending at index i
7 int[] dp = new int[length];
8 Arrays.fill(dp, 1);
9
10 // lastIndexMap stores the most recent index where each character appeared
11 Map<Character, Integer> lastIndexMap = new HashMap<>(26);
12 lastIndexMap.put(s.charAt(0), 0);
13
14 // Iterate through each character starting from index 1
15 for (int i = 1; i < length; ++i) {
16 char currentChar = s.charAt(i);
17
18 // Check all possible characters that can form an ideal string with currentChar
19 for (char candidateChar = 'a'; candidateChar <= 'z'; ++candidateChar) {
20 // Skip if the absolute difference exceeds k
21 if (Math.abs(currentChar - candidateChar) > k) {
22 continue;
23 }
24
25 // If candidateChar exists in our map, try to extend the ideal string
26 if (lastIndexMap.containsKey(candidateChar)) {
27 int lastIndex = lastIndexMap.get(candidateChar);
28 dp[i] = Math.max(dp[i], dp[lastIndex] + 1);
29 }
30 }
31
32 // Update the last occurrence index of currentChar
33 lastIndexMap.put(currentChar, i);
34
35 // Update the maximum length found so far
36 maxLength = Math.max(maxLength, dp[i]);
37 }
38
39 return maxLength;
40 }
41}
42
1class Solution {
2public:
3 int longestIdealString(string s, int k) {
4 int stringLength = s.size();
5 int maxLength = 1;
6
7 // dp[i] represents the length of longest ideal string ending at index i
8 vector<int> dp(stringLength, 1);
9
10 // lastIndexMap stores the most recent index where each character appears
11 unordered_map<char, int> lastIndexMap;
12 lastIndexMap[s[0]] = 0;
13
14 // Iterate through each character starting from index 1
15 for (int i = 1; i < stringLength; ++i) {
16 char currentChar = s[i];
17
18 // Check all possible previous characters within the allowed difference k
19 for (char prevChar = 'a'; prevChar <= 'z'; ++prevChar) {
20 // Skip if the absolute difference exceeds k
21 if (abs(currentChar - prevChar) > k) {
22 continue;
23 }
24
25 // If this character has appeared before, try to extend the ideal string
26 if (lastIndexMap.count(prevChar)) {
27 int prevIndex = lastIndexMap[prevChar];
28 dp[i] = max(dp[i], dp[prevIndex] + 1);
29 }
30 }
31
32 // Update the last occurrence index of current character
33 lastIndexMap[currentChar] = i;
34
35 // Update the maximum length found so far
36 maxLength = max(maxLength, dp[i]);
37 }
38
39 return maxLength;
40 }
41};
42
1/**
2 * Finds the longest ideal string subsequence where adjacent characters
3 * have absolute difference at most k
4 * @param s - Input string to process
5 * @param k - Maximum allowed absolute difference between adjacent characters
6 * @returns Length of the longest ideal subsequence
7 */
8function longestIdealString(s: string, k: number): number {
9 // Dynamic programming array to store the longest ideal subsequence
10 // ending with each letter (a-z represented by indices 0-25)
11 const dpLongestLength: number[] = new Array(26).fill(0);
12
13 // Process each character in the input string
14 for (const currentChar of s) {
15 // Convert character to index (0-25 for a-z)
16 const currentCharIndex: number = currentChar.charCodeAt(0) - 'a'.charCodeAt(0);
17
18 // Find the maximum length of ideal subsequence that can be formed
19 // by appending the current character
20 let maxLengthWithCurrentChar: number = 0;
21
22 // Check all possible previous characters that can precede current character
23 for (let previousCharIndex = 0; previousCharIndex < 26; previousCharIndex++) {
24 // Check if the absolute difference is within the allowed range k
25 if (Math.abs(currentCharIndex - previousCharIndex) <= k) {
26 // Update max length by extending the subsequence ending at previousCharIndex
27 maxLengthWithCurrentChar = Math.max(
28 maxLengthWithCurrentChar,
29 dpLongestLength[previousCharIndex] + 1
30 );
31 }
32 }
33
34 // Update the longest subsequence ending with current character
35 dpLongestLength[currentCharIndex] = Math.max(
36 dpLongestLength[currentCharIndex],
37 maxLengthWithCurrentChar
38 );
39 }
40
41 // Return the maximum length among all possible ending characters
42 return dpLongestLength.reduce(
43 (maxLength: number, currentLength: number) => Math.max(maxLength, currentLength),
44 0
45 );
46}
47
Time and Space Complexity
Time Complexity: O(n × 26)
= O(n)
The algorithm iterates through the string once with the outer loop running from index 1 to n-1, which takes O(n)
time. For each character at position i, the inner loop iterates through all 26 lowercase letters (assuming ascii_lowercase
contains 'a' to 'z'). For each letter, it performs constant time operations: checking the absolute difference, dictionary lookup, and updating the dp array. Since the inner loop runs exactly 26 times regardless of input size, the overall time complexity is O(n × 26)
which simplifies to O(n)
.
Space Complexity: O(n)
The algorithm uses:
- A dp array of size n to store the longest ideal string ending at each position:
O(n)
- A dictionary d that stores at most 26 entries (one for each lowercase letter) with the most recent index of each character:
O(26)
=O(1)
- A few integer variables (n, ans, a, b):
O(1)
The dominant space usage is the dp array, making the overall space complexity O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Not Updating Character Positions Correctly
Issue: A common mistake is trying to track all positions where each character appears, rather than just the most recent position. This leads to unnecessary complexity and potential errors.
Incorrect Approach:
# Trying to maintain all positions for each character
positions = defaultdict(list)
for i in range(n):
positions[s[i]].append(i)
# Then checking all positions - inefficient!
Why it's problematic:
- You don't need to check all previous occurrences of a character
- The most recent occurrence will always have the best (largest) dp value for that character
- This increases time complexity unnecessarily
Correct Solution:
# Only track the most recent position last_index[s[i]] = i # Always overwrite with current position
Pitfall 2: Incorrect Distance Calculation
Issue: Confusing character distance with index distance, or incorrectly calculating alphabetical distance.
Incorrect Approaches:
# Wrong: Using index distance instead of character distance
if abs(i - j) > k: # This checks position distance, not character distance
# Wrong: Subtracting characters directly
if abs(s[i] - s[j]) > k: # Python doesn't allow direct string subtraction
Correct Solution:
# Convert characters to ASCII values first
if abs(ord(s[i]) - ord(candidate_char)) > k:
Pitfall 3: Forgetting Base Case Initialization
Issue: Not initializing dp array properly or forgetting to handle the first character.
Incorrect Approach:
dp = [0] * n # Wrong: each character can form a subsequence of length 1 # or forgetting to initialize the dictionary d = {} # Missing first character
Correct Solution:
dp = [1] * n # Each character is at least a subsequence of length 1 last_index = {s[0]: 0} # Initialize with first character's position
Pitfall 4: Inefficient Character Checking
Issue: Only checking characters that appear in the string, missing potential valid predecessors.
Incorrect Approach:
# Only checking characters that exist in string s
for j in range(i):
if abs(ord(s[i]) - ord(s[j])) <= k:
dp[i] = max(dp[i], dp[j] + 1)
Why it's problematic:
- This approach has O(n²) time complexity for each position
- It might miss optimal solutions when intermediate characters don't appear in the string
Correct Solution:
# Check all 26 possible characters
for candidate_char in ascii_lowercase:
if abs(ord(s[i]) - ord(candidate_char)) > k:
continue
if candidate_char in last_index:
dp[i] = max(dp[i], dp[last_index[candidate_char]] + 1)
This ensures O(26n) = O(n) complexity and considers all valid predecessor characters.
You are given an array of intervals where intervals[i] = [start_i, end_i]
represent the start and end of the ith
interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!