779. K-th Symbol in Grammar
Problem Description
We construct a table with n
rows (using 1-based indexing). The table follows a specific pattern:
- Row 1 starts with
0
- For each subsequent row, we generate it from the previous row by:
- Replacing every
0
with01
- Replacing every
1
with10
- Replacing every
For example, when n = 3
:
- Row 1:
0
- Row 2:
01
(the0
from row 1 becomes01
) - Row 3:
0110
(the0
from row 2 becomes01
, and the1
becomes10
)
Given two integers n
and k
, you need to find and return the k
-th symbol (1-indexed) in the n
-th row.
The pattern creates a binary sequence where each row is generated from the previous one through the replacement rules. The key observation is that each row has exactly 2^(n-1)
elements, and the first half of any row matches the entire previous row, while the second half is the bitwise inverse of the previous row.
The recursive solution leverages this pattern:
- If
k
is in the first half of rown
(i.e.,k ≤ 2^(n-2)
), then thek
-th element is the same as thek
-th element in rown-1
- If
k
is in the second half, then thek
-th element is the inverse of the corresponding element in rown-1
, which is at positionk - 2^(n-2)
Intuition
Let's trace through how the pattern develops to understand the structure:
Row 1: 0 Row 2: 01 Row 3: 0110 Row 4: 01101001 Row 5: 0110100110010110
The first insight comes from noticing that each row has exactly 2^(n-1)
elements. Row 1 has 1 element, row 2 has 2 elements, row 3 has 4 elements, and so on.
The key breakthrough is recognizing that when we generate row n
from row n-1
, we're essentially:
- Keeping the entire row
n-1
as the first half - Creating the second half by inverting each bit from row
n-1
For example, row 3 is 0110
:
- First half
01
is exactly row 2 - Second half
10
is the inverse of row 2 (where0→1
and1→0
)
This pattern holds for all rows! Row 4 (01101001
) can be split into:
- First half
0110
(which is row 3) - Second half
1001
(which is the inverse of row 3:0110
→1001
)
This recursive structure suggests we don't need to generate the entire row to find the k
-th element. Instead, we can determine which half k
falls into and recursively solve a smaller problem:
- If
k
is in the first half (position1
to2^(n-2)
), the answer is the same as finding thek
-th element in rown-1
- If
k
is in the second half (position2^(n-2) + 1
to2^(n-1)
), the answer is the inverse of the(k - 2^(n-2))
-th element in rown-1
The base case is when n = 1
, which always returns 0
. This recursive approach allows us to find any element in O(n)
time without generating the entire sequence.
Solution Approach
The solution uses recursion to efficiently find the k
-th symbol without generating the entire row. Here's how the implementation works:
Base Case:
When n = 1
, we return 0
since the first row always contains only 0
.
Recursive Cases:
-
First Half Check: If
k <= (1 << (n - 2))
, meaningk
is in the first half of rown
:- Since the first half of row
n
is identical to the entire rown-1
, we recursively callkthGrammar(n - 1, k)
- The expression
(1 << (n - 2))
calculates2^(n-2)
, which is half the size of rown
- Since the first half of row
-
Second Half Check: If
k
is in the second half of rown
:- We need to find the corresponding position in row
n-1
, which isk - (1 << (n - 2))
- Since the second half is the bitwise inverse of row
n-1
, we recursively callkthGrammar(n - 1, k - (1 << (n - 2)))
and XOR the result with1
- The XOR operation (
^ 1
) flips the bit:0 ^ 1 = 1
and1 ^ 1 = 0
- We need to find the corresponding position in row
Example Walkthrough:
Let's trace kthGrammar(4, 6)
:
- Row 4 has 8 elements, so the first half ends at position 4
- Since
6 > 4
, we're in the second half - We calculate
kthGrammar(3, 6 - 4) ^ 1 = kthGrammar(3, 2) ^ 1
- For
kthGrammar(3, 2)
: Row 3 has 4 elements, first half ends at 2 - Since
2 <= 2
, we're in the first half - We calculate
kthGrammar(2, 2)
- For
kthGrammar(2, 2)
: Row 2 has 2 elements, first half ends at 1 - Since
2 > 1
, we're in the second half - We calculate
kthGrammar(1, 2 - 1) ^ 1 = kthGrammar(1, 1) ^ 1 = 0 ^ 1 = 1
- Going back up:
kthGrammar(3, 2) = 1
- Finally:
kthGrammar(4, 6) = 1 ^ 1 = 0
The algorithm runs in O(n)
time complexity with O(n)
space complexity due to the recursion stack.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's find the 5th symbol in row 4 using our recursive approach.
First, let's visualize what row 4 looks like:
- Row 1:
0
- Row 2:
01
- Row 3:
0110
- Row 4:
01101001
We want to find kthGrammar(4, 5)
, which should return 1
(the 5th position in 01101001
).
Step 1: Check if we're in row 1 (base case)
n = 4
, not the base case, continue
Step 2: Determine which half position 5 falls into
- Row 4 has
2^3 = 8
elements - First half contains positions 1-4, second half contains positions 5-8
- Since
k = 5 > 4
, we're in the second half
Step 3: Recursively find the corresponding element in row 3
- In the second half, so we need the inverse of position
5 - 4 = 1
in row 3 - Call
kthGrammar(3, 1)
and then invert the result
Step 4: Solve kthGrammar(3, 1)
- Row 3 has
2^2 = 4
elements - First half contains positions 1-2
- Since
k = 1 ≤ 2
, we're in the first half - Call
kthGrammar(2, 1)
(no inversion needed)
Step 5: Solve kthGrammar(2, 1)
- Row 2 has
2^1 = 2
elements - First half contains position 1
- Since
k = 1 ≤ 1
, we're in the first half - Call
kthGrammar(1, 1)
Step 6: Base case kthGrammar(1, 1)
- Returns
0
Step 7: Backtrack with results
kthGrammar(2, 1) = 0
kthGrammar(3, 1) = 0
kthGrammar(4, 5) = 0 ^ 1 = 1
(inverted because position 5 is in the second half)
The answer is 1
, which matches the 5th position in row 4: 01101001
.
Solution Implementation
1class Solution:
2 def kthGrammar(self, n: int, k: int) -> int:
3 """
4 Find the k-th character in the n-th row of a grammar sequence.
5
6 Grammar rules:
7 - Row 1: 0
8 - Each 0 becomes 01, each 1 becomes 10 in the next row
9
10 Args:
11 n: The row number (1-indexed)
12 k: The position in the row (1-indexed)
13
14 Returns:
15 The k-th character (0 or 1) in the n-th row
16 """
17 # Base case: first row always starts with 0
18 if n == 1:
19 return 0
20
21 # Calculate the midpoint of the current row
22 # The n-th row has 2^(n-1) elements, so half is 2^(n-2)
23 mid_point = 1 << (n - 2)
24
25 # If k is in the first half of the current row
26 # The value is the same as the k-th position in the previous row
27 if k <= mid_point:
28 return self.kthGrammar(n - 1, k)
29
30 # If k is in the second half of the current row
31 # The value is the complement (XOR with 1) of the corresponding
32 # position in the first half of the previous row
33 # We subtract mid_point from k to get the corresponding position
34 return self.kthGrammar(n - 1, k - mid_point) ^ 1
35
1class Solution {
2 /**
3 * Finds the k-th symbol in the n-th row of a grammar sequence.
4 * Grammar rules:
5 * - Row 1: 0
6 * - Each 0 becomes 01, each 1 becomes 10 in the next row
7 *
8 * @param n The row number (1-indexed)
9 * @param k The position in the row (1-indexed)
10 * @return The k-th symbol (0 or 1) in the n-th row
11 */
12 public int kthGrammar(int n, int k) {
13 // Base case: first row always contains only 0
14 if (n == 1) {
15 return 0;
16 }
17
18 // Calculate the midpoint of the current row
19 // The n-th row has 2^(n-1) elements, so midpoint is 2^(n-2)
20 int midpoint = 1 << (n - 2);
21
22 // If k is in the first half of the current row
23 // The value is the same as the k-th position in the previous row
24 if (k <= midpoint) {
25 return kthGrammar(n - 1, k);
26 }
27
28 // If k is in the second half of the current row
29 // The value is the complement (XOR with 1) of the corresponding position
30 // in the first half of the previous row
31 // Position in previous row: k - midpoint
32 return kthGrammar(n - 1, k - midpoint) ^ 1;
33 }
34}
35
1class Solution {
2public:
3 int kthGrammar(int n, int k) {
4 // Base case: first row always contains just 0
5 if (n == 1) {
6 return 0;
7 }
8
9 // Calculate the midpoint of the current row
10 // Row n has 2^(n-1) elements, so midpoint is 2^(n-2)
11 int midpoint = 1 << (n - 2);
12
13 // If k is in the first half, it's the same as the kth element in row (n-1)
14 if (k <= midpoint) {
15 return kthGrammar(n - 1, k);
16 }
17
18 // If k is in the second half, it's the complement of the corresponding
19 // element in the first half of row (n-1)
20 // The corresponding position is (k - midpoint)
21 return kthGrammar(n - 1, k - midpoint) ^ 1;
22 }
23};
24
1/**
2 * Finds the k-th symbol in the n-th row of a binary grammar pattern.
3 * The pattern starts with 0, and each row is formed by replacing:
4 * - 0 with 01
5 * - 1 with 10
6 *
7 * @param n - The row number (1-indexed)
8 * @param k - The position in the row (1-indexed)
9 * @returns The k-th symbol (0 or 1) in the n-th row
10 */
11function kthGrammar(n: number, k: number): number {
12 // Base case: first row always contains only 0
13 if (n === 1) {
14 return 0;
15 }
16
17 // Calculate the midpoint of the current row
18 // The n-th row has 2^(n-1) elements, so midpoint is 2^(n-2)
19 const midpoint: number = 1 << (n - 2);
20
21 // If k is in the first half of the row
22 // The first half is identical to the previous row
23 if (k <= midpoint) {
24 return kthGrammar(n - 1, k);
25 }
26
27 // If k is in the second half of the row
28 // The second half is the bitwise complement of the corresponding position in the previous row
29 // XOR with 1 flips the bit (0 becomes 1, 1 becomes 0)
30 return kthGrammar(n - 1, k - midpoint) ^ 1;
31}
32
Time and Space Complexity
The time complexity is O(n)
. The function makes recursive calls, and in each call, it reduces the problem size by decreasing n
by 1. The recursion continues until it reaches the base case when n == 1
. Therefore, the maximum recursion depth is n - 1
, resulting in O(n)
time complexity.
The space complexity is O(n)
. This is due to the recursive call stack. Since the function calls itself recursively up to n - 1
times before reaching the base case, the call stack will have at most n
function calls stored simultaneously, leading to O(n)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Errors with 1-Based Indexing
One of the most common mistakes is forgetting that both n
and k
use 1-based indexing. Developers often accidentally treat them as 0-based, leading to incorrect calculations.
Incorrect approach:
def kthGrammar(self, n: int, k: int) -> int:
if n == 0: # Wrong! n starts from 1, not 0
return 0
mid_point = 1 << (n - 1) # Wrong! This gives full row size, not half
# ...
Solution: Always remember that row 1 is the first row (not row 0), and position counting starts from 1. The midpoint calculation should use 1 << (n - 2)
to get half the size of row n
.
2. Incorrect Midpoint Calculation
Another frequent error is miscalculating the midpoint of the current row. Since row n
has 2^(n-1)
elements, the midpoint should be 2^(n-2)
, not 2^(n-1)
.
Incorrect approach:
def kthGrammar(self, n: int, k: int) -> int:
if n == 1:
return 0
mid_point = (1 << (n - 1)) // 2 # Overly complex way
# or
mid_point = 2 ** (n - 2) # Less efficient than bit shifting
Solution: Use bit shifting for efficiency: mid_point = 1 << (n - 2)
. This directly calculates 2^(n-2)
without unnecessary operations.
3. Forgetting to Flip the Bit for Second Half
When k
is in the second half, developers sometimes forget to invert the result from the recursive call.
Incorrect approach:
def kthGrammar(self, n: int, k: int) -> int:
if n == 1:
return 0
mid_point = 1 << (n - 2)
if k <= mid_point:
return self.kthGrammar(n - 1, k)
else:
return self.kthGrammar(n - 1, k - mid_point) # Missing XOR!
Solution: Always XOR with 1 when dealing with the second half: return self.kthGrammar(n - 1, k - mid_point) ^ 1
4. Attempting to Generate the Entire Row
A naive approach would be to actually generate each row up to row n
, which leads to exponential time and space complexity.
Incorrect approach:
def kthGrammar(self, n: int, k: int) -> int:
row = "0"
for i in range(1, n):
new_row = ""
for char in row:
if char == '0':
new_row += "01"
else:
new_row += "10"
row = new_row
return int(row[k - 1])
Solution: Use the recursive pattern recognition approach. Each row doesn't need to be fully generated - we can determine any specific position by understanding the relationship between rows.
5. Incorrect Position Mapping for Second Half
When calculating the corresponding position in the previous row for elements in the second half, using the wrong offset is a common mistake.
Incorrect approach:
def kthGrammar(self, n: int, k: int) -> int:
if n == 1:
return 0
mid_point = 1 << (n - 2)
if k <= mid_point:
return self.kthGrammar(n - 1, k)
else:
# Wrong! Should map to position in previous row, not use k directly
return self.kthGrammar(n - 1, k) ^ 1
Solution: For the second half, the corresponding position in the previous row is k - mid_point
, not k
. This maps the position correctly to the range [1, mid_point]
in the previous row.
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
Recommended Readings
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
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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
Want a Structured Path to Master System Design Too? Don’t Miss This!