LeetCode 1416 - Restore The Array
The problem gives us a string s that represents several positive integers concatenated together without spaces. Original
Difficulty: 🔴 Hard
Topics: String, Dynamic Programming
Solution
Problem Understanding
The problem gives us a string s that represents several positive integers concatenated together without spaces. Originally, the array contained integers in the range [1, k], and none of the integers had leading zeros.
Our task is to determine how many different valid arrays could have produced the string s.
For example, if:
s = "1317"
k = 2000
then several valid splits are possible:
[1317]
[131,7]
[13,17]
[1,317]
[13,1,7]
[1,31,7]
[1,3,17]
[1,3,1,7]
The answer is therefore 8.
The key challenge is deciding where to split the string. Every substring must satisfy two conditions:
- It must not contain leading zeros.
- Its numeric value must be between
1andk.
The constraints are extremely important:
s.lengthcan be as large as10^5kcan be up to10^9
A brute-force solution that tries all possible splits would be exponentially slow because every position could potentially either split or not split.
The input guarantees that s itself does not begin with a leading zero, but substrings formed during splitting may still start with '0', and those substrings are invalid.
Several edge cases are important:
- Strings containing zeros in difficult positions, such as
"1000" - Very small
k, which invalidates many substrings - Large inputs where recursion without memoization would time out
- Cases where no valid split exists
- Cases where every digit can be split independently
Because the answer can become extremely large, we must return it modulo:
10^9 + 7
Approaches
Brute Force Approach
The brute-force strategy is to recursively try every possible split.
At each index, we consider all substrings starting from that position:
s[i:j]
If the substring is valid, meaning:
- it does not start with
'0' - its integer value is at most
k
then we recursively solve the remaining suffix.
This approach is correct because it explores every possible partition of the string.
However, it is far too slow. In the worst case, every position can branch into multiple recursive calls, leading to exponential complexity. With 10^5 characters, this becomes completely infeasible.
Key Insight
The problem has overlapping subproblems.
If we define:
dp[i] = number of ways to restore the substring s[i:]
then many recursive paths repeatedly compute the same suffix results.
This naturally suggests dynamic programming.
At each index i, we try extending the current number digit by digit until:
- the number exceeds
k - or we reach the end of the string
For every valid number formed, we add:
dp[next_position]
to the answer.
Because k <= 10^9, any valid number can contain at most 10 digits. This is extremely important because it limits the amount of work performed at each position.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n) | O(n) | Tries every possible split recursively |
| Optimal Dynamic Programming | O(n * log10(k)) | O(n) | Reuses overlapping subproblems efficiently |
Algorithm Walkthrough
- Create a DP array where
dp[i]represents the number of valid ways to restore the substring starting at indexi. - Initialize the base case:
dp[n] = 1
where n is the length of the string.
This means there is exactly one way to restore an empty suffix, by choosing nothing. 3. Process the string from right to left.
We move backward because each state depends on future states.
4. At each index i, first check whether:
s[i] == '0'
If so, no valid number can start here because leading zeros are forbidden.
Therefore:
dp[i] = 0
- Otherwise, begin constructing numbers digit by digit.
Start with:
current_number = 0
Then extend the substring one character at a time. 6. For every extension:
current_number = current_number * 10 + digit
- If
current_number > k, stop immediately.
Any longer substring will only become larger. 8. Otherwise, add the number of ways from the next position:
dp[i] += dp[j + 1]
- Apply modulo arithmetic after each addition.
- After processing all indices, return:
dp[0]
Why it works
The algorithm works because every valid array restoration corresponds to choosing a valid first number and then restoring the remaining suffix independently.
For each index i, the DP state counts all valid decompositions of s[i:]. By trying every valid next number and summing the solutions of the remaining suffixes, we enumerate every legal partition exactly once.
The recurrence is:
$dp[i] = \sum dp[j+1]$
where each substring s[i:j] forms a valid integer in [1, k].
Python Solution
class Solution:
def numberOfArrays(self, s: str, k: int) -> int:
MOD = 10**9 + 7
n = len(s)
dp = [0] * (n + 1)
dp[n] = 1
for i in range(n - 1, -1, -1):
if s[i] == '0':
continue
current_number = 0
for j in range(i, n):
current_number = current_number * 10 + int(s[j])
if current_number > k:
break
dp[i] = (dp[i] + dp[j + 1]) % MOD
return dp[0]
The implementation directly follows the dynamic programming strategy described earlier.
The array dp stores the number of ways to restore each suffix. The base case dp[n] = 1 represents the empty suffix.
The outer loop iterates backward through the string because each position depends on future states.
When a digit '0' is encountered, the loop skips processing because numbers cannot begin with leading zeros.
The inner loop gradually constructs numbers by extending the substring one digit at a time. As soon as the number exceeds k, the loop stops because any further extension would only increase the value.
For every valid number, the algorithm adds the number of restoration ways from the next index.
Modulo arithmetic prevents integer overflow and satisfies the problem requirements.
Go Solution
func numberOfArrays(s string, k int) int {
const MOD int = 1_000_000_007
n := len(s)
dp := make([]int, n+1)
dp[n] = 1
for i := n - 1; i >= 0; i-- {
if s[i] == '0' {
continue
}
currentNumber := 0
for j := i; j < n; j++ {
currentNumber = currentNumber*10 + int(s[j]-'0')
if currentNumber > k {
break
}
dp[i] = (dp[i] + dp[j+1]) % MOD
}
}
return dp[0]
}
The Go implementation mirrors the Python logic closely.
Because Go does not have Python's arbitrary precision integers by default, we rely on the fact that k <= 10^9, so int is sufficient.
Character-to-digit conversion is performed using:
int(s[j] - '0')
The DP slice is initialized with size n + 1, and the base case dp[n] = 1 is identical to the Python solution.
Worked Examples
Example 1
s = "1000"
k = 10000
We compute DP from right to left.
| Index | Substring | Valid Choices | dp Value |
|---|---|---|---|
| 4 | "" | base case | 1 |
| 3 | "0" | none | 0 |
| 2 | "00" | none | 0 |
| 1 | "000" | none | 0 |
| 0 | "1000" | 1000 | 1 |
Detailed processing at index 0:
| Substring | Value | Valid? | Added |
|---|---|---|---|
| "1" | 1 | yes | dp[1] = 0 |
| "10" | 10 | yes | dp[2] = 0 |
| "100" | 100 | yes | dp[3] = 0 |
| "1000" | 1000 | yes | dp[4] = 1 |
Final answer:
dp[0] = 1
Example 2
s = "1000"
k = 10
Valid numbers cannot exceed 10.
At index 0:
| Substring | Value | Valid? |
|---|---|---|
| "1" | 1 | yes |
| "10" | 10 | yes |
| "100" | 100 | no |
But both "1" and "10" lead to suffixes beginning with '0', which are invalid.
Therefore:
dp[0] = 0
Example 3
s = "1317"
k = 2000
DP table construction:
| Index | Possible Numbers | Contributions | dp[i] |
|---|---|---|---|
| 4 | base case | 1 | 1 |
| 3 | 7 | dp[4] | 1 |
| 2 | 1, 17 | dp[3] + dp[4] | 2 |
| 1 | 3, 31, 317 | dp[2] + dp[3] + dp[4] | 4 |
| 0 | 1, 13, 131, 1317 | dp[1] + dp[2] + dp[3] + dp[4] | 8 |
Final answer:
8
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n * log10(k)) | Each index explores at most the digit length of k |
| Space | O(n) | DP array stores one value per position |
The important optimization is that we never examine arbitrarily long substrings.
Since k <= 10^9, a valid number can contain at most 10 digits. Therefore, each index performs only a small constant amount of work, making the overall runtime effectively linear in the size of the string.
Test Cases
sol = Solution()
assert sol.numberOfArrays("1000", 10000) == 1 # single valid number
assert sol.numberOfArrays("1000", 10) == 0 # impossible because of zeros
assert sol.numberOfArrays("1317", 2000) == 8 # provided example
assert sol.numberOfArrays("1", 1) == 1 # smallest valid input
assert sol.numberOfArrays("9", 8) == 0 # single digit exceeds k
assert sol.numberOfArrays("123", 1000) == 4 # many valid partitions
assert sol.numberOfArrays("123", 12) == 2 # restricted by k
assert sol.numberOfArrays("111111", 111111) > 0 # many overlapping states
assert sol.numberOfArrays("101", 1000) == 1 # only [101] works
assert sol.numberOfArrays("101", 10) == 1 # [10,1]
assert sol.numberOfArrays("1001", 10000) == 1 # only full number valid
assert sol.numberOfArrays("1001", 10) == 0 # impossible split
assert sol.numberOfArrays("9999999999", 1000000000) >= 0 # large values
assert sol.numberOfArrays("123456789", 9) == 1 # only single digits allowed
Test Case Summary
| Test | Why |
|---|---|
"1000", 10000 |
Valid full-number restoration |
"1000", 10 |
No valid partition exists |
"1317", 2000 |
Multiple valid decompositions |
"1", 1 |
Minimum input size |
"9", 8 |
Single number exceeds limit |
"123", 1000 |
Many valid splits |
"123", 12 |
Restrictive k blocks longer substrings |
"111111", 111111 |
Heavy overlapping subproblems |
"101", 1000 |
Internal zero handling |
"101", 10 |
Mixed valid and invalid partitions |
"1001", 10000 |
Entire string must remain intact |
"1001", 10 |
Leading zero issues invalidate splits |
"9999999999", 1000000000 |
Large numeric values |
"123456789", 9 |
Only single-digit partitions valid |
Edge Cases
One important edge case involves substrings beginning with '0'. Since leading zeros are forbidden, any partition that starts with zero must immediately be rejected. A common bug is accidentally allowing substrings like "01" or "000". The implementation handles this by skipping processing whenever s[i] == '0'.
Another critical edge case occurs when numbers exceed k. Once the current constructed number becomes larger than k, continuing to extend the substring is pointless because additional digits only increase the value further. The implementation immediately breaks out of the inner loop, which is both correct and efficient.
A third important edge case is when no valid restoration exists at all. For example:
s = "1000"
k = 10
Every possible split eventually produces a segment with leading zeros or a number larger than k. The DP naturally propagates zeros upward, resulting in a final answer of 0.
Large inputs are also significant. Since s.length can reach 10^5, recursive brute-force solutions without memoization would cause exponential runtime or stack overflow. The bottom-up DP approach avoids recursion entirely and runs efficiently even at maximum input size.