1718. Construct the Lexicographically Largest Valid Sequence
Problem Description
You need to construct a sequence using integers from 1 to n with specific placement rules.
The sequence must follow these constraints:
- The number 1 appears exactly once in the sequence
- Every number from 2 to n appears exactly twice in the sequence
- For each number
i
(where 2 ≤ i ≤ n), the two occurrences must be separated by exactlyi
positions
The distance between two positions in the sequence is calculated as the absolute difference of their indices. For example, if number i
appears at positions a
and b
, then |b - a| = i
.
Since the sequence contains one occurrence of 1 and two occurrences each of numbers 2 through n, the total length of the sequence will be 2n - 1
.
Among all valid sequences that satisfy these constraints, you need to return the lexicographically largest one. A sequence is lexicographically larger if at the first position where two sequences differ, it contains a larger number.
For example, when n = 3
:
- A valid sequence could be
[3, 1, 2, 3, 2]
- Number 1 appears once at position 1 (0-indexed)
- Number 2 appears at positions 2 and 4, with distance |4 - 2| = 2
- Number 3 appears at positions 0 and 3, with distance |3 - 0| = 3
The problem guarantees that a valid solution always exists for any given n.
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 involves constructing a sequence with specific placement rules, not nodes and edges as in graph problems.
Need to solve for kth smallest/largest?
- No: While we need the lexicographically largest sequence, we're not finding the kth element from a collection. We're constructing a specific sequence that satisfies constraints.
Involves Linked Lists?
- No: The problem deals with constructing an array/sequence, not linked list operations.
Does the problem have small constraints?
- Yes: The constraint is
1 ≤ n ≤ 20
, which means the maximum sequence length is2n - 1 = 39
. This is a small constraint that makes exhaustive search feasible.
Brute force / Backtracking?
-
Yes: Given the small constraints and the need to:
- Try different placements for each number
- Ensure distance constraints are satisfied
- Find the lexicographically largest valid sequence
- Backtrack when a placement doesn't lead to a valid solution
Backtracking is the appropriate approach.
Conclusion: The flowchart correctly leads us to use a Backtracking approach. We need to:
- Try placing numbers from largest to smallest (to ensure lexicographically largest result)
- For each number
i
, attempt to place it at positionu
andu+i
- Recursively try to fill the remaining positions
- Backtrack if we reach an invalid state
- Return the first valid complete sequence found (which will be lexicographically largest due to our ordering)
Intuition
To find the lexicographically largest valid sequence, we need to prioritize placing larger numbers as early as possible in the sequence. This immediately suggests a greedy strategy combined with backtracking.
Think about building the sequence position by position from left to right. At each empty position, we want to place the largest available number that can still satisfy the distance constraint. For a number i
(where i > 1
), if we place it at position u
, we must also place it at position u + i
. Both positions must be empty for this placement to be valid.
The key insight is that we should try numbers in descending order (from n
down to 1
) at each position. This ensures that whenever we successfully place a number, it's the largest possible choice for that position, contributing to the lexicographically largest result.
Why backtracking? Because placing a large number early might block valid placements for other numbers later. For example, if we place number 3
at positions 0
and 3
, we've now occupied those spots. If later we can't place all remaining numbers due to this choice, we need to undo it (backtrack) and try the next smaller number.
The special case is number 1
, which only appears once. We handle it separately - when we encounter an empty position and all numbers from n
down to 2
can't be placed there (either they're already used or their pair position would be out of bounds or occupied), we try placing 1
.
The algorithm naturally terminates when we've filled all 2n - 1
positions, meaning we've successfully placed number 1
once and all other numbers twice with their required distances. Since we always try larger numbers first, the first complete valid sequence we find is guaranteed to be the lexicographically largest one.
Learn more about Backtracking patterns.
Solution Approach
The implementation uses a depth-first search (DFS) with backtracking to construct the sequence. Let's walk through the key components:
Data Structures:
path
: An array of size2n
to store the sequence being constructed. We use indices from 1 to2n-1
(ignoring index 0).cnt
: An array tracking how many times each number can still be used. Initially,cnt[i] = 2
for all numbers exceptcnt[1] = 1
since 1 appears only once.
Main Algorithm Flow:
The dfs(u)
function tries to fill position u
in the sequence:
-
Base Case: If
u == n * 2
, we've filled all positions successfully, returnTrue
. -
Skip Filled Positions: If
path[u]
is already filled, move to the next position withdfs(u + 1)
. -
Try Placing Numbers (n down to 2): For each number
i
fromn
to2
in descending order:- Check if number
i
is available (cnt[i] > 0
) - Check if the paired position
u + i
is within bounds and empty - If valid:
- Place
i
at both positions:path[u] = path[u + i] = i
- Mark
i
as used:cnt[i] = 0
- Recursively try to fill the next position
- If successful, return
True
- Otherwise, backtrack: restore
path[u] = path[u + i] = 0
andcnt[i] = 2
- Place
- Check if number
-
Try Placing Number 1: After trying all numbers from
n
to2
, try placing1
:- Check if
1
is available (cnt[1] > 0
) - Place it at position
u
:path[u] = 1
andcnt[1] = 0
- Recursively try to fill the next position
- If unsuccessful, backtrack:
path[u] = 0
andcnt[1] = 1
- Check if
-
Return False: If no valid placement works, return
False
to trigger backtracking.
Why This Works:
The algorithm explores possibilities in order of preference (larger numbers first). When we place a number i
at position u
, we immediately place its pair at position u + i
, ensuring the distance constraint is satisfied. The backtracking mechanism allows us to undo choices that lead to dead ends.
Starting the DFS from position 1 (not 0) and returning path[1:]
gives us the final sequence of length 2n - 1
.
The greedy choice of trying larger numbers first, combined with the exhaustive backtracking search, guarantees we find the lexicographically largest valid sequence.
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 walk through the algorithm with n = 3
to see how it constructs the lexicographically largest sequence.
Setup:
- We need a sequence of length
2n - 1 = 5
path = [0, 0, 0, 0, 0, 0, 0]
(indices 1-6, we'll use 1-5)cnt = [0, 1, 2, 2]
(number 1 appears once, numbers 2 and 3 appear twice)
DFS Execution:
Step 1: dfs(1)
- Fill position 1
- Try
i = 3
: Can we place 3 at positions 1 and 4 (1+3)?- Position 1 is empty ✓
- Position 4 is empty ✓
- Place:
path = [0, 3, 0, 0, 3, 0, 0]
,cnt[3] = 0
- Continue with
dfs(2)
Step 2: dfs(2)
- Fill position 2
- Position 2 is empty, need to fill it
- Try
i = 3
: Already used (cnt[3] = 0
) ✗ - Try
i = 2
: Can we place 2 at positions 2 and 4 (2+2)?- Position 2 is empty ✓
- Position 4 is NOT empty (has 3) ✗
- Try
i = 1
: Can we place 1 at position 2?cnt[1] = 1
(available) ✓- Place:
path = [0, 3, 1, 0, 3, 0, 0]
,cnt[1] = 0
- Continue with
dfs(3)
Step 3: dfs(3)
- Fill position 3
- Position 3 is empty, need to fill it
- Try
i = 3
: Already used ✗ - Try
i = 2
: Can we place 2 at positions 3 and 5 (3+2)?- Position 3 is empty ✓
- Position 5 is empty ✓
- Place:
path = [0, 3, 1, 2, 3, 2, 0]
,cnt[2] = 0
- Continue with
dfs(4)
Step 4: dfs(4)
- Skip position 4
- Position 4 already has 3, skip to
dfs(5)
Step 5: dfs(5)
- Skip position 5
- Position 5 already has 2, skip to
dfs(6)
Step 6: dfs(6)
- Base case
u == 6 == n * 2
, we've filled all positions!- Return
True
Final Result:
The algorithm returns path[1:6] = [3, 1, 2, 3, 2]
Verification:
- Number 1 appears once at position 1 (0-indexed)
- Number 2 appears at positions 2 and 4, distance = |4 - 2| = 2 ✓
- Number 3 appears at positions 0 and 3, distance = |3 - 0| = 3 ✓
This is indeed the lexicographically largest valid sequence for n = 3
. The algorithm found it by greedily placing 3 first (the largest number), then fitting in the remaining numbers while satisfying all constraints.
Solution Implementation
1class Solution:
2 def constructDistancedSequence(self, n: int) -> List[int]:
3 """
4 Constructs a lexicographically largest sequence where:
5 - Each number from 1 to n appears exactly twice (except 1 appears once)
6 - For number i (i > 1), the two occurrences are exactly i positions apart
7 - For number 1, it appears exactly once
8
9 Args:
10 n: The maximum number to use in the sequence
11
12 Returns:
13 The lexicographically largest valid sequence
14 """
15
16 def backtrack(position: int) -> bool:
17 """
18 Recursively tries to fill the sequence starting from the given position.
19
20 Args:
21 position: Current position in the sequence to fill
22
23 Returns:
24 True if a valid sequence can be constructed, False otherwise
25 """
26 # Base case: all positions have been filled successfully
27 if position == sequence_length:
28 return True
29
30 # If current position is already filled, move to next position
31 if sequence[position] != 0:
32 return backtrack(position + 1)
33
34 # Try numbers from n down to 2 (larger numbers first for lexicographic order)
35 for number in range(n, 1, -1):
36 # Check if this number is still available to use
37 if available_count[number] > 0:
38 second_position = position + number
39
40 # Check if the second occurrence position is valid and empty
41 if second_position < sequence_length and sequence[second_position] == 0:
42 # Place the number at both positions
43 available_count[number] = 0
44 sequence[position] = number
45 sequence[second_position] = number
46
47 # Recursively try to fill the rest
48 if backtrack(position + 1):
49 return True
50
51 # Backtrack if unsuccessful
52 sequence[position] = 0
53 sequence[second_position] = 0
54 available_count[number] = 2
55
56 # Try placing number 1 (which appears only once)
57 if available_count[1] > 0:
58 available_count[1] = 0
59 sequence[position] = 1
60
61 if backtrack(position + 1):
62 return True
63
64 # Backtrack if unsuccessful
65 sequence[position] = 0
66 available_count[1] = 1
67
68 return False
69
70 # Initialize the sequence length (2n - 1 positions total)
71 sequence_length = n * 2
72
73 # Initialize the sequence with zeros (empty positions)
74 sequence = [0] * sequence_length
75
76 # Initialize count of available uses for each number
77 # Numbers 2 to n can be used twice, number 1 can be used once
78 available_count = [2] * sequence_length
79 available_count[1] = 1
80
81 # Start backtracking from position 1 (index 1)
82 backtrack(1)
83
84 # Return the sequence excluding the first element (index 0)
85 return sequence[1:]
86
1class Solution {
2 // Array to store the constructed sequence (1-indexed)
3 private int[] sequence;
4 // Array to track remaining occurrences of each number
5 private int[] remainingCount;
6 // The input value n
7 private int n;
8
9 public int[] constructDistancedSequence(int n) {
10 this.n = n;
11 // Initialize sequence array with size n*2 (1-indexed, so we need extra space)
12 sequence = new int[n * 2];
13 // Initialize count array to track remaining uses of each number
14 remainingCount = new int[n * 2];
15 // Each number from 2 to n appears twice
16 Arrays.fill(remainingCount, 2);
17 // Number 1 appears only once
18 remainingCount[1] = 1;
19
20 // Start DFS from position 1 (1-indexed)
21 dfs(1);
22
23 // Convert result to 0-indexed array of size 2n-1
24 int[] result = new int[n * 2 - 1];
25 for (int i = 0; i < result.length; i++) {
26 result[i] = sequence[i + 1];
27 }
28 return result;
29 }
30
31 private boolean dfs(int position) {
32 // Base case: all positions filled successfully
33 if (position == n * 2) {
34 return true;
35 }
36
37 // If current position is already filled, move to next position
38 if (sequence[position] > 0) {
39 return dfs(position + 1);
40 }
41
42 // Try placing numbers from n down to 2 (lexicographically largest first)
43 for (int number = n; number > 1; number--) {
44 // Check if we can place this number at current position
45 // Conditions: number still has remaining uses AND
46 // the paired position (position + number) is valid and empty
47 if (remainingCount[number] > 0 &&
48 position + number < n * 2 &&
49 sequence[position + number] == 0) {
50
51 // Place the number at both positions
52 remainingCount[number] = 0;
53 sequence[position] = number;
54 sequence[position + number] = number;
55
56 // Recursively try to fill the rest
57 if (dfs(position + 1)) {
58 return true;
59 }
60
61 // Backtrack: restore the state
62 remainingCount[number] = 2;
63 sequence[position] = 0;
64 sequence[position + number] = 0;
65 }
66 }
67
68 // Try placing 1 (which only appears once)
69 if (remainingCount[1] > 0) {
70 sequence[position] = 1;
71 remainingCount[1] = 0;
72
73 // Recursively try to fill the rest
74 if (dfs(position + 1)) {
75 return true;
76 }
77
78 // Backtrack: restore the state
79 remainingCount[1] = 1;
80 sequence[position] = 0;
81 }
82
83 // No valid placement found
84 return false;
85 }
86}
87
1class Solution {
2public:
3 int n;
4 vector<int> remainingCount; // Tracks how many times each number can still be used
5 vector<int> sequence; // The sequence being constructed
6
7 vector<int> constructDistancedSequence(int _n) {
8 n = _n;
9
10 // Initialize data structures
11 remainingCount.resize(n * 2, 2); // Each number (except 1) appears twice
12 sequence.resize(n * 2); // Sequence has length 2n - 1 (indexed from 1)
13 remainingCount[1] = 1; // Number 1 appears only once
14
15 // Start DFS from position 1 (1-indexed)
16 dfs(1);
17
18 // Convert result to 0-indexed vector (excluding index 0)
19 vector<int> result;
20 for (int i = 1; i < n * 2; ++i) {
21 result.push_back(sequence[i]);
22 }
23
24 return result;
25 }
26
27private:
28 bool dfs(int position) {
29 // Base case: all positions filled successfully
30 if (position == n * 2) {
31 return true;
32 }
33
34 // If current position already filled, move to next position
35 if (sequence[position]) {
36 return dfs(position + 1);
37 }
38
39 // Try placing numbers from n down to 2 (greedy: larger numbers first for lexicographically largest result)
40 for (int number = n; number > 1; --number) {
41 // Check if we can place this number at current position
42 // For number i (i > 1), the second occurrence must be at position + i
43 if (remainingCount[number] &&
44 position + number < n * 2 &&
45 !sequence[position + number]) {
46
47 // Place the number at both required positions
48 sequence[position] = number;
49 sequence[position + number] = number;
50 remainingCount[number] = 0;
51
52 // Recursively try to fill remaining positions
53 if (dfs(position + 1)) {
54 return true;
55 }
56
57 // Backtrack if unsuccessful
58 remainingCount[number] = 2;
59 sequence[position] = 0;
60 sequence[position + number] = 0;
61 }
62 }
63
64 // Try placing number 1 (it only appears once)
65 if (remainingCount[1]) {
66 sequence[position] = 1;
67 remainingCount[1] = 0;
68
69 // Recursively try to fill remaining positions
70 if (dfs(position + 1)) {
71 return true;
72 }
73
74 // Backtrack if unsuccessful
75 remainingCount[1] = 1;
76 sequence[position] = 0;
77 }
78
79 // No valid placement found
80 return false;
81 }
82};
83
1let n: number;
2let remainingCount: number[]; // Tracks how many times each number can still be used
3let sequence: number[]; // The sequence being constructed
4
5function constructDistancedSequence(_n: number): number[] {
6 n = _n;
7
8 // Initialize data structures
9 remainingCount = new Array(n * 2).fill(2); // Each number (except 1) appears twice
10 sequence = new Array(n * 2).fill(0); // Sequence has length 2n - 1 (indexed from 1)
11 remainingCount[1] = 1; // Number 1 appears only once
12
13 // Start DFS from position 1 (1-indexed)
14 dfs(1);
15
16 // Convert result to 0-indexed array (excluding index 0)
17 const result: number[] = [];
18 for (let i = 1; i < n * 2; i++) {
19 result.push(sequence[i]);
20 }
21
22 return result;
23}
24
25function dfs(position: number): boolean {
26 // Base case: all positions filled successfully
27 if (position === n * 2) {
28 return true;
29 }
30
31 // If current position already filled, move to next position
32 if (sequence[position]) {
33 return dfs(position + 1);
34 }
35
36 // Try placing numbers from n down to 2 (greedy: larger numbers first for lexicographically largest result)
37 for (let number = n; number > 1; number--) {
38 // Check if we can place this number at current position
39 // For number i (i > 1), the second occurrence must be at position + i
40 if (remainingCount[number] &&
41 position + number < n * 2 &&
42 !sequence[position + number]) {
43
44 // Place the number at both required positions
45 sequence[position] = number;
46 sequence[position + number] = number;
47 remainingCount[number] = 0;
48
49 // Recursively try to fill remaining positions
50 if (dfs(position + 1)) {
51 return true;
52 }
53
54 // Backtrack if unsuccessful
55 remainingCount[number] = 2;
56 sequence[position] = 0;
57 sequence[position + number] = 0;
58 }
59 }
60
61 // Try placing number 1 (it only appears once)
62 if (remainingCount[1]) {
63 sequence[position] = 1;
64 remainingCount[1] = 0;
65
66 // Recursively try to fill remaining positions
67 if (dfs(position + 1)) {
68 return true;
69 }
70
71 // Backtrack if unsuccessful
72 remainingCount[1] = 1;
73 sequence[position] = 0;
74 }
75
76 // No valid placement found
77 return false;
78}
79
Time and Space Complexity
Time Complexity: O(n!)
The algorithm uses backtracking to construct a valid sequence. In the worst case, we need to try all possible permutations of numbers from 1 to n, with specific placement constraints:
- For each position
u
in the path, we try to place numbers fromn
down to1
- For number
i
(wherei > 1
), we need to place it twice with exactlyi-1
positions between them - For number
1
, we only place it once - The backtracking explores all valid placement combinations until finding the lexicographically largest valid sequence
- While pruning occurs due to constraints (distance requirements and available positions), the worst-case remains factorial as we potentially explore all valid arrangements of n numbers with their specific placement rules
Space Complexity: O(n)
The space complexity consists of:
path
array of size2n - 1
(since we need2n - 1
positions for n numbers where each number except 1 appears twice):O(n)
cnt
array of size2n
to track remaining occurrences of each number:O(n)
- Recursion stack depth is at most
2n - 1
(the length of the final sequence):O(n)
- Total space complexity:
O(n) + O(n) + O(n) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Distance Calculation Between Paired Numbers
Pitfall: A common mistake is misunderstanding how the distance constraint works. When placing number i
at position u
, developers might incorrectly calculate the second position or confuse zero-based vs one-based indexing.
Example of Wrong Implementation:
# WRONG: Using incorrect distance calculation second_position = position + number - 1 # Off by one error # or second_position = position + number + 1 # Adding extra distance
Solution: The correct calculation is straightforward: if number i
is placed at position u
, the second occurrence should be at position u + i
. This ensures the distance between them is exactly i
:
second_position = position + number # Correct # Distance = second_position - position = number
2. Array Index Confusion (0-based vs 1-based)
Pitfall: The code uses 1-based indexing for the sequence array but returns sequence[1:]
. This mixing of index conventions can lead to off-by-one errors or accessing wrong positions.
Why it happens: The sequence array is allocated with size 2n
but only positions 1 through 2n-1
are used. Position 0 is never filled, which can be confusing.
Solution: Either:
- Use consistent 0-based indexing throughout:
sequence_length = 2 * n - 1 sequence = [0] * sequence_length # Start backtracking from position 0 backtrack(0) return sequence # Return entire array
- Or clearly document why 1-based indexing is used and ensure all position calculations account for it.
3. Forgetting to Reset the Available Count During Backtracking
Pitfall: When backtracking, forgetting to restore the available_count
for a number can lead to incorrect results where valid solutions are missed.
Wrong:
# Placing the number available_count[number] = 0 sequence[position] = number sequence[second_position] = number if backtrack(position + 1): return True # WRONG: Forgetting to restore available_count sequence[position] = 0 sequence[second_position] = 0 # available_count[number] = 2 # Missing this line!
Solution: Always restore both the sequence positions AND the available count:
# Backtrack completely sequence[position] = 0 sequence[second_position] = 0 available_count[number] = 2 # Must restore this
4. Not Checking Bounds for the Second Position
Pitfall: Forgetting to verify that second_position
is within the valid range before accessing sequence[second_position]
.
Wrong:
# WRONG: No bounds checking if sequence[second_position] == 0: # Could cause IndexError # place numbers...
Solution: Always check bounds first:
if second_position < sequence_length and sequence[second_position] == 0: # Safe to place numbers
5. Trying Numbers in Wrong Order for Lexicographic Maximization
Pitfall: Iterating through numbers in ascending order (1 to n) instead of descending order will find a valid sequence but not the lexicographically largest one.
Wrong:
# WRONG: This finds a valid sequence but not the largest
for number in range(2, n + 1): # Ascending order
# try placing number...
Solution: Always try larger numbers first:
for number in range(n, 1, -1): # Descending order
# try placing number...
This ensures we place larger numbers in earlier positions when possible, creating the lexicographically largest sequence.
Which two pointer techniques do you use to check if a string is a palindrome?
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!