Facebook Pixel

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:

  1. The string num can only use digits '1' through '9', and each digit can be used at most once
  2. When pattern[i] == 'I', the digit at position i in num must be less than the digit at position i + 1 (increasing relationship)
  3. When pattern[i] == 'D', the digit at position i in num must be greater than the digit at position i + 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Trying digits in ascending order (1, 2, 3...) at each position for lexicographic minimality
  2. Checking if the current digit satisfies the pattern constraint with the previous digit
  3. If we can't place any valid digit at a position, we backtrack and try the next available digit at the previous position
  4. 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 used
  • t: A temporary list to build the current string candidate
  • ans: 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:

  1. 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.

  2. 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)
    • 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

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":

  1. Start at position 0, try digit 1
  2. Move to position 1, need a digit > 1 (due to 'I'), try 2
  3. Move to position 2, need a digit < 2 (due to 'D'), can't use 1 (already used)
  4. Backtrack to position 1, try 3
  5. Move to position 2, need digit < 3, try 2
  6. 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 Evaluator

Example 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 length n+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 digits
  • t list: O(n+1) for storing the current permutation being built
  • Recursion call stack: O(n+1) maximum depth since we recurse up to len(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] or pattern[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.

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

Which type of traversal does breadth first search do?


Recommended Readings

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

Load More