Facebook Pixel

1849. Splitting a String Into Descending Consecutive Values

MediumStringBacktrackingEnumeration
Leetcode Link

Problem Description

You are given a string s that consists of only digits.

Your task is to check if you can split the string into two or more non-empty substrings such that:

  1. The numerical values of the substrings are in descending order
  2. The difference between any two adjacent substrings' numerical values is exactly 1

For example:

  • The string s = "0090089" can be split into ["0090", "089"] with numerical values [90, 89]. Since the values are in descending order (90 > 89) and their difference is 1 (90 - 89 = 1), this is a valid split.
  • The string s = "001" can be split in multiple ways like ["0", "01"], ["00", "1"], or ["0", "0", "1"], resulting in numerical values [0, 1], [0, 1], and [0, 0, 1] respectively. None of these are valid because they are not in descending order.

The function should return true if it's possible to find at least one valid way to split the string according to the rules above, or false otherwise.

Note that a substring is a contiguous sequence of characters in the string, and the numerical value of a substring like "0090" is 90 (leading zeros are ignored when converting to a number).

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 splitting a string into substrings, not nodes and edges as in graph problems.

Need to solve for kth smallest/largest?

  • No: We're not looking for the kth smallest or largest element; we're checking if a valid split exists.

Involves Linked Lists?

  • No: The problem works with strings, not linked list data structures.

Does the problem have small constraints?

  • Yes: While not explicitly stated in the problem, string splitting problems typically have manageable constraints. More importantly, we need to explore all possible ways to split the string, which suggests an exhaustive search approach.

Brute force / Backtracking?

  • Yes: We need to try all possible ways to split the string. At each position, we make a choice about where to split, then recursively check if the remaining string can be split validly. If our current choice doesn't lead to a valid solution, we backtrack and try a different split point.

Conclusion: The flowchart suggests using a backtracking approach. This makes sense because:

  1. We need to explore multiple split possibilities at each position
  2. We need to maintain state (the previous number) while exploring
  3. We need to backtrack when a split doesn't lead to a valid solution
  4. The problem has a recursive structure where each split decision affects subsequent decisions
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we look at this problem, we need to find a way to split a string into parts where each part's numerical value is exactly 1 less than the previous part. The key insight is that we don't know in advance how many digits each part should have or where to make the splits.

Think of it like trying different ways to cut a rope - at each position, we have multiple choices: we can cut here and make this the end of the current number, or we can continue and include more digits. This "try different possibilities" nature immediately suggests a backtracking approach.

The recursive structure becomes clear when we think about the problem step by step:

  1. Start from the beginning of the string
  2. Try taking 1 digit, 2 digits, 3 digits... as our first number
  3. Once we decide on the first number, the rest of the string becomes a smaller version of the same problem - we need to split it such that the first part equals our previous number minus 1
  4. If we can't successfully split the remaining string with our current choice, we backtrack and try a different length for our current number

The beauty of this approach is that we can use a parameter to track the "previous number" we split. Initially, we don't have a previous number (we can represent this as -1), so any first number is valid. After that, each subsequent number must be exactly 1 less than the previous one.

For example, with "0090089":

  • We might first try "0" as the first number (value = 0)
  • Then we need the rest "090089" to start with -1, which is impossible
  • We backtrack and try "00" (value = 0), still doesn't work
  • Eventually we try "0090" (value = 90)
  • Now we need "089" to start with 89, which it does!
  • We've found a valid split: [90, 89]

The recursion naturally handles the constraint checking (difference of 1) and explores all possibilities through backtracking when a path doesn't work out.

Learn more about Backtracking patterns.

Solution Approach

The solution implements a depth-first search (DFS) with backtracking to explore all possible ways to split the string.

Function Design: We define a recursive function dfs(i, x) where:

  • i represents the current position in the string we're processing
  • x represents the numerical value of the previously split substring (initialized to -1 to indicate no previous value)

Base Case: When i >= len(s), we've successfully processed the entire string, so we return True.

Recursive Logic:

  1. Building the current number: Starting from position i, we try different lengths for the current substring. We build the numerical value y by iterating through positions j from i onwards:

    y = y * 10 + int(s[j])

    This incrementally builds the number digit by digit.

  2. Boundary determination: The loop runs until:

    • len(s) - 1 if x < 0 (first number case) - we must leave at least one character for the second substring
    • len(s) otherwise - subsequent numbers can use all remaining characters
  3. Validation and recursion: For each potential split at position j, we check:

    • If x < 0 (this is the first number), any value of y is acceptable
    • Otherwise, we need x - y == 1 (descending by exactly 1)

    If the condition is met, we recursively call dfs(j + 1, y) to process the remaining string with y as the new previous value.

  4. Backtracking: If the recursive call returns True, we've found a valid split and propagate this success up. If not, the loop continues to try the next possible length for the current substring.

Initial Call: The solution starts with dfs(0, -1), processing from the beginning of the string with no previous value.

Example Walkthrough with "0090089":

  • Start with dfs(0, -1)
  • Try first number as "0" (y=0), recursively check rest with dfs(1, 0)
  • Eventually backtrack and try "0090" (y=90), recursively check with dfs(4, 90)
  • In the recursive call, build "089" (y=89), verify 90 - 89 = 1
  • Recursively check dfs(7, 89), which returns True (base case)
  • Valid split found: [90, 89]

The algorithm efficiently prunes invalid paths early while ensuring all valid splitting possibilities are explored through backtracking.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the string s = "1000999" to illustrate the solution approach.

Initial call: dfs(0, -1) - start at index 0 with no previous number.

First level (finding the first number):

  • Try "1" (y=1): Call dfs(1, 1)

    • Need next number to be 0
    • Try "0" (y=0): Check if 1-0=1 ✓
    • Call dfs(2, 0)
      • Need next number to be -1 (impossible for positive integers)
      • Backtrack
    • Try "00" (y=0): Check if 1-0=1 ✓
    • Call dfs(3, 0)
      • Similar issue, need -1
      • Backtrack
    • Continue trying longer substrings... all fail
    • Return False
  • Try "10" (y=10): Call dfs(2, 10)

    • Need next number to be 9
    • Try "0" (y=0): 10-0≠1 ✗
    • Try "00" (y=0): 10-0≠1 ✗
    • Try "009" (y=9): 10-9=1 ✓
    • Call dfs(5, 9)
      • Need next number to be 8
      • Try "9" (y=9): 9-9≠1 ✗
      • Try "99" (y=99): 9-99≠1 ✗
      • Return False
    • Backtrack
  • Try "100" (y=100): Call dfs(3, 100)

    • Need next number to be 99
    • Try "0" (y=0): 100-0≠1 ✗
    • Try "09" (y=9): 100-9≠1 ✗
    • Try "099" (y=99): 100-99=1 ✓
    • Call dfs(6, 99)
      • Need next number to be 98
      • Only "9" remains (y=9): 99-9≠1 ✗
      • Return False
    • Backtrack
  • Try "1000" (y=1000): Call dfs(4, 1000)

    • Need next number to be 999
    • Try "9" (y=9): 1000-9≠1 ✗
    • Try "99" (y=99): 1000-99≠1 ✗
    • Try "999" (y=999): 1000-999=1 ✓
    • Call dfs(7, 999)
      • i=7 >= len(s)=7, return True (base case)
    • Return True
  • Found valid split: [1000, 999]

The algorithm successfully finds that "1000999" can be split into [1000, 999], which are in descending order with a difference of exactly 1.

Solution Implementation

1class Solution:
2    def splitString(self, s: str) -> bool:
3        """
4        Check if string can be split into descending sequence where each value 
5        is exactly 1 less than the previous value.
6      
7        Args:
8            s: Input string containing digits
9          
10        Returns:
11            True if string can be split into valid descending sequence, False otherwise
12        """
13      
14        def dfs(start_index: int, previous_value: int) -> bool:
15            """
16            Recursively try to split the string into descending values.
17          
18            Args:
19                start_index: Current position in the string
20                previous_value: The value of the previous split (-1 if no previous value)
21              
22            Returns:
23                True if valid split is possible from current position, False otherwise
24            """
25            # Base case: reached end of string successfully
26            if start_index >= len(s):
27                return True
28          
29            current_value = 0
30            # Determine the ending index for the current split attempt
31            # If this is the first split (previous_value < 0), we can't use the entire string
32            # as a single number (need at least 2 parts)
33            end_index = len(s) - 1 if previous_value < 0 else len(s)
34          
35            # Try all possible splits starting from current position
36            for split_point in range(start_index, end_index):
37                # Build the current number digit by digit
38                current_value = current_value * 10 + int(s[split_point])
39              
40                # Check if this split is valid:
41                # - Either it's the first number (previous_value < 0)
42                # - Or it's exactly 1 less than the previous number
43                if (previous_value < 0 or previous_value - current_value == 1):
44                    # Recursively check if the rest of the string can be split
45                    if dfs(split_point + 1, current_value):
46                        return True
47          
48            return False
49      
50        # Start the DFS with index 0 and -1 as initial previous value
51        return dfs(0, -1)
52
1class Solution {
2    private char[] digits;
3  
4    /**
5     * Checks if the string can be split into a descending sequence of numbers
6     * where each number is exactly 1 less than the previous number.
7     * 
8     * @param s the input string containing only digits
9     * @return true if the string can be split into a valid descending sequence
10     */
11    public boolean splitString(String s) {
12        this.digits = s.toCharArray();
13        // Start DFS with initial position 0 and no previous number (-1 as sentinel)
14        return dfs(0, -1);
15    }
16  
17    /**
18     * Recursive helper function to check if remaining string can form valid sequence
19     * 
20     * @param startIndex current position in the string
21     * @param previousNumber the previous number in the sequence (-1 if no previous number)
22     * @return true if a valid descending sequence can be formed from current position
23     */
24    private boolean dfs(int startIndex, long previousNumber) {
25        // Base case: reached end of string successfully
26        if (startIndex >= digits.length) {
27            return true;
28        }
29      
30        long currentNumber = 0;
31        // If this is the first number, we can't use the entire string (need at least 2 parts)
32        // Otherwise, we can use all remaining digits
33        int maxEndIndex = previousNumber < 0 ? digits.length - 1 : digits.length;
34      
35        // Try all possible lengths for the current number
36        for (int endIndex = startIndex; endIndex < maxEndIndex; endIndex++) {
37            // Build the current number digit by digit
38            currentNumber = currentNumber * 10 + (digits[endIndex] - '0');
39          
40            // Check if current number is valid:
41            // - If first number (previousNumber < 0), any number is valid
42            // - Otherwise, current number must be exactly 1 less than previous
43            boolean isValidSequence = (previousNumber < 0) || (previousNumber - currentNumber == 1);
44          
45            if (isValidSequence && dfs(endIndex + 1, currentNumber)) {
46                return true;
47            }
48        }
49      
50        return false;
51    }
52}
53
1class Solution {
2public:
3    bool splitString(string s) {
4        // Lambda function for DFS traversal to check if string can be split
5        // into descending consecutive values
6        auto dfs = [&](this auto&& dfs, int startIndex, long long previousValue) -> bool {
7            // Base case: if we've processed the entire string, it's a valid split
8            if (startIndex >= s.size()) {
9                return true;
10            }
11          
12            // Initialize current number being formed
13            long long currentNumber = 0;
14          
15            // Determine the ending index for iteration
16            // If previousValue is -1 (first split), we need at least one more character for second part
17            // Otherwise, we can use all remaining characters
18            int endIndex = (previousValue < 0) ? s.size() - 1 : s.size();
19          
20            // Try all possible splits starting from current position
21            for (int i = startIndex; i < endIndex; ++i) {
22                // Build the current number digit by digit
23                currentNumber = currentNumber * 10 + (s[i] - '0');
24              
25                // Prevent overflow - if number becomes too large, stop trying
26                if (currentNumber > 1e10) {
27                    break;
28                }
29              
30                // Check if current split is valid:
31                // 1. Either it's the first number (previousValue < 0), or
32                // 2. Current number is exactly 1 less than previous number
33                bool isValidSplit = (previousValue < 0) || (previousValue - currentNumber == 1);
34              
35                // If valid split and remaining string can also be split validly
36                if (isValidSplit && dfs(i + 1, currentNumber)) {
37                    return true;
38                }
39            }
40          
41            // No valid split found
42            return false;
43        };
44      
45        // Start DFS with index 0 and previousValue -1 (indicating first number)
46        return dfs(0, -1);
47    }
48};
49
1/**
2 * Checks if a string can be split into a sequence of consecutive descending numbers
3 * @param s - The input string containing digits
4 * @returns true if the string can be split into consecutive descending numbers, false otherwise
5 */
6function splitString(s: string): boolean {
7    /**
8     * Depth-first search to recursively try different ways to split the string
9     * @param startIndex - Current position in the string to start parsing from
10     * @param previousNumber - The number formed in the previous split (-1 indicates no previous number)
11     * @returns true if a valid split is found from current position, false otherwise
12     */
13    const dfs = (startIndex: number, previousNumber: number): boolean => {
14        // Base case: if we've processed the entire string, the split is valid
15        if (startIndex >= s.length) {
16            return true;
17        }
18      
19        // Initialize current number being formed
20        let currentNumber = 0;
21      
22        // Determine the ending boundary for the loop
23        // If no previous number exists, we can use almost the entire remaining string
24        // Otherwise, we use the entire remaining string
25        const endBoundary = previousNumber < 0 ? s.length - 1 : s.length;
26      
27        // Try forming numbers of different lengths starting from current position
28        for (let currentIndex = startIndex; currentIndex < endBoundary; ++currentIndex) {
29            // Build the current number by appending the digit at current position
30            currentNumber = currentNumber * 10 + Number(s[currentIndex]);
31          
32            // Check if current number is valid (either first number or exactly 1 less than previous)
33            const isFirstNumber = previousNumber < 0;
34            const isConsecutiveDescending = previousNumber - currentNumber === 1;
35          
36            if ((isFirstNumber || isConsecutiveDescending) && dfs(currentIndex + 1, currentNumber)) {
37                return true;
38            }
39        }
40      
41        // No valid split found from this position
42        return false;
43    };
44  
45    // Start the DFS from index 0 with no previous number
46    return dfs(0, -1);
47}
48

Time and Space Complexity

Time Complexity: O(n^2) where n is the length of the string.

The algorithm uses a recursive depth-first search approach. At each recursive call starting from position i, we iterate through all possible substrings from position i to the end (or len(s) - 1 for the first call). This creates up to n possible branches at each position. In the worst case, we explore all possible ways to split the string:

  • From position 0: we try n-1 different splits
  • From position 1: we try n-2 different splits
  • And so on...

This gives us approximately (n-1) + (n-2) + ... + 1 = n(n-1)/2 operations, which simplifies to O(n^2).

Space Complexity: O(n) where n is the length of the string.

The space complexity is determined by the maximum depth of the recursion stack. In the worst case, we can have a valid split where each subsequent number consists of a single digit (when the string allows for consecutive decreasing numbers by 1). This would create a recursion depth equal to the length of the string n. Therefore, the space complexity is O(n) due to the recursive call stack.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Integer Overflow with Large Numbers

The Problem: The most critical pitfall in this solution is potential integer overflow. When building current_value by repeatedly multiplying by 10 and adding digits, the value can exceed Python's integer limits in other languages or cause performance issues with very large numbers. For example, a string like "99999999999999999999" (20 nines) would create a number with 20 digits.

Why It Happens:

current_value = current_value * 10 + int(s[split_point])

This line accumulates digits without any bounds checking. While Python handles arbitrary precision integers, this can still cause:

  • Memory issues with extremely large numbers
  • Performance degradation
  • In other languages (Java, C++), integer overflow leading to incorrect results

Solution: Add an early termination check to prevent building unnecessarily large numbers:

def dfs(start_index: int, previous_value: int) -> bool:
    if start_index >= len(s):
        return True
  
    current_value = 0
    end_index = len(s) - 1 if previous_value < 0 else len(s)
  
    for split_point in range(start_index, end_index):
        current_value = current_value * 10 + int(s[split_point])
      
        # Early termination: if current_value is already too small, 
        # adding more digits will only make it larger (and further from previous_value - 1)
        if previous_value >= 0 and current_value > previous_value:
            break
      
        if (previous_value < 0 or previous_value - current_value == 1):
            if dfs(split_point + 1, current_value):
                return True
  
    return False

2. Handling Leading Zeros Incorrectly

The Problem: The current solution doesn't explicitly handle the semantic difference between "01" (value: 1) and "1" (value: 1). While they have the same numerical value, splitting decisions might be affected when considering different substring lengths.

Example Issue: For string "10009", you might split it as ["1", "000", "9"] expecting values [1, 0, 9], but this wouldn't form a valid descending sequence. However, ["100", "09"] with values [100, 9] also wouldn't work. The algorithm handles this correctly by converting to integers, but it's not immediately obvious.

Solution: Add explicit documentation or validation for leading zero cases:

def dfs(start_index: int, previous_value: int) -> bool:
    if start_index >= len(s):
        return True
  
    # Special case: if we're starting with '0', we need to be careful
    # Multiple leading zeros collapse to the same value
    if s[start_index] == '0':
        # Try using just this zero as a number
        if previous_value < 0 or previous_value == 1:
            if dfs(start_index + 1, 0):
                return True
        # Continue trying longer substrings with leading zeros
  
    current_value = 0
    end_index = len(s) - 1 if previous_value < 0 else len(s)
  
    for split_point in range(start_index, end_index):
        current_value = current_value * 10 + int(s[split_point])
      
        if previous_value >= 0 and current_value > previous_value:
            break
          
        if (previous_value < 0 or previous_value - current_value == 1):
            if dfs(split_point + 1, current_value):
                return True
  
    return False

3. Edge Case: Single Character String

The Problem: The constraint requires splitting into "two or more" substrings. For a single character string like "5", the algorithm correctly returns False because of the end_index = len(s) - 1 logic when previous_value < 0, but this logic might not be immediately clear.

Solution: Add an explicit check for clarity:

def splitString(self, s: str) -> bool:
    # Edge case: need at least 2 characters to split into 2+ parts
    if len(s) < 2:
        return False
  
    def dfs(start_index: int, previous_value: int) -> bool:
        # ... rest of the implementation
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?


Recommended Readings

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

Load More