1849. Splitting a String Into Descending Consecutive Values - Explanation
Description
Split the digit string s into at least two nonempty, contiguous parts that together use every character. Interpret each part as a non-negative decimal integer; leading zeros are allowed. Return true if every value is exactly one less than the preceding value, and false if no such split exists.
Example 1:
Input: s = "3210"
Output: trueExplanation: Split into 3, 2, 1, and 0. Each value is exactly one smaller than the previous value.
Example 2:
Input: s = "00100"
Output: trueExplanation: The split 001 | 00 has values 1 and 0. Leading zeros are allowed, and the difference is exactly one.
Example 3:
Input: s = "00"
Output: falseExplanation: The only split is 0 | 0. Equal adjacent values do not differ by one, so the answer is false.
Constraints:
1 <= s.length <= 20scontains only decimal digits.
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Backtracking / Recursion - Used to explore all possible ways to split the string and validate sequences
- String to Integer Conversion - Building numbers digit by digit from string characters
- Stack-based Iteration - Alternative to recursion for simulating DFS without the call stack
- Pruning Techniques - Early termination when the current number exceeds the previous value
1. Backtracking
Intuition
The problem asks us to split a string into at least two parts where each consecutive part represents a number that is exactly one less than the previous. Since we need to explore all possible ways to split the string, backtracking is a natural choice. We try every possible split point, build numbers digit by digit, and check if they form a valid descending consecutive sequence.
Algorithm
- Create a helper function
isValidthat checks if a list of numbers forms a descending consecutive sequence (each element is exactly one less than the previous) and contains at least two elements. - Use depth-first search starting at index
0with an empty list of splits. - At each position, try extending the current number by including more digits (from current index to end of string).
- For each potential number, add it to the splits list and recursively process the remaining string.
- If we reach the end of the string, validate the sequence. If valid, return
true. - Backtrack by removing the last added number and try the next split point.
class Solution:
def splitString(self, s: str) -> bool:
n = len(s)
def isValid(splits):
for i in range(1, len(splits)):
if splits[i] != splits[i - 1] - 1:
return False
return len(splits) > 1
def dfs(i, splits):
if i == n:
return isValid(splits)
num = 0
for j in range(i, n):
num = num * 10 + int(s[j])
splits.append(num)
if dfs(j + 1, splits):
return True
splits.pop()
return False
return dfs(0, [])Time & Space Complexity
- Time complexity:
- Space complexity:
2. Recursion - I
Intuition
Instead of collecting all splits and validating at the end, we can optimize by passing the previous number directly. Once we fix the first number, we only need to find subsequent numbers that are exactly one less. This eliminates the need to store all splits and allows early termination when we find a valid sequence.
Algorithm
- Iterate through all possible first numbers by trying each prefix of the string (excluding the last character to ensure at least two parts).
- For each first number, call
dfswith the remaining string and the first number as the previous value. - In
dfs, try to form numbers from the current position. If a number equalsprev - 1, recursively check the rest. - If we reach the end of the string during recursion, we found a valid split.
- Return
trueif any starting number leads to a valid sequence.
class Solution:
def splitString(self, s: str) -> bool:
def dfs(index, prev):
if index == len(s):
return True
num = 0
for j in range(index, len(s)):
num = num * 10 + int(s[j])
if num + 1 == prev and dfs(j + 1, num):
return True
return False
val = 0
for i in range(len(s) - 1):
val = val * 10 + int(s[i])
if dfs(i + 1, val):
return True
return FalseTime & Space Complexity
- Time complexity:
- Space complexity: for recursion stack.
3. Recursion - II
Intuition
Building on the previous approach, we add an important pruning optimization. Since we need descending consecutive values, once the current number we are building becomes greater than or equal to the previous number, there is no point continuing to add more digits. This early termination significantly reduces the search space.
Algorithm
- Same setup as Recursion I: iterate through possible first numbers.
- In
dfs, build numbers digit by digit from the current position. - If the current number equals
prev - 1, recursively check the remaining string. - Key optimization: if the current number becomes greater than or equal to
prev, break out of the loop early since adding more digits will only make it larger. - Return
truewhen the end of string is reached with a valid sequence.
class Solution:
def splitString(self, s: str) -> bool:
def dfs(index, prev):
if index == len(s):
return True
num = 0
for j in range(index, len(s)):
num = num * 10 + int(s[j])
if num + 1 == prev and dfs(j + 1, num):
return True
if num >= prev:
break
return False
val = 0
for i in range(len(s) - 1):
val = val * 10 + int(s[i])
if dfs(i + 1, val):
return True
return FalseTime & Space Complexity
- Time complexity:
- Space complexity: for recursion stack.
4. Stack
Intuition
Instead of using recursion with the call stack, we can simulate the same process with an explicit stack. This converts the recursive solution into an iterative one, which can be useful for avoiding stack overflow on very deep recursions and makes the state transitions more explicit.
Algorithm
- For each possible first number, push the state (next index, first number value) onto the stack.
- While the stack is not empty, pop a state containing the current index and previous value.
- Build numbers starting from the current index. When a number equals
prev - 1:- If this number uses all remaining characters, return
true. - Otherwise, push the new state (next index, current number) onto the stack.
- If this number uses all remaining characters, return
- Apply the same pruning: if the number grows to be greater than or equal to
prev, stop building. - If all possibilities are exhausted without finding a valid sequence, return
false.
class Solution:
def splitString(self, s: str) -> bool:
n = len(s)
stack = []
val = 0
for i in range(n - 1):
val = val * 10 + int(s[i])
stack.append((i + 1, val))
while stack:
index, prev = stack.pop()
num = 0
for j in range(index, n):
num = num * 10 + int(s[j])
if num + 1 == prev:
if j + 1 == n:
return True
stack.append((j + 1, num))
elif num >= prev:
break
return FalseTime & Space Complexity
- Time complexity:
- Space complexity:
Common Pitfalls
Integer Overflow with Large Numbers
The string can be up to 20 characters long, meaning numbers can exceed 64-bit integer limits. Using int or even standard long without considering overflow can produce incorrect comparisons. Use unsigned long long or language-specific big integer handling, and be cautious when building numbers digit by digit.
Not Requiring At Least Two Parts
The problem requires splitting into at least two parts. A common mistake is returning true when the entire string forms a single number. Always ensure the loop for the first number excludes the last character, forcing at least one more part to exist.
Missing the Pruning Optimization
Without early termination when the current number exceeds or equals the previous number, the solution explores many unnecessary branches. Since we need strictly descending consecutive values, once num >= prev, no valid sequence can be formed by adding more digits, so breaking out of the loop is essential for efficiency.
Sign in to join the discussion