Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1718. Construct the Lexicographically Largest Valid Sequence - Leetcode Solution

Problem Description

Given an integer n, construct a sequence seq of length 2 * n - 1 using the numbers from 1 to n (each number used exactly twice except for 1, which is used once). The arrangement must satisfy the following rule:

  • If seq[i] = x and x > 1, then seq[i + x] must also be x (i.e., the two occurrences of x are exactly x indices apart).
  • All positions in seq must be filled, and each number must appear the correct number of times.
  • Among all possible valid sequences, return the lexicographically largest one.

For example, for n = 3, a valid sequence is [3, 1, 2, 3, 2].

Thought Process

At first glance, this problem seems similar to permutation or backtracking problems where we need to place numbers under certain constraints. A brute-force approach would be to generate all possible sequences and check which ones are valid, but this quickly becomes infeasible as n increases because the number of possibilities grows factorially.

To optimize, we observe that we want the lexicographically largest result. This means we should try to place the largest numbers first, as early as possible in the sequence. We also need to respect the distance constraint for each number: for x > 1, its two occurrences must be exactly x indices apart.

The problem thus becomes a backtracking search, but with a greedy twist: always try to place the largest unused number at the earliest possible valid position. If we ever get stuck, we backtrack and try the next possibility.

Solution Approach

We use a recursive backtracking algorithm with the following steps:

  1. Initialize: Create an array seq of length 2 * n - 1 filled with zeros to represent empty slots. Also, maintain a boolean array or set to track which numbers have been used.
  2. Backtracking: At each position in seq, try to place the largest unused number that fits:
    • For each number x from n down to 1:
    • If x has not been used, and for x > 1, both seq[i] and seq[i + x] are empty, place x at both positions.
    • For x = 1, only one occurrence is needed; place it at the first available slot.
    • Mark x as used and recursively proceed to the next empty slot.
    • If at any point the placement leads to a dead end (no valid number can be placed), backtrack and try the next possibility.
  3. Lexicographical Order: Always attempt numbers in descending order to ensure the sequence is as large as possible lexicographically.
  4. Termination: When the sequence is completely filled, return it as the answer.

We use a hash set or boolean array for O(1) lookups to check if a number is already used. The recursive structure ensures all constraints are met, and the greedy order ensures the largest sequence.

Example Walkthrough

Let's walk through the algorithm for n = 3:

  1. Initial state: seq = [0, 0, 0, 0, 0]
  2. Try largest number (3): Try to place 3 at index 0 and 3 at index 0+3=3.
    seq = [3, 0, 0, 3, 0]
  3. Next empty slot (index 1): Try next largest unused number (2).
    Can we place 2 at index 1 and 1+2=3? No, 3 is already at index 3.
    Try 2 at index 2 and 2+2=4: both positions empty.
    seq = [3, 0, 2, 3, 2]
  4. Next empty slot (index 1): Try 1 (only one occurrence needed).
    seq = [3, 1, 2, 3, 2]
  5. All numbers are placed, and all constraints are satisfied. This is the lexicographically largest valid sequence.

If at any step a placement is not possible, the algorithm backtracks and tries the next possible number or position.

Time and Space Complexity

Brute-force approach: Would try all permutations of the numbers, checking each for validity. The number of permutations is approximately (2n-1)!, which is infeasible for larger n.

Optimized approach: The backtracking algorithm still explores many possibilities, but pruning (by skipping invalid placements and always starting with the largest numbers) reduces the search space significantly. In the worst case, the time complexity remains exponential, but in practice, it is much faster due to early pruning.

  • Time Complexity: O(n!) in the worst case, but much less in practice because of pruning and constraints.
  • Space Complexity: O(n) for the recursion stack and O(n) for the sequence array.

Summary

The key to solving this problem efficiently is to combine backtracking with a greedy strategy: always try to place the largest number first, and only backtrack when necessary. By carefully managing the constraints and using data structures for quick lookups, we can construct the lexicographically largest valid sequence without exhaustively checking all possibilities. This approach balances correctness and efficiency, making it elegant and practical for reasonable values of n.

Code Implementation

class Solution:
    def constructDistancedSequence(self, n: int) -> List[int]:
        res = [0] * (2 * n - 1)
        used = [False] * (n + 1)
        
        def backtrack(idx):
            if idx == len(res):
                return True
            if res[idx] != 0:
                return backtrack(idx + 1)
            for num in range(n, 0, -1):
                if used[num]:
                    continue
                if num == 1:
                    res[idx] = 1
                    used[1] = True
                    if backtrack(idx + 1):
                        return True
                    res[idx] = 0
                    used[1] = False
                else:
                    j = idx + num
                    if j < len(res) and res[idx] == 0 and res[j] == 0:
                        res[idx] = res[j] = num
                        used[num] = True
                        if backtrack(idx + 1):
                            return True
                        res[idx] = res[j] = 0
                        used[num] = False
            return False
        
        backtrack(0)
        return res
      
class Solution {
public:
    vector<int> res;
    vector<bool> used;
    int n, N;
    bool backtrack(int idx) {
        if (idx == N) return true;
        if (res[idx] != 0) return backtrack(idx + 1);
        for (int num = n; num >= 1; --num) {
            if (used[num]) continue;
            if (num == 1) {
                res[idx] = 1;
                used[1] = true;
                if (backtrack(idx + 1)) return true;
                res[idx] = 0;
                used[1] = false;
            } else {
                int j = idx + num;
                if (j < N && res[idx] == 0 && res[j] == 0) {
                    res[idx] = res[j] = num;
                    used[num] = true;
                    if (backtrack(idx + 1)) return true;
                    res[idx] = res[j] = 0;
                    used[num] = false;
                }
            }
        }
        return false;
    }
    vector<int> constructDistancedSequence(int n_) {
        n = n_;
        N = 2 * n - 1;
        res.assign(N, 0);
        used.assign(n + 1, false);
        backtrack(0);
        return res;
    }
};
      
class Solution {
    int[] res;
    boolean[] used;
    int n, N;
    public int[] constructDistancedSequence(int n) {
        this.n = n;
        this.N = 2 * n - 1;
        res = new int[N];
        used = new boolean[n + 1];
        backtrack(0);
        return res;
    }
    private boolean backtrack(int idx) {
        if (idx == N) return true;
        if (res[idx] != 0) return backtrack(idx + 1);
        for (int num = n; num >= 1; --num) {
            if (used[num]) continue;
            if (num == 1) {
                res[idx] = 1;
                used[1] = true;
                if (backtrack(idx + 1)) return true;
                res[idx] = 0;
                used[1] = false;
            } else {
                int j = idx + num;
                if (j < N && res[idx] == 0 && res[j] == 0) {
                    res[idx] = res[j] = num;
                    used[num] = true;
                    if (backtrack(idx + 1)) return true;
                    res[idx] = res[j] = 0;
                    used[num] = false;
                }
            }
        }
        return false;
    }
}
      
var constructDistancedSequence = function(n) {
    const N = 2 * n - 1;
    const res = new Array(N).fill(0);
    const used = new Array(n + 1).fill(false);

    function backtrack(idx) {
        if (idx === N) return true;
        if (res[idx] !== 0) return backtrack(idx + 1);
        for (let num = n; num >= 1; --num) {
            if (used[num]) continue;
            if (num === 1) {
                res[idx] = 1;
                used[1] = true;
                if (backtrack(idx + 1)) return true;
                res[idx] = 0;
                used[1] = false;
            } else {
                let j = idx + num;
                if (j < N && res[idx] === 0 && res[j] === 0) {
                    res[idx] = res[j] = num;
                    used[num] = true;
                    if (backtrack(idx + 1)) return true;
                    res[idx] = res[j] = 0;
                    used[num] = false;
                }
            }
        }
        return false;
    }
    backtrack(0);
    return res;
};