Number of Ways to Separate Numbers
HardPrompt
You wrote down many positive integers in a string called num. However, you realized that you forgot to add commas to seperate the different numbers. You remember that the list of integers was non-decreasing and that no integer had leading zeros.
Return the number of possible lists of integers that you could have written down to get the string num. Since the answer may be large, return it modulo 109 + 7.
Example 1:
Input: num = "327"
Output: 2
Explanation: You could have written down the numbers:
3, 27
327Example 2:
Input: num = "094"
Output: 0
Explanation: No numbers can have leading zeros and all numbers must be positive.Example 3:
Input: num = "0"
Output: 0
Explanation: No numbers can have leading zeros and all numbers must be positive.
Constraints:
1 <= num.length <= 3500numconsists of digits'0'through'9'.
Approaches
3 approaches with complexity analysis and trade-offs.
The most intuitive approach is to try all possible partitions of the number string num using recursion (backtracking). We can explore every possible way to split the string and, for each complete partition, check if it satisfies the given conditions: the sequence of numbers is non-decreasing and no number has a leading zero.
Algorithm
- Define a recursive function
backtrack(startIndex, prevNumStr)which returns the number of valid partitions for the suffixnum.substring(startIndex)given the previous number wasprevNumStr. - Base Case: If
startIndexreaches the end of the string, a valid partition has been found, so return 1. - Recursive Step:
- Initialize a counter
ways = 0. - Iterate
ifromstartIndexto the end of the string. - Form the
currentNumStrfromstartIndextoi. - Validation & Pruning:
- If
currentNumStrhas a leading zero (and is not just "0"), it's invalid. Since any longer string starting atstartIndexwill also have a leading zero, we can break the loop. - Compare
currentNumStrwithprevNumStr. IfcurrentNumStris smaller, this path is invalid, so wecontinueto the next iteration to form a larger number. - The comparison is done first by length, then lexicographically if lengths are equal.
- If
- If
currentNumStris valid, recursively callbacktrack(i + 1, currentNumStr)and add the result toways.
- Initialize a counter
- The initial call is
backtrack(0, "").
Walkthrough
We define a recursive helper function that explores all possible splits. This function takes the current starting index and the string representation of the previous number as parameters. It iterates through all possible endpoints for the current number, forming a substring. For each substring, it checks two conditions: validity (no leading zeros) and the non-decreasing property (the current number must be greater than or equal to the previous one). If the conditions are met, it makes a recursive call for the rest of the string. The sum of the results from these recursive calls gives the total number of ways for the current state. This method explores the entire search space of partitions, which grows exponentially with the length of the input string.
class Solution { int n; String num; int MOD = 1_000_000_007; public int numberOfCombinations(String num) { this.num = num; this.n = num.length(); if (num.charAt(0) == '0') { return 0; } // Using a 2D array for memoization would turn this into a DP solution. // Without it, it's pure backtracking. return backtrack(0, ""); } private int backtrack(int startIndex, String prevNumStr) { if (startIndex == n) { return 1; } long totalWays = 0; for (int i = startIndex; i < n; i++) { String currentNumStr = num.substring(startIndex, i + 1); if (currentNumStr.length() > 1 && currentNumStr.charAt(0) == '0') { break; // Pruning } if (!prevNumStr.isEmpty()) { if (currentNumStr.length() < prevNumStr.length()) { continue; // Pruning } if (currentNumStr.length() == prevNumStr.length() && currentNumStr.compareTo(prevNumStr) < 0) { continue; // Pruning } } totalWays = (totalWays + backtrack(i + 1, currentNumStr)) % MOD; } return (int) totalWays; }}Complexity
Time
O(2^n * n)
Space
O(n)
Trade-offs
Pros
Simple to understand and implement.
Correctly solves the problem for very small inputs.
Cons
Extremely inefficient with a time complexity that is exponential.
Will result in a 'Time Limit Exceeded' (TLE) error for the given constraints.
Recalculates solutions for the same subproblems repeatedly.
This approach improves upon brute force by using dynamic programming to avoid recomputing results for overlapping subproblems. We define a 2D DP state dp[i][j] as the number of ways to partition the prefix num[0...i-1] where the last number has length j. The transition involves summing up the ways for all valid previous partitions. The main bottleneck is the string comparison needed in each DP state transition to enforce the non-decreasing constraint. A naive substring comparison takes O(length) time, leading to an overall time complexity of O(n^3).
Algorithm
- Let
dp[i][j]be the number of ways to partition the prefixnum[0...i-1]such that the last number has lengthj. - Let
prefixSum[i][j]besum(dp[i][k])forkfrom 1 toj. - Iterate
ifrom 1 ton(prefix end) andjfrom 1 toi(last number length). - The current number is
curr = num[i-j...i-1]. - If
currhas a leading zero,dp[i][j] = 0. - Otherwise, the previous number ended at
prevEnd = i-j.- If
prevEnd == 0, this is the first number, sodp[i][j] = 1. - If
prevEnd > 0, we calculatedp[i][j]by summing up ways from valid previous partitions:- Previous number is shorter: Sum
dp[prevEnd][k]fork < j. This isprefixSum[prevEnd][min(j-1, prevEnd)]. - Previous number has same length: If
prevEnd >= j, comparenum[prevEnd-j...prevEnd-1]withcurr. If it's smaller or equal, adddp[prevEnd][j].
- Previous number is shorter: Sum
- If
- Update
prefixSum[i][j] = (prefixSum[i][j-1] + dp[i][j]). - The final answer is
prefixSum[n][n].
Walkthrough
The core idea is to build up the solution for larger prefixes from the solutions for smaller prefixes. The state dp[i][j] represents the number of ways to partition num's prefix of length i where the last number, num.substring(i-j, i), has length j. To compute dp[i][j], we look at the prefix of length i-j. The last number of any partition of this prefix, say of length k, must be less than or equal to our current number. We can optimize the summation over k by splitting the logic: when k < j, the previous number is always smaller. When k == j, we must perform a direct comparison. To make the summation part efficient, we use an auxiliary prefixSum table. prefixSum[i][j] stores the sum of dp[i][k] for k from 1 to j. This allows us to get the sum for shorter previous numbers in O(1) time. However, the comparison for same-length numbers still takes O(j) time, making the total complexity O(n^3).
class Solution { public int numberOfCombinations(String num) { int n = num.length(); int MOD = 1_000_000_007; if (num.charAt(0) == '0') { return 0; } int[][] dp = new int[n + 1][n + 1]; int[][] prefixSum = new int[n + 1][n + 1]; for (int i = 1; i <= n; i++) { for (int j = 1; j <= i; j++) { int start = i - j; if (num.charAt(start) == '0') { dp[i][j] = 0; } else { int prevEnd = start; if (prevEnd == 0) { dp[i][j] = 1; } else { int ways = 0; // Case 1: Previous number is shorter int maxPrevLen = Math.min(j - 1, prevEnd); if (maxPrevLen > 0) { ways = (ways + prefixSum[prevEnd][maxPrevLen]) % MOD; } // Case 2: Previous number has same length if (prevEnd >= j) { if (num.substring(prevEnd - j, prevEnd).compareTo(num.substring(start, i)) <= 0) { ways = (ways + dp[prevEnd][j]) % MOD; } } dp[i][j] = ways; } } prefixSum[i][j] = (prefixSum[i][j - 1] + dp[i][j]) % MOD; } } return prefixSum[n][n]; }}Complexity
Time
O(n^3)
Space
O(n^2)
Trade-offs
Pros
Much more efficient than brute-force.
Systematic approach that guarantees correctness.
Introduces the DP structure that can be further optimized.
Cons
The O(n^3) complexity is too slow for the given constraints (
n <= 3500).The repeated substring creation and comparison is inefficient.
This approach optimizes the O(n^3) DP solution by accelerating the substring comparisons. The bottleneck in the previous approach was comparing two substrings of the same length, which took O(length) time. We can reduce this to O(1) by precomputing a Longest Common Prefix (LCP) table. The lcp[i][j] table stores the length of the common prefix between the suffixes of num starting at indices i and j. With this table, any two substrings of the same length can be compared in constant time. This optimization reduces the total time complexity from O(n^3) to O(n^2), which is efficient enough to pass within the time limits.
Algorithm
- Precomputation: Compute a 2D
lcptable wherelcp[i][j]is the length of the Longest Common Prefix of suffixesnum.substring(i)andnum.substring(j). This takes O(n^2). - DP Calculation: Use the same DP state
dp[i][j]andprefixSumtable as the O(n^3) approach. - The transition logic is identical, but the comparison of same-length substrings is now optimized.
- To compare
num[start1...start1+len-1]andnum[start2...start2+len-1], use thelcptable:- Get
common = lcp[start1][start2]. - If
common >= len, strings are equal. - Otherwise, they differ at
start1+commonandstart2+common. Compare these characters.
- Get
- This comparison now takes O(1) time.
- The DP calculation, which was O(n^3), becomes O(n^2).
- The final answer is
prefixSum[n][n].
Walkthrough
The overall DP structure remains the same as the O(n^3) approach. The key improvement is the elimination of the costly O(j) string comparison inside the inner loop. We first spend O(n^2) time to build an LCP table. lcp[i][j] can be computed with a simple DP relation: lcp[i][j] = 1 + lcp[i+1][j+1] if num[i] == num[j], and 0 otherwise. We fill this by iterating i and j backwards from n-1.
Once the lcp table is ready, we can write a helper function isLessOrEqual(start1, start2, len) that performs the comparison in O(1). This function uses lcp[start1][start2] to find the first differing character and compares them. The main DP loops then proceed as before, but now each state transition takes O(1) time. The total time complexity is dominated by the O(n^2) LCP precomputation and the O(n^2) DP table filling.
class Solution { private int MOD = 1_000_000_007; private int[][] lcp; private String num; private int n; // Compares num.substring(start1, start1+len) and num.substring(start2, start2+len) private boolean isLessOrEqual(int start1, int start2, int len) { int common = lcp[start1][start2]; if (common >= len) { return true; // Equal } return num.charAt(start1 + common) < num.charAt(start2 + common); } public int numberOfCombinations(String num) { this.num = num; this.n = num.length(); if (num.charAt(0) == '0') { return 0; } // Precompute LCP array in O(n^2) lcp = new int[n + 1][n + 1]; for (int i = n - 1; i >= 0; i--) { for (int j = n - 1; j >= 0; j--) { if (num.charAt(i) == num.charAt(j)) { lcp[i][j] = 1 + lcp[i + 1][j + 1]; } } } int[][] dp = new int[n + 1][n + 1]; int[][] prefixSum = new int[n + 1][n + 1]; for (int i = 1; i <= n; i++) { for (int j = 1; j <= i; j++) { int start = i - j; if (num.charAt(start) == '0') { dp[i][j] = 0; } else { int prevEnd = start; if (prevEnd == 0) { dp[i][j] = 1; } else { int ways = 0; // Case 1: Previous number is shorter int maxPrevLen = Math.min(j - 1, prevEnd); if (maxPrevLen > 0) { ways = (ways + prefixSum[prevEnd][maxPrevLen]) % MOD; } // Case 2: Previous number has same length (O(1) comparison) if (prevEnd >= j) { if (isLessOrEqual(prevEnd - j, start, j)) { ways = (ways + dp[prevEnd][j]) % MOD; } } dp[i][j] = ways; } } prefixSum[i][j] = (prefixSum[i][j - 1] + dp[i][j]) % MOD; } } return prefixSum[n][n]; }}Complexity
Time
O(n^2)
Space
O(n^2)
Trade-offs
Pros
Highly efficient and passes for the given constraints.
Optimal solution for this problem.
Demonstrates a powerful optimization technique for string-based DP problems.
Cons
Requires more space for the LCP table.
Implementation is more complex due to the LCP precomputation step.
Solutions
Solution
class Solution {private static final int MOD = (int)1 e9 + 7;public int numberOfCombinations(String num) { int n = num.length(); int[][] lcp = new int[n + 1][n + 1]; for (int i = n - 1; i >= 0; --i) { for (int j = n - 1; j >= 0; --j) { if (num.charAt(i) == num.charAt(j)) { lcp[i][j] = 1 + lcp[i + 1][j + 1]; } } } int[][] dp = new int[n + 1][n + 1]; dp[0][0] = 1; for (int i = 1; i <= n; ++i) { for (int j = 1; j <= i; ++j) { int v = 0; if (num.charAt(i - j) != '0') { if (i - j - j >= 0) { int x = lcp[i - j][i - j - j]; if (x >= j || num.charAt(i - j + x) >= num.charAt(i - j - j + x)) { v = dp[i - j][j]; } } if (v == 0) { v = dp[i - j][Math.min(j - 1, i - j)]; } } dp[i][j] = (dp[i][j - 1] + v) % MOD; } } return dp[n][n]; }}class Solution { public: const int mod = 1e9 + 7 ; int numberOfCombinations ( string num ) { int n = num . size (); vector < vector < int >> lcp ( n + 1 , vector < int > ( n + 1 )); for ( int i = n - 1 ; i >= 0 ; -- i ) { for ( int j = n - 1 ; j >= 0 ; -- j ) { if ( num [ i ] == num [ j ]) { lcp [ i ][ j ] = 1 + lcp [ i + 1 ][ j + 1 ]; } } } auto cmp = [ & ]( int i , int j , int k ) { int x = lcp [ i ][ j ]; return x >= k || num [ i + x ] >= num [ j + x ]; }; vector < vector < int >> dp ( n + 1 , vector < int > ( n + 1 )); dp [ 0 ][ 0 ] = 1 ; for ( int i = 1 ; i <= n ; ++ i ) { for ( int j = 1 ; j <= i ; ++ j ) { int v = 0 ; if ( num [ i - j ] != '0' ) { if ( i - j - j >= 0 && cmp ( i - j , i - j - j , j )) { v = dp [ i - j ][ j ]; } else { v = dp [ i - j ][ min ( j - 1 , i - j )]; } } dp [ i ][ j ] = ( dp [ i ][ j - 1 ] + v ) % mod ; } } return dp [ n ][ n ]; } };class Solution : def numberOfCombinations ( self , num : str ) -> int : def cmp ( i , j , k ): x = lcp [ i ][ j ] return x >= k or num [ i + x ] >= num [ j + x ] mod = 10 ** 9 + 7 n = len ( num ) lcp = [[ 0 ] * ( n + 1 ) for _ in range ( n + 1 )] for i in range ( n - 1 , - 1 , - 1 ): for j in range ( n - 1 , - 1 , - 1 ): if num [ i ] == num [ j ]: lcp [ i ][ j ] = 1 + lcp [ i + 1 ][ j + 1 ] dp = [[ 0 ] * ( n + 1 ) for _ in range ( n + 1 )] dp [ 0 ][ 0 ] = 1 for i in range ( 1 , n + 1 ): for j in range ( 1 , i + 1 ): v = 0 if num [ i - j ] != '0' : if i - j - j >= 0 and cmp ( i - j , i - j - j , j ): v = dp [ i - j ][ j ] else : v = dp [ i - j ][ min ( j - 1 , i - j )] dp [ i ][ j ] = ( dp [ i ][ j - 1 ] + v ) % mod return dp [ n ][ n ]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.