1238. Circular Permutation in Binary Representation
Problem Description
You are given two integers n
and start
. You need to return any permutation p
of all integers from 0
to 2^n - 1
that satisfies these conditions:
- The first element must be
start
:p[0] = start
- Every two consecutive elements in the permutation must differ by exactly one bit in their binary representation:
p[i]
andp[i+1]
differ by only one bit - The first and last elements must also differ by exactly one bit:
p[0]
andp[2^n - 1]
differ by only one bit
For example, if n = 2
and start = 3
, you have integers [0, 1, 2, 3]
. A valid permutation could be [3, 2, 0, 1]
because:
3
(binary: 11) and2
(binary: 10) differ by one bit2
(binary: 10) and0
(binary: 00) differ by one bit0
(binary: 00) and1
(binary: 01) differ by one bit1
(binary: 01) and3
(binary: 11) differ by one bit (wrapping around)
The solution uses Gray code, which is a binary numeral system where two successive values differ in only one bit. The conversion from binary to Gray code is done using the formula: gray(x) = x ^ (x >> 1)
. After generating the Gray code sequence for all numbers from 0
to 2^n - 1
, the solution finds where start
appears in this sequence and rotates the array to place start
at the beginning.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: While we need to create a sequence where adjacent elements are connected by a specific rule (differing by one bit), this isn't a traditional graph problem with nodes and edges to traverse.
Need to solve for kth smallest/largest?
- No: We're not looking for a kth element; we need to generate a complete permutation satisfying specific constraints.
Involves Linked Lists?
- No: The problem doesn't involve linked list data structures.
Does the problem have small constraints?
- Yes: The constraint is
n
, and we're generating a permutation of size2^n
. For practical purposes,n
is typically small (usually ≤ 20), as2^n
grows exponentially. Evenn = 20
gives us over a million elements.
Brute force / Backtracking?
- Yes: We need to explore different arrangements to find a valid permutation where each adjacent pair differs by exactly one bit. Backtracking would allow us to build the permutation step by step, backtracking when we can't find a valid next element.
Conclusion: The flowchart suggests using a Backtracking approach for this problem. We would start with start
, then recursively try to add elements that differ by one bit from the current element, backtracking if we reach a dead end. However, the actual solution cleverly uses Gray code properties to avoid explicit backtracking, converting the problem into a simple array rotation operation.
Intuition
When we first look at this problem, the constraint that adjacent elements must differ by exactly one bit immediately brings to mind binary representations. Let's think about what this means: if we have 3
(binary: 11
) and want the next number to differ by one bit, we could have 2
(binary: 10
), 1
(binary: 01
), or 7
(binary: 111
).
The backtracking approach would work: start with start
, mark it as visited, then try each possible next number that differs by one bit. If we get stuck, backtrack and try a different path. But this could be computationally expensive, especially as n
grows.
Here's the key insight: this problem is asking for a specific type of sequence that already exists in computer science - it's called a Gray code sequence! Gray code is a binary numeral system where two successive values differ in only one bit. It's used in various applications like error correction in digital communications and position encoders.
For any n
, there's a standard Gray code sequence that covers all numbers from 0
to 2^n - 1
, where each adjacent pair differs by one bit, and crucially, the first and last elements also differ by one bit (forming a cycle).
The formula to convert a binary number to its Gray code equivalent is surprisingly simple: gray(x) = x ^ (x >> 1)
. This XORs each bit with the bit to its left, creating the Gray code representation.
So instead of using backtracking to search for a valid sequence, we can:
- Generate the standard Gray code sequence for all numbers from
0
to2^n - 1
- Find where
start
appears in this sequence - Rotate the sequence so that
start
becomes the first element
This transforms a potentially complex backtracking problem into a simple array manipulation problem. The rotation maintains all the adjacency relationships because Gray code forms a cycle - cutting it at any point and rejoining maintains the property that adjacent elements differ by one bit.
Learn more about Math and Backtracking patterns.
Solution Approach
The solution leverages the Gray code properties to efficiently generate the required permutation. Let's walk through the implementation step by step:
Step 1: Generate Gray Code Sequence
The first line creates a Gray code sequence for all numbers from 0
to 2^n - 1
:
g = [i ^ (i >> 1) for i in range(1 << n)]
Breaking this down:
1 << n
computes2^n
using bit shifting (shifting 1 left byn
positions)- For each integer
i
from0
to2^n - 1
, we apply the Gray code conversion formula:i ^ (i >> 1)
i >> 1
shiftsi
right by one bit (equivalent to integer division by 2)i ^ (i >> 1)
XORs the original number with its right-shifted version
For example, when n = 3
:
- Binary
5
is101
5 >> 1
gives010
(which is2
)101 ^ 010 = 111
(which is7
in Gray code)
Step 2: Find the Starting Position
Next, we locate where start
appears in the Gray code sequence:
j = g.index(start)
This finds the index j
where g[j] == start
. Since Gray code is a bijection (one-to-one mapping), every number from 0
to 2^n - 1
appears exactly once in the sequence.
Step 3: Rotate the Sequence
Finally, we rotate the Gray code sequence to place start
at the beginning:
return g[j:] + g[:j]
This slices the array into two parts:
g[j:]
- elements from indexj
to the end (starting withstart
)g[:j]
- elements from the beginning up to (but not including) indexj
By concatenating these two parts, we get a rotated sequence where start
is the first element.
Why This Works:
The Gray code sequence forms a cycle where the last element differs from the first element by exactly one bit. When we rotate this cycle, we maintain all the adjacency properties:
- Elements that were adjacent remain adjacent (except at the cut point)
- The new first and last elements (which were originally adjacent in the cycle) still differ by one bit
- All intermediate adjacent pairs continue to differ by one bit
The time complexity is O(2^n)
for generating the Gray code sequence and finding the index, and the space complexity is also O(2^n)
for storing the result array.
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 a concrete example with n = 3
and start = 5
.
Step 1: Generate Gray Code Sequence
First, we generate the Gray code for all numbers from 0 to 7 (since 2³ = 8):
i (decimal) | i (binary) | i >> 1 | i ^ (i >> 1) | Gray (binary) | Gray (decimal) |
---|---|---|---|---|---|
0 | 000 | 000 | 000 | 000 | 0 |
1 | 001 | 000 | 001 | 001 | 1 |
2 | 010 | 001 | 011 | 011 | 3 |
3 | 011 | 001 | 010 | 010 | 2 |
4 | 100 | 010 | 110 | 110 | 6 |
5 | 101 | 010 | 111 | 111 | 7 |
6 | 110 | 011 | 101 | 101 | 5 |
7 | 111 | 011 | 100 | 100 | 4 |
So our Gray code sequence g = [0, 1, 3, 2, 6, 7, 5, 4]
Let's verify adjacent elements differ by one bit:
- 0 (000) and 1 (001) ✓
- 1 (001) and 3 (011) ✓
- 3 (011) and 2 (010) ✓
- 2 (010) and 6 (110) ✓
- 6 (110) and 7 (111) ✓
- 7 (111) and 5 (101) ✓
- 5 (101) and 4 (100) ✓
- 4 (100) and 0 (000) ✓ (wraps around)
Step 2: Find Starting Position
We need to find where start = 5
appears in the sequence:
g.index(5)
returns6
(sinceg[6] = 5
)
Step 3: Rotate the Sequence
Now we rotate the array to place 5 at the beginning:
g[6:]
gives us[5, 4]
g[:6]
gives us[0, 1, 3, 2, 6, 7]
- Concatenating:
[5, 4] + [0, 1, 3, 2, 6, 7] = [5, 4, 0, 1, 3, 2, 6, 7]
Final Result: [5, 4, 0, 1, 3, 2, 6, 7]
Let's verify this satisfies all conditions:
- First element is 5 ✓
- Adjacent elements differ by one bit:
- 5 (101) and 4 (100) ✓
- 4 (100) and 0 (000) ✓
- 0 (000) and 1 (001) ✓
- 1 (001) and 3 (011) ✓
- 3 (011) and 2 (010) ✓
- 2 (010) and 6 (110) ✓
- 6 (110) and 7 (111) ✓
- First and last differ by one bit: 5 (101) and 7 (111) ✓
The rotation preserves all the one-bit difference properties because we're simply cutting a cycle at a different point and unrolling it from there.
Solution Implementation
1class Solution:
2 def circularPermutation(self, n: int, start: int) -> List[int]:
3 # Generate Gray code sequence for n bits
4 # Gray code formula: G(i) = i XOR (i >> 1)
5 # This creates a sequence where adjacent numbers differ by exactly one bit
6 gray_code_sequence = [i ^ (i >> 1) for i in range(1 << n)]
7
8 # Find the index where the start value appears in the Gray code sequence
9 start_index = gray_code_sequence.index(start)
10
11 # Rotate the sequence to begin with the start value
12 # Concatenate the sequence from start_index to end with the sequence from beginning to start_index
13 return gray_code_sequence[start_index:] + gray_code_sequence[:start_index]
14
1class Solution {
2 public List<Integer> circularPermutation(int n, int start) {
3 // Calculate the total number of elements (2^n)
4 int totalElements = 1 << n;
5
6 // Create an array to store the Gray code sequence
7 int[] grayCode = new int[totalElements];
8
9 // Variable to store the index where 'start' appears in the Gray code sequence
10 int startIndex = 0;
11
12 // Generate Gray code for all numbers from 0 to 2^n - 1
13 for (int i = 0; i < totalElements; i++) {
14 // Gray code formula: i XOR (i right-shifted by 1)
15 grayCode[i] = i ^ (i >> 1);
16
17 // Find the position of 'start' in the Gray code sequence
18 if (grayCode[i] == start) {
19 startIndex = i;
20 }
21 }
22
23 // Create the result list to store the circular permutation
24 List<Integer> result = new ArrayList<>();
25
26 // Build the circular permutation starting from 'start'
27 // Iterate through the Gray code array in a circular manner
28 for (int i = startIndex; i < startIndex + totalElements; i++) {
29 // Use modulo to wrap around the array circularly
30 result.add(grayCode[i % totalElements]);
31 }
32
33 return result;
34 }
35}
36
1class Solution {
2public:
3 vector<int> circularPermutation(int n, int start) {
4 // Total number of elements in the permutation (2^n)
5 int totalElements = 1 << n;
6
7 // Array to store Gray code sequence
8 int grayCode[totalElements];
9
10 // Variable to store the index where 'start' appears in Gray code sequence
11 int startIndex = 0;
12
13 // Generate Gray code sequence for n bits
14 // Gray code formula: G(i) = i XOR (i >> 1)
15 for (int i = 0; i < totalElements; ++i) {
16 // Convert binary number i to its Gray code equivalent
17 grayCode[i] = i ^ (i >> 1);
18
19 // Find the position of 'start' in the Gray code sequence
20 if (grayCode[i] == start) {
21 startIndex = i;
22 }
23 }
24
25 // Result vector to store the circular permutation
26 vector<int> result;
27
28 // Build the circular permutation starting from 'start'
29 // Rotate the Gray code sequence so that it begins with 'start'
30 for (int i = startIndex; i < startIndex + totalElements; ++i) {
31 // Use modulo to wrap around and create circular permutation
32 result.push_back(grayCode[i % totalElements]);
33 }
34
35 return result;
36 }
37};
38
1/**
2 * Generates a circular permutation of integers where consecutive numbers differ by exactly one bit.
3 * Uses Gray code sequence starting from a specific value.
4 *
5 * @param n - The number of bits (permutation will contain 2^n numbers)
6 * @param start - The starting value for the circular permutation
7 * @returns An array containing the circular permutation
8 */
9function circularPermutation(n: number, start: number): number[] {
10 const result: number[] = [];
11
12 // Total number of elements in the permutation is 2^n
13 const totalElements = 1 << n;
14
15 // Generate each element of the circular permutation
16 for (let i = 0; i < totalElements; i++) {
17 // Convert index to Gray code: i XOR (i right-shifted by 1)
18 // Then XOR with start to begin the sequence at the specified starting value
19 const grayCode = i ^ (i >> 1);
20 const permutationValue = grayCode ^ start;
21
22 result.push(permutationValue);
23 }
24
25 return result;
26}
27
Time and Space Complexity
Time Complexity: O(2^n)
The algorithm generates a Gray code sequence of length 2^n
(since 1 << n
equals 2^n
). The list comprehension [i ^ (i >> 1) for i in range(1 << n)]
iterates through all 2^n
values to compute the Gray code for each position. The g.index(start)
operation performs a linear search through the list of size 2^n
in the worst case, which is O(2^n)
. The slicing operations g[j:]
and g[:j]
together create a new list by copying all 2^n
elements. Therefore, the overall time complexity is O(2^n)
.
Space Complexity: O(2^n)
The algorithm creates a list g
that stores all 2^n
Gray code values. The final return statement creates a new list by concatenating two slices, which also contains 2^n
elements. Although we temporarily have two lists of size 2^n
in memory, the space complexity is still O(2^n)
as constant factors are ignored in Big-O notation.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Attempting to Build the Sequence Manually with Backtracking
A common mistake is trying to construct the permutation from scratch using backtracking or DFS, checking bit differences at each step:
# Incorrect/Inefficient Approach
def circularPermutation(n, start):
def countBitDiff(a, b):
return bin(a ^ b).count('1')
result = [start]
used = {start}
def backtrack():
if len(result) == (1 << n):
return countBitDiff(result[-1], result[0]) == 1
for i in range(1 << n):
if i not in used and countBitDiff(result[-1], i) == 1:
result.append(i)
used.add(i)
if backtrack():
return True
result.pop()
used.remove(i)
return False
backtrack()
return result
Why this fails: This approach has exponential time complexity and will timeout for larger values of n
. It explores many invalid paths before finding a valid solution.
2. Misunderstanding the Gray Code Formula
Some might incorrectly implement the Gray code conversion:
# Wrong: Using left shift instead of right shift
gray = [i ^ (i << 1) for i in range(1 << n)] # Incorrect!
# Wrong: Forgetting the XOR operation
gray = [i >> 1 for i in range(1 << n)] # Incorrect!
Solution: Always use the correct Gray code formula: i ^ (i >> 1)
3. Not Handling Edge Cases Properly
Forgetting to validate that start
is within the valid range:
def circularPermutation(n, start):
# Missing validation
gray = [i ^ (i >> 1) for i in range(1 << n)]
j = gray.index(start) # Will throw ValueError if start >= 2^n
return gray[j:] + gray[:j]
Solution: Add input validation (though LeetCode problems typically guarantee valid inputs):
def circularPermutation(n, start):
if start >= (1 << n):
return [] # or handle appropriately
gray = [i ^ (i >> 1) for i in range(1 << n)]
j = gray.index(start)
return gray[j:] + gray[:j]
4. Incorrect Rotation Logic
Mixing up the slice indices when rotating:
# Wrong: This doesn't properly rotate the array return gray[:j] + gray[j:] # Wrong order! # Wrong: Off-by-one errors return gray[j+1:] + gray[:j+1] # Skips start element!
Solution: Use the correct slicing: gray[j:] + gray[:j]
to place element at index j
at the beginning.
5. Trying to Generate Gray Code Recursively Without Memoization
While Gray code can be generated recursively, doing so without proper optimization is inefficient:
# Inefficient recursive approach
def generateGray(n):
if n == 0:
return [0]
prev = generateGray(n-1) # Repeated computation
return prev + [x + (1 << (n-1)) for x in reversed(prev)]
Solution: Use the direct formula i ^ (i >> 1)
for efficiency, or implement memoization if using recursion.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
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
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
Want a Structured Path to Master System Design Too? Don’t Miss This!