Facebook Pixel

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:

  1. It only uses the first k letters of the English lowercase alphabet (a, b, c, ...).
  2. 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 string s
  • 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. We update position i with the smallest valid character that's larger than the current one
  2. 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 the k-th letter)
  • For each candidate character c, we check if it violates the beauty constraint:
    • If i > 0 and cs[i-1] == c: skip (would create adjacent duplicates)
    • If i > 1 and cs[i-2] == c: skip (would create a palindrome of length 3)
  • 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 from i+1 to n-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 and cs[l-1] == c: skip if true
      • Check if l > 1 and cs[l-2] == c: skip if true
      • If valid, assign cs[l] = c and break to move to the next position
  • 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 Evaluator

Example 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'
  • Check 'd':
    • Is cs[2] == 'd'? No βœ“
    • Is cs[1] == 'd'? No βœ“
    • Valid! Set cs[3] = 'd'
    • String becomes: ['a', 'b', 'c', 'd']

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']

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']

Result: "abda"

This demonstrates how the algorithm:

  1. Searches right-to-left for a position to increment
  2. Checks palindrome constraints when trying new characters
  3. 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:

  1. The algorithm returns immediately after finding the first valid solution
  2. Each position is modified at most once in a successful path
  3. 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More