Facebook Pixel

1415. The k-th Lexicographical String of All Happy Strings of Length n

Problem Description

You need to find the k-th "happy string" of length n when all such strings are sorted lexicographically.

A happy string is defined as a string that:

  • Contains only the letters 'a', 'b', and 'c'
  • Has no two consecutive characters that are the same (i.e., s[i] != s[i + 1] for all valid indices)

For example:

  • Happy strings: "abc", "ac", "b", "abcbabcbcb"
  • Not happy strings: "aa" (consecutive 'a's), "baa" (consecutive 'a's), "ababbc" (consecutive 'b's)

Given two integers:

  • n: the length of the happy strings to generate
  • k: the position of the desired string in the lexicographically sorted list

Your task is to:

  1. Generate all possible happy strings of length n
  2. Sort them in lexicographical order (alphabetical order)
  3. Return the k-th string from this sorted list
  4. If there are fewer than k happy strings of length n, return an empty string ""

The solution uses a depth-first search (DFS) approach with backtracking. It builds strings character by character, ensuring that no two consecutive characters are the same. The function generates happy strings in lexicographical order naturally by iterating through characters 'a', 'b', 'c' in that order. Once k happy strings are found, the generation stops early for efficiency, and the k-th string (at index k-1 due to 0-based indexing) is returned.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • No: The problem doesn't involve nodes and edges or graph traversal. We're generating strings based on specific rules.

Need to solve for kth smallest/largest?

  • Yes: We need to find the k-th string in lexicographically sorted order. However, rather than using heap/sorting directly, let's continue exploring since we're generating combinations with constraints.

Involves Linked Lists?

  • No: The problem is about generating strings, not manipulating linked list structures.

Does the problem have small constraints?

  • Yes: The problem has relatively small constraints since:
    • We're only using 3 characters ('a', 'b', 'c')
    • The length n is typically small (as generating all combinations grows exponentially)
    • We need to enumerate all valid happy strings up to the k-th one

Brute force / Backtracking?

  • Yes: Backtracking is the perfect fit because:
    • We need to explore all possible combinations of characters
    • We have constraints (no two consecutive characters can be the same)
    • We can build the solution incrementally and backtrack when we violate constraints
    • We can prune the search space early once we've found k strings

Conclusion: The flowchart correctly leads us to use a Backtracking approach. This makes sense because we're generating all valid "happy strings" by:

  1. Building strings character by character
  2. Checking constraints at each step (no consecutive duplicates)
  3. Backtracking when we can't proceed further
  4. Collecting valid complete strings in lexicographical order
  5. Stopping early once we have k strings for optimization
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to generate the k-th string from a sorted list of valid strings, our first thought might be to generate all possible strings and then filter them. However, with the constraint that no two consecutive characters can be the same, we need a systematic way to build valid strings.

Think of building a happy string like making choices at each position. At position 0, we can choose any of the three characters 'a', 'b', or 'c'. But at position 1, we can only choose from the two characters that are different from what we chose at position 0. This pattern continues for each subsequent position.

The key insight is that if we explore these choices in alphabetical order ('a' before 'b' before 'c'), we naturally generate the strings in lexicographical order. This is perfect because we need the k-th string in sorted order.

Why backtracking? Consider building the string "abc":

  1. Start with empty string
  2. Add 'a' → current string: "a"
  3. Add 'b' (can't add 'a' again) → current string: "ab"
  4. Add 'c' (can't add 'b' again) → current string: "abc"

If at any point we reach the desired length n, we've found a valid happy string. If we can't proceed further (all remaining choices would violate the no-consecutive rule), we backtrack and try a different choice.

The beauty of this approach is that we can stop as soon as we've found k valid strings. We don't need to generate all possible happy strings - just enough to find the k-th one. Since we're generating them in lexicographical order by always trying 'a' before 'b' before 'c', the k-th string we generate is exactly the answer we're looking for.

If we exhaust all possibilities and haven't found k strings, it means there are fewer than k happy strings of length n, so we return an empty string.

Learn more about Backtracking patterns.

Solution Approach

The implementation uses a depth-first search (DFS) with backtracking to generate happy strings. Let's walk through the key components:

Data Structures:

  • s: A list that acts as a stack to build the current string character by character
  • ans: A list to store all valid happy strings found so far

The DFS Function:

The core logic is in the dfs() function which recursively builds happy strings:

  1. Base Case: When len(s) == n, we've built a complete string of length n. We convert the list s to a string using "".join(s) and add it to our answer list.

  2. Early Termination: If len(ans) >= k, we've already found k or more happy strings. Since we generate them in lexicographical order, we can stop early - no need to find more strings.

  3. Recursive Case: We try each character from "abc" in order:

    • Check if we can add character c: either the current string is empty (not s) or the last character is different from c (s[-1] != c)
    • If valid, append c to our current string: s.append(c)
    • Recursively call dfs() to continue building the string
    • Backtrack by removing the last character: s.pop()

Why This Works:

The algorithm naturally generates strings in lexicographical order because:

  • We iterate through characters in the order 'a', 'b', 'c'
  • At each position, we explore all possibilities with 'a' first, then 'b', then 'c'
  • This means "abc..." comes before "acb..." which comes before "bac...", etc.

Example Walkthrough for n=3, k=1:

Start: s = []
Try 'a': s = ['a']
  Try 'b': s = ['a', 'b']
    Try 'a': s = ['a', 'b', 'a'] → Found "aba", add to ans
    Try 'c': s = ['a', 'b', 'c'] → Found "abc", add to ans
  Try 'c': s = ['a', 'c']
    Try 'a': s = ['a', 'c', 'a'] → Found "aca", add to ans
    ...

The first string found is "aba", which would be our answer for k=1.

Final Step:

After DFS completes, we check if we found at least k strings:

  • If len(ans) < k: return "" (not enough happy strings exist)
  • Otherwise: return ans[k-1] (the k-th string, using 0-based indexing)

The time complexity is O(3 × 2^(n-1)) in the worst case, as we have 3 choices for the first character and 2 choices for each subsequent character. However, the early termination when we find k strings significantly improves practical performance.

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 3rd happy string of length 2 (n=2, k=3).

Step 1: Start with empty string

  • s = [], ans = []
  • Try adding first character

Step 2: Add 'a' as first character

  • s = ['a']
  • Now try second character (can't be 'a')
    • Add 'b': s = ['a','b'] → Length is 2, found "ab" → ans = ["ab"]
    • Backtrack: s = ['a']
    • Add 'c': s = ['a','c'] → Length is 2, found "ac" → ans = ["ab", "ac"]
    • Backtrack: s = ['a']
  • Backtrack: s = []

Step 3: Add 'b' as first character

  • s = ['b']
  • Try second character (can't be 'b')
    • Add 'a': s = ['b','a'] → Length is 2, found "ba" → ans = ["ab", "ac", "ba"]
    • We now have 3 strings (k=3), so we can stop early!

Step 4: Return the result

  • ans = ["ab", "ac", "ba"]
  • The 3rd string (index 2) is "ba"
  • Return "ba"

Notice how the strings are naturally generated in lexicographical order: "ab" < "ac" < "ba". This is because we always try 'a' before 'b' before 'c' at each position. The early termination after finding k strings saves us from generating unnecessary strings like "bc", "ca", "cb".

Solution Implementation

1class Solution:
2    def getHappyString(self, n: int, k: int) -> str:
3        """
4        Generate the k-th lexicographically smallest happy string of length n.
5        A happy string is a string where no two adjacent characters are the same.
6      
7        Args:
8            n: Length of the happy string
9            k: The k-th smallest happy string to return
10          
11        Returns:
12            The k-th happy string, or empty string if less than k happy strings exist
13        """
14        def backtrack(current_string: list[str], result_list: list[str]) -> None:
15            """
16            Use backtracking to generate all valid happy strings in lexicographical order.
17          
18            Args:
19                current_string: Current string being built
20                result_list: List to store all valid happy strings
21            """
22            # Base case: if we've built a string of length n, add it to results
23            if len(current_string) == n:
24                result_list.append("".join(current_string))
25                return
26          
27            # Optimization: stop early if we already have k strings
28            if len(result_list) >= k:
29                return
30          
31            # Try each character 'a', 'b', 'c' in lexicographical order
32            for char in "abc":
33                # Only add the character if it's different from the last character
34                # (or if the string is empty)
35                if not current_string or current_string[-1] != char:
36                    # Add character and explore
37                    current_string.append(char)
38                    backtrack(current_string, result_list)
39                    # Backtrack: remove the character to try other options
40                    current_string.pop()
41      
42        # Initialize empty lists for building strings and storing results
43        all_happy_strings = []
44        current_build = []
45      
46        # Generate happy strings using backtracking
47        backtrack(current_build, all_happy_strings)
48      
49        # Return the k-th string (1-indexed) or empty string if not enough strings exist
50        return "" if len(all_happy_strings) < k else all_happy_strings[k - 1]
51
1class Solution {
2    // List to store all valid happy strings in lexicographical order
3    private List<String> happyStrings = new ArrayList<>();
4    // StringBuilder to build the current string during DFS
5    private StringBuilder currentString = new StringBuilder();
6    // Target length of happy strings
7    private int targetLength;
8    // The k-th happy string to find
9    private int targetIndex;
10
11    /**
12     * Returns the k-th lexicographically smallest happy string of length n.
13     * A happy string is a string where no two adjacent characters are the same.
14     * 
15     * @param n the length of the happy string
16     * @param k the index (1-based) of the happy string to return
17     * @return the k-th happy string, or empty string if less than k happy strings exist
18     */
19    public String getHappyString(int n, int k) {
20        this.targetLength = n;
21        this.targetIndex = k;
22      
23        // Generate all happy strings using DFS
24        generateHappyStrings();
25      
26        // Return the k-th string if it exists, otherwise return empty string
27        return happyStrings.size() < k ? "" : happyStrings.get(k - 1);
28    }
29
30    /**
31     * Recursively generates all happy strings of the target length using DFS.
32     * Uses backtracking to explore all valid combinations of 'a', 'b', 'c'.
33     */
34    private void generateHappyStrings() {
35        // Base case: if current string reaches target length, add it to the result list
36        if (currentString.length() == targetLength) {
37            happyStrings.add(currentString.toString());
38            return;
39        }
40      
41        // Optimization: stop generating if we already have k strings
42        if (happyStrings.size() >= targetIndex) {
43            return;
44        }
45      
46        // Try appending each character 'a', 'b', 'c'
47        for (char character : "abc".toCharArray()) {
48            // Check if we can append this character (either string is empty or last char is different)
49            if (currentString.isEmpty() || currentString.charAt(currentString.length() - 1) != character) {
50                // Append the character
51                currentString.append(character);
52              
53                // Recursively generate the rest of the string
54                generateHappyStrings();
55              
56                // Backtrack: remove the last character to try other options
57                currentString.deleteCharAt(currentString.length() - 1);
58            }
59        }
60    }
61}
62
1class Solution {
2public:
3    string getHappyString(int n, int k) {
4        // Store all valid happy strings
5        vector<string> happyStrings;
6      
7        // Current string being built
8        string currentString = "";
9      
10        // Define recursive DFS function using lambda
11        auto generateHappyStrings = [&](this auto&& generateHappyStrings) -> void {
12            // Base case: if current string reaches target length, add to result
13            if (currentString.size() == n) {
14                happyStrings.emplace_back(currentString);
15                return;
16            }
17          
18            // Early termination: stop generating if we already have k strings
19            if (happyStrings.size() >= k) {
20                return;
21            }
22          
23            // Try adding each character from 'a' to 'c'
24            for (char ch = 'a'; ch <= 'c'; ++ch) {
25                // Add character only if string is empty or last character is different
26                if (currentString.empty() || currentString.back() != ch) {
27                    // Add character to current string
28                    currentString.push_back(ch);
29                  
30                    // Recursively generate remaining characters
31                    generateHappyStrings();
32                  
33                    // Backtrack: remove the last added character
34                    currentString.pop_back();
35                }
36            }
37        };
38      
39        // Start the DFS generation process
40        generateHappyStrings();
41      
42        // Return k-th happy string if it exists, otherwise return empty string
43        return happyStrings.size() < k ? "" : happyStrings[k - 1];
44    }
45};
46
1/**
2 * Generates the k-th lexicographically smallest happy string of length n.
3 * A happy string is a string that doesn't contain any of the strings "aaa", "bbb", or "ccc" as a substring.
4 * 
5 * @param n - The length of the happy string
6 * @param k - The position of the desired happy string (1-indexed)
7 * @returns The k-th happy string, or empty string if it doesn't exist
8 */
9function getHappyString(n: number, k: number): string {
10    // Array to store all valid happy strings
11    const happyStrings: string[] = [];
12  
13    // Current string being built during DFS
14    const currentString: string[] = [];
15  
16    /**
17     * Depth-First Search to generate all happy strings
18     * Uses backtracking to explore all valid combinations
19     */
20    const generateHappyStrings = (): void => {
21        // Base case: if current string reaches target length, add to results
22        if (currentString.length === n) {
23            happyStrings.push(currentString.join(''));
24            return;
25        }
26      
27        // Early termination: stop if we've already found k strings
28        if (happyStrings.length >= k) {
29            return;
30        }
31      
32        // Try adding each character 'a', 'b', 'c'
33        for (const character of 'abc') {
34            // Check if adding this character keeps the string happy
35            // (either string is empty or last character is different)
36            if (currentString.length === 0 || currentString[currentString.length - 1] !== character) {
37                // Add character and continue DFS
38                currentString.push(character);
39                generateHappyStrings();
40                // Backtrack: remove character to try next option
41                currentString.pop();
42            }
43        }
44    };
45  
46    // Start the DFS to generate all happy strings
47    generateHappyStrings();
48  
49    // Return the k-th string (convert to 0-indexed) or empty string if it doesn't exist
50    return happyStrings[k - 1] ?? '';
51}
52

Time and Space Complexity

Time Complexity: O(n × 3^n)

The algorithm uses DFS to generate all valid happy strings of length n. At each position, we have at most 3 choices ('a', 'b', 'c'), but due to the constraint that adjacent characters cannot be the same, we effectively have at most 2 choices for positions after the first one. However, the branching factor in the worst case is still 3 at each level. The recursion tree has depth n, leading to at most 3^n leaf nodes. For each complete string generated, we perform O(n) work to join the characters and append to the result. Therefore, the total time complexity is O(n × 3^n).

Note: The reference answer suggests O(n × 2^n), which considers that after choosing the first character, each subsequent position has only 2 valid choices (excluding the previous character). This gives us 3 × 2^(n-1) total valid strings, which simplifies to O(2^n) strings, each requiring O(n) time to process, resulting in O(n × 2^n).

Space Complexity: O(n)

The space complexity consists of:

  • The recursion call stack which goes up to depth n: O(n)
  • The temporary list s which stores at most n characters: O(n)
  • The ans list stores the generated strings, but if we consider only the auxiliary space (excluding the output), the space complexity is O(n)

The dominant factor is the recursion depth and the temporary storage, both being O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Indexing When Returning the k-th String

The Pitfall: One of the most common mistakes is forgetting that k is 1-indexed while Python lists are 0-indexed. Developers might incorrectly return ans[k] instead of ans[k-1], which would either return the wrong string or cause an IndexError.

Wrong Implementation:

# Incorrect - returns the (k+1)-th string or crashes
return "" if len(ans) < k else ans[k]  # ❌ Wrong!

Correct Implementation:

# Correct - converts 1-indexed k to 0-indexed position
return "" if len(ans) < k else ans[k-1]  # ✅ Correct!

2. Not Implementing Early Termination Optimization

The Pitfall: Without early termination, the algorithm continues generating all possible happy strings even after finding the k-th one. This wastes computational resources, especially when k is small and n is large.

Inefficient Implementation:

def dfs():
    if len(s) == n:
        ans.append("".join(s))
        return
    # Missing early termination check here! ❌
    for c in "abc":
        if not s or s[-1] != c:
            s.append(c)
            dfs()
            s.pop()

Optimized Implementation:

def dfs():
    if len(s) == n:
        ans.append("".join(s))
        return
    if len(ans) >= k:  # ✅ Stop once we have k strings
        return
    for c in "abc":
        if not s or s[-1] != c:
            s.append(c)
            dfs()
            s.pop()

3. Using String Concatenation Instead of List for Building

The Pitfall: Using string concatenation during backtracking is inefficient because strings are immutable in Python. Each concatenation creates a new string object, leading to O(n²) time complexity per string generation.

Inefficient Implementation:

def dfs(current_string):  # ❌ Passing string directly
    if len(current_string) == n:
        ans.append(current_string)
        return
    for c in "abc":
        if not current_string or current_string[-1] != c:
            dfs(current_string + c)  # ❌ String concatenation is expensive

Efficient Implementation:

def dfs():
    if len(s) == n:
        ans.append("".join(s))  # ✅ Convert list to string only when needed
        return
    for c in "abc":
        if not s or s[-1] != c:
            s.append(c)  # ✅ List operations are O(1)
            dfs()
            s.pop()  # ✅ Efficient backtracking

4. Forgetting to Handle Edge Cases

The Pitfall: Not properly handling cases where k is larger than the total number of possible happy strings, or when n = 0.

Example Edge Cases to Consider:

  • When n = 1 and k = 4: Only 3 happy strings exist ("a", "b", "c"), so should return ""
  • When n = 0: Should handle appropriately (though typically n ≥ 1 in constraints)
  • When k = 0 or negative: Should validate input if not guaranteed by constraints

Robust Implementation:

# Ensure proper validation and edge case handling
if n == 0 or k <= 0:
    return ""
# ... rest of the implementation
return "" if len(ans) < k else ans[k-1]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input 56?

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Recommended Readings

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

Load More