1718. Construct the Lexicographically Largest Valid Sequence
Problem Description
The challenge is to create a sequence based on a given integer n
with a specific set of requirements. The sequence should be such that:
- The integer
1
appears exactly once. - Each integer from
2
ton
appears exactly twice. - For each integer
i
from2
ton
, the locations of the two occurrences ofi
in the sequence are separated by exactly a distance ofi
. This means if the first occurrence is at positionj
, the second occurrence must be at positionj + i
.
The goal is to return the lexicographically largest sequence possible while satisfying the above conditions. Remember, a sequence a
is considered lexicographically larger than a sequence b
if, at their first differing position, the number in a
is greater than the number in b
.
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 is about constructing a sequence, not about nodes and edges typical in graph problems.
Need to solve for kth smallest/largest?
- No: The task is not focused on sorting elements or finding a specific order such as kth smallest/largest.
Involves Linked Lists?
- No: The objective involves constructing a numerical sequence, not manipulating linked list structures.
Does the problem have small constraints?
- Yes: The problem can be solved efficiently within the typical constraints of an integer array.
Brute force / Backtracking?
- Yes: To construct the lexicographically largest valid sequence, exploration of all possible fitting sequences through backtracking is a practical approach.
Conclusion: The flowchart suggests using a backtracking approach to construct the lexicographically largest valid sequence by exploring valid possibilities and ensuring the constraints are satisfied at each step.
Intuition
The solution to this problem lies in depth-first search (DFS) and utilizes backtracking to explore all possible positions for each integer. Since we’re aiming for the lexicographically largest sequence, it’s beneficial to place the larger numbers first. This is why, in the DFS, integers are attempted in a descending order from n
down to 1
.
Here's how we derive at the approach:
-
Depth-first search (DFS) is a common approach for problems that involve exploring all possible combinations or sequences to find one that meets given conditions. In this case, DFS is used to try different positions for each integer iteratively.
-
We employ backtracking, which goes hand-in-hand with DFS. Backtracking allows us to undo certain steps and roll back to earlier decisions if we determine that the current path doesn't lead to a solution. This is especially important because not every initial placement of an integer will lead to a valid sequence.
-
Beginning with the largest integer and placing it in the sequence before moving on to smaller ones ensures that we will get the lexicographically largest sequence. Because if a smaller number is placed before a larger number, the sequence cannot be the largest as per lexicographic rules.
-
To keep track of the sequence and which numbers have been used, an array
path
is used to represent the current sequence, and a count arraycnt
helps us know how many times an integer has been placed in the sequence (0
for placed twice,2
for not placed at all, and1
for only1
which should be placed once). -
The recursive
dfs
function works by trying to place each number at the current positionu
. If placing the numberi
is valid (not already used up and there is room atu + i
for the second occurrence), the function recurs with the updated sequenceu+1
. If at any step the sequence can't be completed, the function backtracks by resetting the count and path for that number and then tries the next number.
The solution iteratively attempts to construct the sequence by exploring different positions for placing each number and backtracking if it ends up at a dead end. The process continues until the entire sequence is filled satisfactorily.
Learn more about Backtracking patterns.
Solution Approach
The solution provided is a Python implementation of depth-first search (DFS), an algorithmic technique to iterate through all possible sequences that satisfy the problem’s conditions.
Here is a breakdown of the main elements of the solution:
-
Depth-First Search (DFS): DFS is selected for its ability to iterate deep into one possible sequence path before backtracking and trying another path. The nature of DFS makes it fitting for problems where we need to explore all possible combinations to find one that meets particular criteria.
-
Backtracking: Backtracking is a form of recursion that involves choosing a path, and if it leads to an undesirable solution, we retreat and choose another path. The implementation uses backtracking to place numbers in the sequence, then backtrack if the current number choice does not lead to a valid sequence.
-
Recursive Function
dfs
: Thedfs
function takes the current positionu
in the sequence array as an argument. It tries to find a suitable place for each integer, beginning withn
and decrementing. If the integer can be placed without violating the conditions,dfs
is called recursively to try to place the next number. -
Count Array
cnt
: An array calledcnt
keeps a count of how many times each integer can still be used in the sequence. Initially, all values are set to2
(representing the 'unused' state), except for the integer1
set to1
. -
Path Array
path
: An array calledpath
represents the current state of the sequence. Zeros in this array represent empty positions where integers can be placed.
The procedure of the DFS in the code is as follows:
-
Base Case Check: If
u
equals the sequence lengthn * 2
, we have filled all positions in the sequence successfully, and the function returnsTrue
. -
Skip Filled Position: If
path[u]
is non-zero, it means the position is already filled, and the function calls itself with the next positionu + 1
. -
Place Larger Numbers First: The loop runs through integers
i
fromn
down to2
(since1
is only placed once). It checks if the integeri
can be placed at the current and correspondingu + i
positions by ensuring that it has not been used yet (cnt[i] > 0
) and that theu + i
position is empty. -
Placing an Integer and Recursive Call: If the number
i
can be placed, it is put in thepath
at the current position and atu + i
. The count is decremented (or set to0
, meaning the number is placed in both positions), anddfs(u + 1)
is called. -
Backtracking: If the recursive call returns
False
, we backtrack by resetting thepath
andcnt
for the integeri
and trying the next integer. -
Placing the Number
1
: After trying all numbers fromn
down to2
, ifcnt[1]
is non-zero, it means1
has not yet been placed. It is then placed atpath[u]
and a recursive call is made to attempt to place the next numbers.
Once the DFS completes, and if it finds a valid sequence, the path
array holds the lexicographically largest sequence, with the first element trimmed (path[1:]
), as the sequence was built with a "dummy" start position to simplify index calculations.
The algorithm used ensures that the lexicographically largest sequence is found by always trying the larger numbers first. It avoids unnecessary searches by checking if placement is possible before proceeding and backtracks efficiently when it hits a dead end.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's say we want to create a sequence based on the integer n = 3
with the set of requirements mentioned. To illustrate the solution approach using depth-first search (DFS) and backtracking, we'll walk through a small example.
-
We start with the largest number,
n = 3
. We have an empty path[0, 0, 0, 0, 0, 0]
(since the sequence will be of lengthn * 2
) and a count arraycnt
equal to[0, 1, 2, 2]
(0 for filled, 1 for the number 1, and 2 for numbers 2 and 3). -
We try to place
3
in the first position of the path. Since3
should be placed again exactly 3 positions apart, we check the fourth positionpath[3 + 0]
. Both are empty, so we place3
in positions0
and3
, yielding[3, 0, 0, 3, 0, 0]
, and decrementcnt[3]
to0
. -
With
3
placed, we move to the next vacant position and try to place2
. Sincecnt[2]
is non-zero, we can place2
at position1
and subsequently at position1 + 2 = 3
. However, since position3
is already occupied by3
, placing2
here isn't possible. So, we skip this and move to the next available position which is2
. -
At position
2
, we place2
, and then at2 + 2 = 4
, yielding[3, 0, 2, 3, 2, 0]
, and we updatecnt[2]
to0
. -
Now, we only have
1
left to place, which only appears once. The next vacant position is1
, so we place1
there, leading to[3, 1, 2, 3, 2, 0]
. Since the position5
is still empty, the sequence is not complete. -
We place the last
1
at position5
to complete the sequence, resulting in[3, 1, 2, 3, 2, 1]
. We have now constructed a valid sequence withcnt
equal to[0, 0, 0, 0]
indicating all numbers have been placed according to the rules.
By applying DFS and backtracking, we started with the largest number and worked our way down, ensuring at each step that we were creating the lexicographically largest sequence possible. When a number could not be placed, we moved to the next available spot without backtracking since the path wasn't filled yet. Once all numbers were placed correctly, we arrived at the final sequence.
Solution Implementation
1from typing import List
2
3class Solution:
4 def constructDistancedSequence(self, n: int) -> List[int]:
5 # Helper function for depth-first search.
6 def dfs(index):
7 # Base case: if the end of the sequence is reached, return True.
8 if index == len(sequence):
9 return True
10
11 # If the current position already has a value, skip to the next position.
12 if sequence[index]:
13 return dfs(index + 1)
14
15 # Try placing numbers starting from n down to 2.
16 for i in range(n, 1, -1):
17 # Check if the number is available and can fit in the current position.
18 if count[i] > 0 and index + i < len(sequence) and sequence[index+i] == 0:
19 # Place the number at the current and corresponding positions and mark as used.
20 count[i] = 0
21 sequence[index] = sequence[index+i] = i
22 # Recursively try to fill in the next position.
23 if dfs(index + 1):
24 return True
25 # If placing i didn't lead to a solution, backtrack.
26 sequence[index] = sequence[index+i] = 0
27 count[i] = 2
28
29 # Try placing number 1 if available.
30 if count[1] > 0:
31 count[1], sequence[index] = 0, 1
32 # Recursively try to fill in the next position.
33 if dfs(index + 1):
34 return True
35 # If placing 1 didn't lead to a solution, backtrack.
36 sequence[index], count[1] = 0, 1
37
38 # If no number can be placed, return False.
39 return False
40
41 # Initialise the sequence with zeros, length is 'n' elements plus 'n - 1' gaps.
42 sequence = [0] * (n * 2 - 1)
43 # Count array initialized with 2 for numbers 2 to n, and 1 for number 1.
44 count = [2] * (n + 1)
45 count[1] = 1
46
47 # Start DFS from the first position.
48 dfs(0)
49 # Return the sequence with the leading zero removed.
50 return sequence[:n] + sequence[n+1:]
51
52# Example usage:
53
54# sol = Solution()
55# result = sol.constructDistancedSequence(3)
56# print(result) # Output would be the filled sequence satisfying the conditions.
57
1class Solution {
2 private int[] path; // path array to store the sequence
3 private int[] count; // count array to keep track of the usage of numbers
4 private int numberLimit; // the limit 'n' as provided in the problem statement
5
6 // Function to construct the distance sequence
7 public int[] constructDistancedSequence(int n) {
8 this.numberLimit = n;
9 path = new int[n * 2 - 1]; // since the last number will always be '1'
10 count = new int[n + 1]; // to track count of each number
11 Arrays.fill(count, 2); // each number can be used twice except '1'
12 count[1] = 1; // '1' is used only once
13
14 if(dfs(0)) {
15 return path;
16 }
17
18 return new int[0]; // return an empty array if a valid sequence is not found
19 }
20
21 // Helper method for the depth-first search
22 private boolean dfs(int index) {
23 if (index == path.length) {
24 return true; // all the numbers are placed successfully
25 }
26 if (path[index] > 0) {
27 return dfs(index + 1); // skip filled positions
28 }
29
30 for (int i = numberLimit; i > 1; --i) {
31 if (count[i] > 0 && index + i < path.length && path[index + i - 1] == 0) {
32 // Try to place the number 'i'
33 count[i]--;
34 path[index] = i;
35 path[index + i - 1] = i;
36
37 // Recurse, if successful, return true
38 if (dfs(index + 1)) {
39 return true;
40 }
41
42 // Backtrack
43 count[i]++;
44 path[index] = 0;
45 path[index + i - 1] = 0;
46 }
47 }
48
49 if (count[1] > 0) {
50 // Place '1' in the current index
51 path[index] = 1;
52 count[1]--;
53
54 // Recurse, if successful, return true
55 if (dfs(index + 1)) {
56 return true;
57 }
58
59 // Backtrack if placing '1' did not lead to a solution
60 count[1]++;
61 path[index] = 0;
62 }
63
64 // No valid placement was found for this index; return false
65 return false;
66 }
67}
68
1class Solution {
2public:
3 int sequenceLength;
4 vector<int> counter, resultSequence;
5
6 // Function to construct the Distanced Sequence based on the given integer `n`.
7 vector<int> constructDistancedSequence(int n) {
8 sequenceLength = n;
9 counter.resize(n * 2, 2); // Initializing counter with '2' to track the availability of pairs.
10 resultSequence.resize(n * 2); // Creating a result sequence array with 2 * n size.
11 counter[1] = 1; // 1 only appears once in the sequence.
12
13 // Start the Depth First Search from the first position.
14 dfs(1);
15
16 // Compile the result without the zeroth position (1-indexed).
17 vector<int> ans;
18 for (int i = 1; i < n * 2; ++i) {
19 ans.push_back(resultSequence[i]);
20 }
21 return ans;
22 }
23
24 // Recursive function to fill the sequence with valid numbers at the correct distances.
25 bool dfs(int position) {
26 // Base case: if the end of the sequence is reached, the sequence is valid.
27 if (position == sequenceLength * 2) return true;
28
29 // Skip already filled positions.
30 if (resultSequence[position]) return dfs(position + 1);
31
32 // Try to place numbers starting from the largest.
33 for (int i = sequenceLength; i > 1; --i) {
34 // Check if the number is available and the corresponding paired position is not filled.
35 if (counter[i] && position + i < sequenceLength * 2 && !resultSequence[position + i]) {
36 resultSequence[position] = resultSequence[position + i] = i; // Place the number at both positions.
37 counter[i] = 0; // Mark the number as used.
38
39 if (dfs(position + 1)) return true; // Recursively continue to fill the next position.
40
41 // Backtrack if the placement does not lead to a solution.
42 counter[i] = 2;
43 resultSequence[position] = resultSequence[position + i] = 0;
44 }
45 }
46
47 // Special case for '1' as it appears only once.
48 if (counter[1]) {
49 resultSequence[position] = 1; // Place '1' at the current position.
50 counter[1] = 0; // Mark '1' as used.
51
52 if (dfs(position + 1)) return true; // Recursively continue with the next position.
53
54 // Backtrack if placement of '1' does not lead to solution.
55 counter[1] = 1;
56 resultSequence[position] = 0;
57 }
58
59 // If no placement is possible, return false for backtracking.
60 return false;
61 }
62};
63
1// The length of the resulting distanced sequence.
2let sequenceLength: number;
3// Counter to track the availability of pairs (initialized later).
4let counter: number[];
5// Array representing the resulting distanced sequence.
6let resultSequence: number[];
7
8// Function to construct the Distanced Sequence based on the given integer `n`.
9function constructDistancedSequence(n: number): number[] {
10 sequenceLength = 2 * n;
11 counter = new Array(n + 1).fill(2); // Initializing counter with '2' for each pair.
12 resultSequence = new Array(sequenceLength).fill(0); // Creating a result sequence array with 2 * n size, filled with zeros.
13 counter[1] = 1; // 1 only appears once in the sequence.
14
15 // Start the Depth First Search from the first position.
16 dfs(1);
17
18 // Compile the result without the zeroth position (1-indexed equivalent in original array).
19 return resultSequence.slice(1);
20}
21
22// Recursive function to fill the sequence with valid numbers at the correct distances.
23function dfs(position: number): boolean {
24 // Base case: if the end of the sequence is reached, the sequence is completed.
25 if (position === sequenceLength) return true;
26
27 // Skip already filled positions.
28 if (resultSequence[position] !== 0) return dfs(position + 1);
29
30 // Try to place numbers starting from the largest.
31 for (let i = sequenceLength / 2; i > 1; --i) {
32 // Check if the number is available and the corresponding paired position is not filled.
33 if (counter[i] > 0 && position + i < sequenceLength && resultSequence[position + i] === 0) {
34 // Place the number at both positions.
35 resultSequence[position] = i;
36 resultSequence[position + i] = i;
37 // Mark the number as used.
38 counter[i] = 0;
39
40 // Recursively continue to fill the next position.
41 if (dfs(position + 1)) return true;
42
43 // Backtrack if the placement does not lead to a solution.
44 counter[i] = 2;
45 resultSequence[position] = 0;
46 resultSequence[position + i] = 0;
47 }
48 }
49
50 // Special case for '1' as it appears only once.
51 if (counter[1] > 0) {
52 // Place '1' at the current position.
53 resultSequence[position] = 1;
54 // Mark '1' as used.
55 counter[1] = 0;
56
57 // Recursively continue with the next position.
58 if (dfs(position + 1)) return true;
59
60 // Backtrack if placement of '1' does not lead to solution.
61 counter[1] = 1;
62 resultSequence[position] = 0;
63 }
64
65 // If no placement is possible, return false for backtracking.
66 return false;
67}
68
69// Example usage:
70let sequence = constructDistancedSequence(3);
71console.log(sequence); // This would output the distanced sequence based on your selected `n`.
72
Time and Space Complexity
The given Python code defines a function to construct a distinct sequence based on the input n
using a backtracking approach. Let’s analyze its time complexity and space complexity:
Time Complexity
- The maximum size of the sequence to be constructed is
n * 2 - 1
. We'll refer to this asm
. - For each empty position in the path, the algorithm will attempt to place the largest number
(n)
down to2
, and then try1
if all others fail. - The function
dfs
is called recursively. For each placement of a numberi
, it has to placei
at one position andi
ati
positions away from the first, excluding1
which only occupies one position. - Each placement of a number
i > 1
reduces the number of recursive calls byi + 1
(sincei
and the positioni
steps away are filled), and for each placement of1
, the number of recursive calls is reduced by1
. - Since we attempt all possible placements for each number, in the worst case we have to explore nearly all permutations of the numbers greater than
1
. It’s difficult to tightly bound this, but it’s a factorial relationship, leading us to an estimated upper bound ofO((n-1)!*n)
. However, this could be considered overly pessimistic because the early placement of larger numbers greatly restricts subsequent placements.
In practice, the actual time complexity is hard to express in a closed form due to various pruning conditions in the backtracking process, which can significantly reduce the number of branches that need to be explored.
Space Complexity
O(n)
:path
array of sizem
.O(n)
:cnt
array of similar size, although the maximum meaningful index isn
.
Hence, the space complexity is dominated by these data structures and would be O(m)
which simplifies to O(2n - 1)
and can be denoted as O(n)
since we drop constants in big O notation.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
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
LeetCode 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 we
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 algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!