2375. Construct Smallest Number From DI String
Problem Description
You need to create a string of digits based on a pattern of 'I' (increasing) and 'D' (decreasing) characters.
Given a pattern string of length n
containing only 'I' and 'D' characters, you must construct a string num
of length n + 1
following these rules:
- The string
num
can only use digits '1' through '9', and each digit can be used at most once - When
pattern[i] == 'I'
, the digit at positioni
innum
must be less than the digit at positioni + 1
(increasing relationship) - When
pattern[i] == 'D'
, the digit at positioni
innum
must be greater than the digit at positioni + 1
(decreasing relationship)
The goal is to find the lexicographically smallest string that satisfies all these conditions.
For example:
- If pattern = "ID", you need 3 digits where the first is less than the second (due to 'I'), and the second is greater than the third (due to 'D'). A valid answer would be "132".
- If pattern = "DDD", you need 4 digits in strictly decreasing order. The lexicographically smallest would be "4321".
The solution uses a depth-first search (DFS) approach with backtracking. It tries different digit combinations, checking at each step whether the current digit satisfies the increasing or decreasing constraint based on the pattern. The search continues until a valid arrangement is found, which will be the lexicographically smallest due to trying digits in ascending order from 1 to 9.
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 doesn't involve nodes and edges or graph traversal. We're constructing a string based on a pattern.
Need to solve for kth smallest/largest?
- No: While we need the lexicographically smallest string, we're not finding the kth element from a collection.
Involves Linked Lists?
- No: The problem deals with string construction, not linked list operations.
Does the problem have small constraints?
- Yes: The constraints are quite small - we can only use digits 1-9 (at most 9 digits), and each digit can be used at most once. This means the maximum length of our result string is 10.
Brute force / Backtracking?
- Yes: Given the small constraints and the need to find a valid arrangement that satisfies multiple conditions (increasing/decreasing patterns), backtracking is ideal. We need to:
- Try different digit combinations
- Check if each placement satisfies the I/D constraints
- Backtrack when we hit an invalid state
- Continue until we find the lexicographically smallest valid solution
Conclusion: The flowchart correctly leads us to use a Backtracking approach. The small constraint size (max 9 digits) makes it feasible to explore different permutations, and the need to satisfy specific ordering conditions while finding the optimal solution makes backtracking the perfect fit for this problem.
Intuition
When we need the lexicographically smallest string, our first instinct should be to place the smallest available digits as early as possible. However, we're constrained by the pattern - we can't just place digits 1, 2, 3... in order because we must respect the 'I' and 'D' relationships.
The key insight is that with only 9 possible digits and each used at most once, we're dealing with a very limited search space. This makes backtracking feasible - we can systematically try placing each available digit at each position and see if it works.
Why backtracking specifically? Consider what happens when we try to build the string greedily from left to right:
- At position 0, we'd want to place '1' for lexicographic minimality
- But what if the pattern starts with "DDD"? We'd need digits that decrease from '1', which is impossible since we can't use digits less than 1
This shows we can't always make locally optimal choices. Sometimes we need to "reserve" smaller digits for later positions. When we hit a dead end (like trying to find a digit smaller than what we've already placed for a 'D' pattern), we need to backtrack and try a different digit at an earlier position.
The backtracking approach naturally handles this by:
- Trying digits in ascending order (1, 2, 3...) at each position for lexicographic minimality
- Checking if the current digit satisfies the pattern constraint with the previous digit
- If we can't place any valid digit at a position, we backtrack and try the next available digit at the previous position
- The first complete valid string we find will be lexicographically smallest because we always try smaller digits first
This systematic exploration with the ability to undo choices (backtrack) when we hit constraints is exactly what makes backtracking the natural solution for this problem.
Learn more about Stack, Greedy and Backtracking patterns.
Solution Approach
The implementation uses a depth-first search (DFS) with backtracking to systematically explore all possible digit arrangements. Let's walk through the key components:
Data Structures:
vis
: A boolean array of size 10 to track which digits (1-9) have been usedt
: A temporary list to build the current string candidateans
: Stores the first valid solution found (which will be lexicographically smallest)
DFS Function Logic:
The recursive dfs(u)
function works as follows, where u
represents the current position we're trying to fill:
-
Base Case: When
u == len(pattern) + 1
, we've successfully placed all required digits. Since we try digits in ascending order, the first valid solution is the lexicographically smallest, so we save it and return. -
Recursive Case: For each digit from 1 to 9:
- Check if the digit is already used (
vis[i]
) - If we're not at position 0, validate the pattern constraint:
- For 'I' pattern: The previous digit must be less than current digit (
int(t[-1]) < i
) - For 'D' pattern: The previous digit must be greater than current digit (
int(t[-1]) > i
)
- For 'I' pattern: The previous digit must be less than current digit (
- If constraints are satisfied:
- Mark digit as used:
vis[i] = True
- Add digit to current string:
t.append(str(i))
- Recursively try to fill the next position:
dfs(u + 1)
- Backtrack by unmarking the digit and removing it from the string
- Mark digit as used:
- Check if the digit is already used (
Early Termination:
The code includes an optimization with if ans: return
at the beginning of the DFS function. Once we find the first valid solution, we stop exploring further branches since:
- We try digits in ascending order (1, 2, 3...)
- The first valid arrangement found will be lexicographically smallest
- No need to explore other possibilities
Example Walkthrough:
For pattern = "ID":
- Start at position 0, try digit 1
- Move to position 1, need a digit > 1 (due to 'I'), try 2
- Move to position 2, need a digit < 2 (due to 'D'), can't use 1 (already used)
- Backtrack to position 1, try 3
- Move to position 2, need digit < 3, try 2
- Success! Return "132"
The backtracking ensures we systematically explore all possibilities while the digit ordering (trying 1 before 2, 2 before 3, etc.) guarantees the first valid solution is lexicographically smallest.
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 trace through the solution for pattern = "DI" step by step:
Goal: Create a 3-digit string where digit[0] > digit[1] (D) and digit[1] < digit[2] (I)
Step 1: Position 0 (u=0)
- Try digit 1: Place it temporarily → t = ["1"]
- Mark vis[1] = True
Step 2: Position 1 (u=1)
- Pattern[0] = 'D', so we need digit < 1
- Try digits 1-9: None work! (1 is used, and 2-9 are all ≥ 1)
- Backtrack to position 0
Step 3: Back at Position 0
- Remove 1, unmark vis[1] = False
- Try digit 2: Place it → t = ["2"]
- Mark vis[2] = True
Step 4: Position 1 (u=1)
- Pattern[0] = 'D', so we need digit < 2
- Try digit 1: It's available and 1 < 2 ✓
- Place it → t = ["2", "1"]
- Mark vis[1] = True
Step 5: Position 2 (u=2)
- Pattern[1] = 'I', so we need digit > 1
- Try digit 3: It's available and 3 > 1 ✓
- Place it → t = ["2", "1", "3"]
- Mark vis[3] = True
Step 6: Base Case (u=3)
- We've placed all 3 digits successfully
- Save ans = "213" and return
Final Answer: "213"
This example demonstrates:
- How we start with the smallest available digit (1) but backtrack when it doesn't work
- How the 'D' pattern forces us to start with a larger digit to leave room for decrease
- How we systematically try digits in ascending order, ensuring lexicographic minimality
- The first complete valid string we find ("213") is our answer
Solution Implementation
1class Solution:
2 def smallestNumber(self, pattern: str) -> str:
3 """
4 Find the smallest positive integer whose digits follow the given pattern.
5 'I' means the next digit should be greater than current digit.
6 'D' means the next digit should be smaller than current digit.
7
8 Args:
9 pattern: A string consisting of 'I' and 'D' characters
10
11 Returns:
12 The smallest number as a string that follows the pattern
13 """
14
15 def backtrack(position: int) -> None:
16 """
17 Recursively build the number digit by digit using backtracking.
18
19 Args:
20 position: Current position in the result (0 to len(pattern))
21 """
22 nonlocal result
23
24 # If we already found an answer, stop searching
25 if result:
26 return
27
28 # Base case: we've placed all digits successfully
29 if position == len(pattern) + 1:
30 result = ''.join(current_number)
31 return
32
33 # Try each digit from 1 to 9
34 for digit in range(1, 10):
35 # Skip if this digit is already used
36 if used_digits[digit]:
37 continue
38
39 # Check if this digit satisfies the pattern constraint
40 if position > 0:
41 last_digit = int(current_number[-1])
42
43 # For 'I' pattern, next digit must be greater than previous
44 if pattern[position - 1] == 'I' and last_digit >= digit:
45 continue
46
47 # For 'D' pattern, next digit must be smaller than previous
48 if pattern[position - 1] == 'D' and last_digit <= digit:
49 continue
50
51 # Use this digit and continue building
52 used_digits[digit] = True
53 current_number.append(str(digit))
54
55 # Recurse to place the next digit
56 backtrack(position + 1)
57
58 # Backtrack: remove the digit and mark as unused
59 current_number.pop()
60 used_digits[digit] = False
61
62 # Initialize tracking variables
63 used_digits = [False] * 10 # Track which digits (1-9) have been used
64 current_number = [] # Build the result number as list of digit strings
65 result = None # Store the final answer
66
67 # Start the backtracking process from position 0
68 backtrack(0)
69
70 return result
71
1class Solution {
2 // Track which digits (1-9) have been used
3 private boolean[] usedDigits = new boolean[10];
4 // Build the current number being constructed
5 private StringBuilder currentNumber = new StringBuilder();
6 // Store the input pattern
7 private String pattern;
8 // Store the final answer (smallest valid number)
9 private String answer;
10
11 /**
12 * Finds the smallest number whose adjacent digits follow the given pattern.
13 * 'I' means the next digit should be greater than the current digit.
14 * 'D' means the next digit should be less than the current digit.
15 *
16 * @param pattern String consisting of 'I' and 'D' characters
17 * @return The smallest number satisfying the pattern
18 */
19 public String smallestNumber(String pattern) {
20 this.pattern = pattern;
21 dfs(0);
22 return answer;
23 }
24
25 /**
26 * Depth-first search to build the smallest valid number.
27 * Tries digits from 1-9 in order to ensure the smallest result.
28 *
29 * @param position Current position in the number being built
30 */
31 private void dfs(int position) {
32 // Early termination if answer already found
33 if (answer != null) {
34 return;
35 }
36
37 // Base case: completed building a number with length = pattern.length() + 1
38 if (position == pattern.length() + 1) {
39 answer = currentNumber.toString();
40 return;
41 }
42
43 // Try each digit from 1 to 9 (smallest to largest for lexicographically smallest result)
44 for (int digit = 1; digit < 10; ++digit) {
45 // Skip if digit is already used
46 if (!usedDigits[digit]) {
47 // Check if current digit violates the pattern constraint
48 if (position > 0) {
49 int previousDigit = currentNumber.charAt(position - 1) - '0';
50 char patternChar = pattern.charAt(position - 1);
51
52 // Skip if 'I' pattern requires increase but current digit is not greater
53 if (patternChar == 'I' && previousDigit >= digit) {
54 continue;
55 }
56 // Skip if 'D' pattern requires decrease but current digit is not less
57 if (patternChar == 'D' && previousDigit <= digit) {
58 continue;
59 }
60 }
61
62 // Use this digit and explore further
63 usedDigits[digit] = true;
64 currentNumber.append(digit);
65
66 // Recursive call to build next position
67 dfs(position + 1);
68
69 // Backtrack: remove digit and mark as unused
70 currentNumber.deleteCharAt(currentNumber.length() - 1);
71 usedDigits[digit] = false;
72 }
73 }
74 }
75}
76
1class Solution {
2public:
3 string smallestNumber(string pattern) {
4 // Initialize member variables
5 this->pattern = pattern;
6 result = "";
7 currentNumber = "";
8 usedDigits.assign(10, false);
9
10 // Start DFS from position 0
11 findSmallestNumber(0);
12 return result;
13 }
14
15private:
16 string result; // Stores the final answer
17 string pattern; // The input pattern of 'I' and 'D'
18 vector<bool> usedDigits; // Tracks which digits (1-9) have been used
19 string currentNumber; // Current number being constructed
20
21 void findSmallestNumber(int position) {
22 // Early termination if we already found an answer
23 if (!result.empty()) {
24 return;
25 }
26
27 // Base case: we've placed all digits (pattern length + 1 digits needed)
28 if (position == pattern.size() + 1) {
29 result = currentNumber;
30 return;
31 }
32
33 // Try each digit from 1 to 9 in ascending order (for lexicographically smallest)
34 for (int digit = 1; digit <= 9; ++digit) {
35 // Skip if digit is already used
36 if (usedDigits[digit]) {
37 continue;
38 }
39
40 // Check pattern constraints if not the first digit
41 if (position > 0) {
42 int lastDigit = currentNumber.back() - '0';
43
44 // For 'I': current digit must be greater than previous
45 if (pattern[position - 1] == 'I' && digit <= lastDigit) {
46 continue;
47 }
48
49 // For 'D': current digit must be less than previous
50 if (pattern[position - 1] == 'D' && digit >= lastDigit) {
51 continue;
52 }
53 }
54
55 // Use this digit and explore further
56 usedDigits[digit] = true;
57 currentNumber += to_string(digit);
58
59 // Recursive call for next position
60 findSmallestNumber(position + 1);
61
62 // Backtrack: restore state for next iteration
63 currentNumber.pop_back();
64 usedDigits[digit] = false;
65 }
66 }
67};
68
1/**
2 * Constructs the smallest number whose digits form the given pattern
3 * @param pattern - String containing 'I' (increase) and 'D' (decrease) characters
4 * @returns The smallest number matching the pattern
5 */
6function smallestNumber(pattern: string): string {
7 const patternLength: number = pattern.length;
8 // Array to store the result digits
9 const result: string[] = new Array(patternLength + 1).fill('');
10 // Track which digits have been used
11 const isVisited: boolean[] = new Array(patternLength + 1).fill(false);
12
13 /**
14 * Depth-first search to build the smallest valid number
15 * @param currentIndex - Current position in the pattern
16 * @param currentDigit - Current digit being considered
17 */
18 const performDFS = (currentIndex: number, currentDigit: number): void => {
19 // Base case: reached the end of pattern
20 if (currentIndex === patternLength) {
21 return;
22 }
23
24 // If current digit is already used, backtrack
25 if (isVisited[currentDigit]) {
26 isVisited[currentDigit] = false;
27
28 // Adjust search direction based on pattern requirement
29 if (pattern[currentIndex] === 'I') {
30 performDFS(currentIndex - 1, currentDigit - 1);
31 } else {
32 performDFS(currentIndex - 1, currentDigit + 1);
33 }
34 return;
35 }
36
37 // Mark current digit as used and place it in result
38 isVisited[currentDigit] = true;
39 result[currentIndex] = String(currentDigit);
40
41 if (pattern[currentIndex] === 'I') {
42 // For 'I' pattern: find next available larger digit
43 for (let nextDigit = currentDigit + 1; nextDigit <= patternLength + 1; nextDigit++) {
44 if (!isVisited[nextDigit]) {
45 performDFS(currentIndex + 1, nextDigit);
46 return;
47 }
48 }
49 // If no valid larger digit found, backtrack
50 isVisited[currentDigit] = false;
51 performDFS(currentIndex, currentDigit - 1);
52 } else {
53 // For 'D' pattern: find next available smaller digit
54 for (let nextDigit = currentDigit - 1; nextDigit > 0; nextDigit--) {
55 if (!isVisited[nextDigit]) {
56 performDFS(currentIndex + 1, nextDigit);
57 return;
58 }
59 }
60 // If no valid smaller digit found, backtrack
61 isVisited[currentDigit] = false;
62 performDFS(currentIndex, currentDigit + 1);
63 }
64 };
65
66 // Start DFS from index 0 with digit 1
67 performDFS(0, 1);
68
69 // Fill the last position with the remaining unused digit
70 for (let digit = 1; digit <= patternLength + 1; digit++) {
71 if (!isVisited[digit]) {
72 result[patternLength] = String(digit);
73 break;
74 }
75 }
76
77 // Join all digits to form the final number string
78 return result.join('');
79}
80
Time and Space Complexity
Time Complexity: O(9! × n)
where n
is the length of the pattern string.
The algorithm uses backtracking to explore all possible permutations of digits 1-9 that satisfy the pattern constraints. In the worst case:
- We try up to 9 choices for the first position
- Up to 8 choices for the second position
- Up to 7 choices for the third position, and so on
- This gives us at most
9!
permutations to explore - For each valid permutation attempt, we perform
O(n)
work to build the string of lengthn+1
- The pruning conditions (checking 'I' and 'D' constraints) help reduce the search space, but don't change the worst-case complexity
Space Complexity: O(n)
where n
is the length of the pattern string.
The space usage includes:
vis
array:O(10)
=O(1)
for tracking used digitst
list:O(n+1)
for storing the current permutation being built- Recursion call stack:
O(n+1)
maximum depth since we recurse up tolen(pattern) + 1
times ans
variable:O(n+1)
for storing the final result string
The dominant space factor is O(n)
from the recursion stack and temporary storage.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Error in Pattern Indexing
The Pitfall:
A very common mistake is incorrectly mapping between the position in the result string and the index in the pattern string. Since the result has n+1
digits for a pattern of length n
, developers often confuse:
- Which pattern character to check when placing a digit
- Whether to check
pattern[position]
orpattern[position-1]
For example, when placing the digit at position 1 in the result, you need to check pattern[0]
(not pattern[1]
) to determine the relationship with the digit at position 0.
Wrong Implementation:
# Incorrect: checking pattern[position] instead of pattern[position-1]
if position < len(pattern):
if pattern[position] == 'I' and last_digit >= digit:
continue
if pattern[position] == 'D' and last_digit <= digit:
continue
Correct Implementation:
# Correct: pattern[position-1] determines the relationship
# between digit at position-1 and position
if position > 0:
last_digit = int(current_number[-1])
if pattern[position - 1] == 'I' and last_digit >= digit:
continue
if pattern[position - 1] == 'D' and last_digit <= digit:
continue
2. Not Enforcing Strict Inequality
The Pitfall:
Using <=
or >=
comparisons instead of strict <
or >
comparisons. The pattern requires strictly increasing or decreasing relationships, not equal values.
Wrong Implementation:
# Incorrect: allows equal digits which violates uniqueness if pattern[position - 1] == 'I' and last_digit > digit: # Should be >= continue if pattern[position - 1] == 'D' and last_digit < digit: # Should be <= continue
Why This Matters:
Even though we're using each digit only once (tracked by used_digits
), the logic error above would cause us to skip valid digits. For pattern "I", if the last digit is 3, we should accept 4, 5, 6, etc., but the wrong comparison (last_digit > digit
) would incorrectly reject all digits greater than 3.
3. Forgetting Early Termination Optimization
The Pitfall: Without early termination, the algorithm continues searching even after finding the first (and lexicographically smallest) valid solution, causing unnecessary computation.
Inefficient Version:
def backtrack(position):
# Missing early termination check
if position == len(pattern) + 1:
result = ''.join(current_number)
return # This doesn't stop the entire search!
Optimized Version:
def backtrack(position):
if result: # Early termination - stop once we find first valid solution
return
if position == len(pattern) + 1:
result = ''.join(current_number)
return
4. Using Digit 0 in the Search
The Pitfall: The problem specifies using digits 1-9 only, but developers might accidentally include 0 in their search range.
Wrong Implementation:
# Incorrect: includes 0 which is not allowed
for digit in range(0, 10): # Should start from 1
if used_digits[digit]:
continue
Correct Implementation:
# Correct: only use digits 1 through 9
for digit in range(1, 10):
if used_digits[digit]:
continue
Why This Matters: Including 0 would create invalid solutions and could also lead to lexicographically incorrect answers. For example, "012" would appear smaller than "123" but contains an invalid digit 0.
Which type of traversal does breadth first search do?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
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
Want a Structured Path to Master System Design Too? Don’t Miss This!