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 generatek
: the position of the desired string in the lexicographically sorted list
Your task is to:
- Generate all possible happy strings of length
n
- Sort them in lexicographical order (alphabetical order)
- Return the k-th string from this sorted list
- If there are fewer than
k
happy strings of lengthn
, 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
- We're only using 3 characters (
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:
- Building strings character by character
- Checking constraints at each step (no consecutive duplicates)
- Backtracking when we can't proceed further
- Collecting valid complete strings in lexicographical order
- Stopping early once we have k strings for optimization
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":
- Start with empty string
- Add
'a'
→ current string:"a"
- Add
'b'
(can't add'a'
again) → current string:"ab"
- 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 characterans
: 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:
-
Base Case: When
len(s) == n
, we've built a complete string of lengthn
. We convert the lists
to a string using"".join(s)
and add it to our answer list. -
Early Termination: If
len(ans) >= k
, we've already foundk
or more happy strings. Since we generate them in lexicographical order, we can stop early - no need to find more strings. -
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 fromc
(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()
- Check if we can add character
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 EvaluatorExample 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']
- Add 'b':
- 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!
- Add 'a':
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 mostn
characters:O(n)
- The
ans
list stores the generated strings, but if we consider only the auxiliary space (excluding the output), the space complexity isO(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
andk = 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]
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
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
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!