2663. Lexicographically Smallest Beautiful String
Problem Description
You are given a string s
of length n
that is "beautiful" and a positive integer k
. A beautiful string has two properties:
- It only uses the first
k
letters of the English lowercase alphabet (a, b, c, ...). - It contains no palindromic substrings of length 2 or more.
The second condition means:
- No two adjacent characters can be the same (this would form a palindrome of length 2)
- No character can be the same as the character two positions before it (this would form a palindrome of length 3)
Your task is to find the lexicographically smallest string that:
- Has the same length
n
as the input strings
- Is lexicographically larger than
s
- Is also a beautiful string (satisfies both conditions above)
If no such string exists, return an empty string.
For example, if s = "abcz"
and k = 26
, you need to find the next beautiful string after "abcz" in lexicographic order. The string must still only use the first 26 letters (a-z) and avoid any palindromic substrings.
The solution works by iterating from the end of the string backwards, trying to increment each character to the next valid option that doesn't create a palindrome with its previous characters. Once a position is found where we can increment, we fill the remaining positions with the lexicographically smallest valid characters.
Intuition
To find the next lexicographically larger beautiful string, we need to think about how we normally find the next lexicographic string - we increment from the rightmost position possible. However, the beauty constraint adds complexity: we can't use just any character, as it might create a palindrome.
The key insight is understanding what makes a string "not beautiful":
- If two adjacent characters are the same, we get a palindrome like "aa"
- If two characters with one character between them are the same, we get a palindrome like "aba"
This means each character must be different from its previous two neighbors. So at any position, we have at most k-2
valid choices (excluding the two previous characters).
Why search from right to left? Consider how we increment numbers: to get the next number after 1299, we can't change the rightmost 9s to anything valid (they're already at maximum), so we move left until we find a digit we can increment (the 2 becomes 3, giving us 1300). Similarly, we need to find the rightmost position where we can place a larger character while maintaining the beauty property.
Once we find such a position i
where we can place a larger valid character:
- We update position
i
with the smallest valid character that's larger than the current one - For all positions after
i
, we greedily fill them with the smallest possible valid characters (starting from 'a', 'b', etc., skipping any that would create palindromes)
This greedy approach after position i
ensures we get the lexicographically smallest result among all valid beautiful strings larger than s
.
If we traverse the entire string without finding any position where we can increment (meaning the string is already at its "maximum" beautiful form for the given length), we return an empty string.
Learn more about Greedy patterns.
Solution Approach
The implementation follows a greedy backtracking strategy with two main phases: finding the increment position and filling the remaining positions.
Phase 1: Finding the Position to Increment
We iterate backwards from the last character (index n-1
) to the first character (index 0
):
- For each position
i
, we get the current character's value:p = ord(cs[i]) - ord('a') + 1
- We try to replace it with a larger character from range
[p, k)
(characters from the next one up to thek
-th letter) - For each candidate character
c
, we check if it violates the beauty constraint:- If
i > 0
andcs[i-1] == c
: skip (would create adjacent duplicates) - If
i > 1
andcs[i-2] == c
: skip (would create a palindrome of length 3)
- If
- If we find a valid character, we assign
cs[i] = c
and move to Phase 2
Phase 2: Filling Remaining Positions
Once we've successfully incremented position i
, we need to fill positions [i+1, n-1]
with the lexicographically smallest valid characters:
- For each position
l
fromi+1
ton-1
:- Try characters from
'a'
to the(k-1)
-th letter in order (range[0, k)
) - For each candidate character
c
:- Check if
l > 0
andcs[l-1] == c
: skip if true - Check if
l > 1
andcs[l-2] == c
: skip if true - If valid, assign
cs[l] = c
and break to move to the next position
- Check if
- Try characters from
- After filling all positions, return the joined string
''.join(cs)
Edge Case:
If we complete the backward iteration without finding any valid position to increment (the outer loop ends without returning), it means no larger beautiful string exists, so we return an empty string ''
.
The time complexity is O(n * k^2)
in the worst case, where we check all positions, try all possible characters at each position, and for each successful increment, fill the remaining positions. The space complexity is O(n)
for storing the character list.
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 finding the next beautiful string after s = "abcb"
with k = 4
(using letters a, b, c, d).
Initial Setup:
- Input string:
"abcb"
- We convert it to a list:
['a', 'b', 'c', 'b']
- We need to find the next lexicographically larger beautiful string
Phase 1: Finding the Position to Increment
Starting from the rightmost position (index 3):
Position 3 (character 'b'):
- Current value:
p = ord('b') - ord('a') + 1 = 2
- Try characters from position 2 to k-1: ['c', 'd']
- Check 'c':
- Is
cs[2] == 'c'
? Yes! This would create adjacent duplicates "cc" - Skip 'c'
- Is
- Check 'd':
- Is
cs[2] == 'd'
? No β - Is
cs[1] == 'd'
? No β - Valid! Set
cs[3] = 'd'
- String becomes:
['a', 'b', 'c', 'd']
- Is
Since we successfully incremented position 3 and it's the last position, we're done!
Result: "abcd"
Let's try a more complex example: s = "abcd"
with k = 4
.
Phase 1: Finding the Position to Increment
Position 3 (character 'd'):
- Current value:
p = 4
- Try characters from position 4 to k-1: No characters available (d is already the 4th letter)
- Can't increment here, move left
Position 2 (character 'c'):
- Current value:
p = 3
- Try character 'd':
- Is
cs[1] == 'd'
? No β - Is
cs[0] == 'd'
? No β - Valid! Set
cs[2] = 'd'
- String becomes:
['a', 'b', 'd', 'd']
- Is
Phase 2: Filling Remaining Positions
Position 3:
- Try 'a':
- Is
cs[2] == 'a'
? No (it's 'd') β - Is
cs[1] == 'a'
? No (it's 'b') β - Valid! Set
cs[3] = 'a'
- String becomes:
['a', 'b', 'd', 'a']
- Is
Result: "abda"
This demonstrates how the algorithm:
- Searches right-to-left for a position to increment
- Checks palindrome constraints when trying new characters
- Fills remaining positions with the smallest valid characters
Solution Implementation
1class Solution:
2 def smallestBeautifulString(self, s: str, k: int) -> str:
3 """
4 Find the lexicographically smallest beautiful string greater than s.
5 A beautiful string has no palindromic substring of length 2 or 3.
6
7 Args:
8 s: Input string containing only lowercase letters
9 k: Size of the alphabet (first k letters from 'a')
10
11 Returns:
12 The smallest beautiful string greater than s, or empty string if none exists
13 """
14 n = len(s)
15 char_list = list(s)
16
17 # Try to increment the string from right to left
18 for i in range(n - 1, -1, -1):
19 # Get the next character value after current character
20 current_char_value = ord(char_list[i]) - ord('a') + 1
21
22 # Try each possible character from current+1 to k-1
23 for next_value in range(current_char_value, k):
24 next_char = chr(ord('a') + next_value)
25
26 # Check if this character would create a palindrome of length 2 or 3
27 # Skip if it matches the previous character (palindrome of length 2)
28 if i > 0 and char_list[i - 1] == next_char:
29 continue
30 # Skip if it matches the character two positions back (palindrome of length 3)
31 if i > 1 and char_list[i - 2] == next_char:
32 continue
33
34 # Valid character found, update it
35 char_list[i] = next_char
36
37 # Fill the remaining positions with the smallest valid characters
38 for pos in range(i + 1, n):
39 # Try each character starting from 'a'
40 for char_value in range(k):
41 candidate_char = chr(ord('a') + char_value)
42
43 # Check for palindrome constraints
44 # Skip if it matches the previous character
45 if pos > 0 and char_list[pos - 1] == candidate_char:
46 continue
47 # Skip if it matches the character two positions back
48 if pos > 1 and char_list[pos - 2] == candidate_char:
49 continue
50
51 # Valid character found for this position
52 char_list[pos] = candidate_char
53 break
54
55 # Successfully constructed a valid beautiful string
56 return ''.join(char_list)
57
58 # No valid beautiful string found
59 return ''
60
1class Solution {
2 public String smallestBeautifulString(String s, int k) {
3 int stringLength = s.length();
4 char[] charArray = s.toCharArray();
5
6 // Iterate through the string from right to left to find the rightmost position
7 // where we can increment a character
8 for (int currentIndex = stringLength - 1; currentIndex >= 0; --currentIndex) {
9 // Get the next character value after the current character
10 int nextCharValue = charArray[currentIndex] - 'a' + 1;
11
12 // Try all possible characters from nextCharValue to k-1
13 for (int candidateValue = nextCharValue; candidateValue < k; ++candidateValue) {
14 char candidateChar = (char) ('a' + candidateValue);
15
16 // Check if this character creates a palindrome with previous characters
17 // Skip if it matches the character at position i-1 or i-2
18 if ((currentIndex > 0 && charArray[currentIndex - 1] == candidateChar) ||
19 (currentIndex > 1 && charArray[currentIndex - 2] == candidateChar)) {
20 continue;
21 }
22
23 // Valid character found, set it at current position
24 charArray[currentIndex] = candidateChar;
25
26 // Fill all positions after currentIndex with the smallest valid characters
27 for (int fillIndex = currentIndex + 1; fillIndex < stringLength; ++fillIndex) {
28 // Try characters from 'a' to find the smallest valid one
29 for (int charOption = 0; charOption < k; ++charOption) {
30 candidateChar = (char) ('a' + charOption);
31
32 // Check if this character creates a palindrome with previous characters
33 if ((fillIndex > 0 && charArray[fillIndex - 1] == candidateChar) ||
34 (fillIndex > 1 && charArray[fillIndex - 2] == candidateChar)) {
35 continue;
36 }
37
38 // Valid character found, set it and move to next position
39 charArray[fillIndex] = candidateChar;
40 break;
41 }
42 }
43
44 // Return the resulting beautiful string
45 return String.valueOf(charArray);
46 }
47 }
48
49 // No valid beautiful string found that is lexicographically larger
50 return "";
51 }
52}
53
1class Solution {
2public:
3 string smallestBeautifulString(string s, int k) {
4 int n = s.size();
5
6 // Try to increment from the rightmost position
7 for (int i = n - 1; i >= 0; --i) {
8 // Get the next character value starting from current char + 1
9 int nextCharValue = s[i] - 'a' + 1;
10
11 // Try all possible characters from nextCharValue to k-1
12 for (int j = nextCharValue; j < k; ++j) {
13 char candidateChar = (char)('a' + j);
14
15 // Check if this character creates a palindrome with previous positions
16 // Skip if it matches the character at position i-1 or i-2
17 if ((i > 0 && s[i - 1] == candidateChar) ||
18 (i > 1 && s[i - 2] == candidateChar)) {
19 continue;
20 }
21
22 // Valid character found, update position i
23 s[i] = candidateChar;
24
25 // Fill remaining positions (i+1 to n-1) with lexicographically smallest valid characters
26 for (int pos = i + 1; pos < n; ++pos) {
27 // Try characters from 'a' to 'a'+k-1
28 for (int charIdx = 0; charIdx < k; ++charIdx) {
29 candidateChar = (char)('a' + charIdx);
30
31 // Check if this character creates a palindrome with previous positions
32 // Skip if it matches the character at position pos-1 or pos-2
33 if ((pos > 0 && s[pos - 1] == candidateChar) ||
34 (pos > 1 && s[pos - 2] == candidateChar)) {
35 continue;
36 }
37
38 // Valid character found for this position
39 s[pos] = candidateChar;
40 break;
41 }
42 }
43
44 // Successfully constructed a valid beautiful string
45 return s;
46 }
47 }
48
49 // No valid beautiful string found (string would need to be longer)
50 return "";
51 }
52};
53
1/**
2 * Finds the lexicographically smallest beautiful string greater than the input string.
3 * A beautiful string has no two adjacent equal characters and no two characters at distance 2 that are equal.
4 * @param s - The input string consisting of lowercase letters
5 * @param k - The size of the alphabet to use (first k letters from 'a')
6 * @returns The smallest beautiful string greater than s, or empty string if none exists
7 */
8function smallestBeautifulString(s: string, k: number): string {
9 // Convert string to character array for easier manipulation
10 const characters: string[] = s.split('');
11 const length: number = characters.length;
12
13 // Try to increment the string from right to left (least significant position first)
14 for (let currentIndex = length - 1; currentIndex >= 0; --currentIndex) {
15 // Get the next character code after the current character (1-indexed from 'a')
16 const nextCharCode: number = characters[currentIndex].charCodeAt(0) - 97 + 1;
17
18 // Try all possible characters starting from the next one up to k
19 for (let charIndex = nextCharCode; charIndex < k; ++charIndex) {
20 const candidateChar: string = String.fromCharCode(charIndex + 97);
21
22 // Check if this character would violate the beautiful string constraints
23 // Skip if it equals the previous character (distance 1)
24 if (currentIndex > 0 && characters[currentIndex - 1] === candidateChar) {
25 continue;
26 }
27 // Skip if it equals the character two positions back (distance 2)
28 if (currentIndex > 1 && characters[currentIndex - 2] === candidateChar) {
29 continue;
30 }
31
32 // Valid character found, set it
33 characters[currentIndex] = candidateChar;
34
35 // Fill the remaining positions with the lexicographically smallest valid characters
36 for (let fillIndex = currentIndex + 1; fillIndex < length; ++fillIndex) {
37 // Try each character starting from 'a'
38 for (let fillCharIndex = 0; fillCharIndex < k; ++fillCharIndex) {
39 const fillChar: string = String.fromCharCode(fillCharIndex + 97);
40
41 // Check constraints for the fill character
42 // Skip if it equals the previous character
43 if (fillIndex > 0 && characters[fillIndex - 1] === fillChar) {
44 continue;
45 }
46 // Skip if it equals the character two positions back
47 if (fillIndex > 1 && characters[fillIndex - 2] === fillChar) {
48 continue;
49 }
50
51 // Valid fill character found, use it and move to next position
52 characters[fillIndex] = fillChar;
53 break;
54 }
55 }
56
57 // Return the constructed beautiful string
58 return characters.join('');
59 }
60 }
61
62 // No valid beautiful string found that is greater than input
63 return '';
64}
65
Time and Space Complexity
The time complexity is O(n Γ k)
, where n
is the length of the string and k
is the size of the alphabet (parameter k).
The algorithm iterates through the string from right to left (outer loop: O(n)
). For each position i
, it tries up to k
different characters (loop with range (p, k)
). When a valid character is found, it fills the remaining positions from i+1
to n-1
(inner loop: O(n)
), and for each position, it tries up to k
characters to find the first valid one.
In the worst case, the algorithm needs to:
- Try multiple positions from the end:
O(n)
- For each position, try multiple characters:
O(k)
- Fill the suffix after finding a valid character:
O(n Γ k)
However, the reference answer states O(n)
. This suggests that despite the nested loops, the amortized complexity is O(n)
because:
- The algorithm returns immediately after finding the first valid solution
- Each position is modified at most once in a successful path
- The constant
k
is bounded (at most 26 for lowercase letters)
Therefore, treating k
as a constant, the time complexity simplifies to O(n)
.
The space complexity is O(n)
for creating the list cs
from the input string. If we consider only auxiliary space (excluding the space needed for the output), it would be O(1)
as stated in the reference answer, since we only use a few variables and modify the list in-place.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Error in Character Range
Pitfall: A common mistake is incorrectly handling the character range when trying to increment. The code current_char_value = ord(char_list[i]) - ord('a') + 1
correctly gets the next value after the current character, but developers often forget the +1
or mistakenly use ord(char_list[i]) - ord('a')
, which would include the current character itself in the range.
Why it's problematic: If you include the current character in the range, you might end up with the same string instead of finding the next lexicographically larger one.
Solution: Always ensure you're starting from current_char_value + 1
when looking for the next valid character:
# Correct: starts from the character after current
current_char_value = ord(char_list[i]) - ord('a') + 1
# Incorrect: would include current character
# current_char_value = ord(char_list[i]) - ord('a')
2. Not Checking Bounds Before Accessing Previous Characters
Pitfall: Forgetting to check if i > 0
before accessing char_list[i-1]
or if i > 1
before accessing char_list[i-2]
can lead to negative indexing, which in Python doesn't cause an error but gives unexpected behavior by wrapping around to the end of the list.
Why it's problematic: Without proper bounds checking, char_list[-1]
would access the last character of the string when you meant to skip the check entirely for the first character.
Solution: Always include boundary checks before accessing previous positions:
# Correct approach with bounds checking if i > 0 and char_list[i - 1] == next_char: continue if i > 1 and char_list[i - 2] == next_char: continue # Incorrect: missing bounds check # if char_list[i - 1] == next_char: # Dangerous when i == 0
3. Forgetting to Fill Remaining Positions After Increment
Pitfall: After successfully incrementing a character at position i
, some implementations forget to properly fill positions [i+1, n-1]
with valid characters, or they might leave them unchanged from the original string.
Why it's problematic: The remaining characters from the original string might not be the lexicographically smallest valid option, or worse, they might create palindromes with the newly changed character.
Solution: Always reset and fill all positions after the increment point with the smallest valid characters:
# After setting char_list[i] = next_char
for pos in range(i + 1, n):
# Must find the smallest valid character for each position
for char_value in range(k):
candidate_char = chr(ord('a') + char_value)
# Check palindrome constraints and assign
4. Incorrect Palindrome Detection Logic
Pitfall: Only checking for adjacent duplicates (length-2 palindromes) but forgetting about length-3 palindromes like "aba", or vice versa.
Why it's problematic: A beautiful string must avoid ALL palindromic substrings of length 2 or more. Missing either check would produce invalid results.
Solution: Always check both conditions:
- No character should equal its immediate predecessor (avoids "aa")
- No character should equal the character two positions before it (avoids "aba")
Both checks are essential and neither can be omitted.
Which of the following shows the order of node visit in a Breadth-first Search?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
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!