Facebook Pixel

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 exactly i 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 is 2n - 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:

  1. Try placing numbers from largest to smallest (to ensure lexicographically largest result)
  2. For each number i, attempt to place it at position u and u+i
  3. Recursively try to fill the remaining positions
  4. Backtrack if we reach an invalid state
  5. Return the first valid complete sequence found (which will be lexicographically largest due to our ordering)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 size 2n to store the sequence being constructed. We use indices from 1 to 2n-1 (ignoring index 0).
  • cnt: An array tracking how many times each number can still be used. Initially, cnt[i] = 2 for all numbers except cnt[1] = 1 since 1 appears only once.

Main Algorithm Flow:

The dfs(u) function tries to fill position u in the sequence:

  1. Base Case: If u == n * 2, we've filled all positions successfully, return True.

  2. Skip Filled Positions: If path[u] is already filled, move to the next position with dfs(u + 1).

  3. Try Placing Numbers (n down to 2): For each number i from n to 2 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 and cnt[i] = 2
  4. Try Placing Number 1: After trying all numbers from n to 2, try placing 1:

    • Check if 1 is available (cnt[1] > 0)
    • Place it at position u: path[u] = 1 and cnt[1] = 0
    • Recursively try to fill the next position
    • If unsuccessful, backtrack: path[u] = 0 and cnt[1] = 1
  5. 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 Evaluator

Example 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 from n down to 1
  • For number i (where i > 1), we need to place it twice with exactly i-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 size 2n - 1 (since we need 2n - 1 positions for n numbers where each number except 1 appears twice): O(n)
  • cnt array of size 2n 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.

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

Which two pointer techniques do you use to check if a string is a palindrome?


Recommended Readings

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

Load More