Facebook Pixel

779. K-th Symbol in Grammar

MediumBit ManipulationRecursionMath
Leetcode Link

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 with 01
    • Replacing every 1 with 10

For example, when n = 3:

  • Row 1: 0
  • Row 2: 01 (the 0 from row 1 becomes 01)
  • Row 3: 0110 (the 0 from row 2 becomes 01, and the 1 becomes 10)

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 row n (i.e., k ≤ 2^(n-2)), then the k-th element is the same as the k-th element in row n-1
  • If k is in the second half, then the k-th element is the inverse of the corresponding element in row n-1, which is at position k - 2^(n-2)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Keeping the entire row n-1 as the first half
  2. 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 (where 0→1 and 1→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: 01101001)

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 (position 1 to 2^(n-2)), the answer is the same as finding the k-th element in row n-1
  • If k is in the second half (position 2^(n-2) + 1 to 2^(n-1)), the answer is the inverse of the (k - 2^(n-2))-th element in row n-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.

Learn more about Recursion and Math patterns.

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:

  1. First Half Check: If k <= (1 << (n - 2)), meaning k is in the first half of row n:

    • Since the first half of row n is identical to the entire row n-1, we recursively call kthGrammar(n - 1, k)
    • The expression (1 << (n - 2)) calculates 2^(n-2), which is half the size of row n
  2. Second Half Check: If k is in the second half of row n:

    • We need to find the corresponding position in row n-1, which is k - (1 << (n - 2))
    • Since the second half is the bitwise inverse of row n-1, we recursively call kthGrammar(n - 1, k - (1 << (n - 2))) and XOR the result with 1
    • The XOR operation (^ 1) flips the bit: 0 ^ 1 = 1 and 1 ^ 1 = 0

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 Evaluator

Example 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.

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

Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?


Recommended Readings

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

Load More