Number of Beautiful Partitions
HardPrompt
You are given a string s that consists of the digits '1' to '9' and two integers k and minLength.
A partition of s is called beautiful if:
sis partitioned intoknon-intersecting substrings.- Each substring has a length of at least
minLength. - Each substring starts with a prime digit and ends with a non-prime digit. Prime digits are
'2','3','5', and'7', and the rest of the digits are non-prime.
Return the number of beautiful partitions of s. Since the answer may be very large, return it modulo 109 + 7.
A substring is a contiguous sequence of characters within a string.
Example 1:
Input: s = "23542185131", k = 3, minLength = 2
Output: 3
Explanation: There exists three ways to create a beautiful partition:
"2354 | 218 | 5131"
"2354 | 21851 | 31"
"2354218 | 51 | 31"Example 2:
Input: s = "23542185131", k = 3, minLength = 3
Output: 1
Explanation: There exists one way to create a beautiful partition: "2354 | 218 | 5131".Example 3:
Input: s = "3312958", k = 3, minLength = 1
Output: 1
Explanation: There exists one way to create a beautiful partition: "331 | 29 | 58".
Constraints:
1 <= k, minLength <= s.length <= 1000sconsists of the digits'1'to'9'.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach uses recursion with memoization, which is a top-down dynamic programming technique. We define a function that solves the problem for a subproblem and use a memoization table to store the results of subproblems to avoid redundant calculations. The state of our recursion is defined by (i, j), representing the starting index of the string suffix to be partitioned and the number of partitions remaining.
Algorithm
- Define a recursive function
solve(i, j)that computes the number of beautiful partitions for the suffix of the stringsstarting at indexi, usingjpartitions. - Base Cases:
- If
jis 0, it means we have used allkpartitions. A valid solution is found only if we have also consumed the entire string (i == n). So, return1ifi == n, otherwise0. - If
i >= n(we've run out of string) butj > 0(still need to make partitions), it's impossible. Return0.
- If
- Recursive Step:
- For a partition to start at index
i,s.charAt(i)must be a prime digit. If not, no beautiful partition can start here, so return0. - If
s.charAt(i)is prime, iterate through all possible end positionspfor the current partition. The end positionpmust satisfy:- The partition length
p - i + 1is at leastminLength. - The character
s.charAt(p)is a non-prime digit.
- The partition length
- For each valid end position
p, we have formed one beautiful partitions.substring(i, p + 1). We then need to partition the rest of the strings.substring(p + 1)intoj - 1partitions. The number of ways to do this is given by the recursive callsolve(p + 1, j - 1). - Sum up the results from these recursive calls, taking the result modulo
10^9 + 7.
- For a partition to start at index
- Memoization:
- To avoid recomputing results for the same state
(i, j), use a 2D arraymemo[n][k+1]to store the computed values. Before computingsolve(i, j), check if it's already in the memoization table.
- To avoid recomputing results for the same state
Walkthrough
The core idea is to define a function, say solve(i, j), which calculates the number of ways to partition the suffix s[i:] into j beautiful partitions. The final answer is solve(0, k).
To compute solve(i, j), we try to form the first of the j partitions. This partition must start at index i and end at some index p. For this partition s[i...p] to be beautiful, it must satisfy three conditions: its length must be at least minLength, s[i] must be prime, and s[p] must be non-prime. If s[i] is not prime, solve(i, j) is 0. Otherwise, we iterate through all valid p and for each, we recursively call solve(p + 1, j - 1) to find the number of ways for the rest of the string. The sum of results from these calls gives solve(i, j).
Without memoization, this would lead to an exponential number of calls. By storing the result of each solve(i, j) call in a 2D array memo, we ensure that each subproblem is solved only once.
class Solution { int n; int k; int minLength; String s; int MOD = 1_000_000_007; Integer[][] memo; private boolean isPrime(char c) { return c == '2' || c == '3' || c == '5' || c == '7'; } private int solve(int i, int j) { if (j == 0) { return i == n ? 1 : 0; } if (i >= n) { return 0; } if (memo[i][j] != null) { return memo[i][j]; } // The first character of a partition must be prime. if (!isPrime(s.charAt(i))) { return memo[i][j] = 0; } long count = 0; // Iterate through possible end points 'p' for the current partition. for (int p = i + minLength - 1; p < n; p++) { // The last character of a partition must be non-prime. if (!isPrime(s.charAt(p))) { count = (count + solve(p + 1, j - 1)) % MOD; } } return memo[i][j] = (int) count; } public int beautifulPartitions(String s, int k, int minLength) { this.n = s.length(); this.k = k; this.minLength = minLength; this.s = s; this.memo = new Integer[n + 1][k + 1]; if (!isPrime(s.charAt(0)) || isPrime(s.charAt(n - 1))) { return 0; } return solve(0, k); }}Complexity
Time
O(n * k * n). There are `n * k` states, and each state computation involves a loop that can run up to `n` times.
Space
O(n * k) for the memoization table and recursion stack depth.
Trade-offs
Pros
Relatively straightforward to formulate from the problem's recursive nature.
Correctly solves the problem for smaller constraints.
Cons
The time complexity of
O(n^2 * k)is too slow for the given constraints (n, k <= 1000) and will result in a Time Limit Exceeded (TLE) error.
This approach uses bottom-up dynamic programming. We build the solution iteratively, starting from smaller subproblems. The key insight is to optimize the transition step. The naive DP transition involves a summation over previous results, leading to O(n^2 * k) complexity. By pre-calculating suffix sums for each column of the DP table, we can reduce the transition to O(1), thus improving the overall time complexity.
Algorithm
- Define a 2D DP array
dp[i][j], wheredp[i][j]stores the number of beautiful partitions of the suffixs[i...n-1]intojparts. - Base Case:
dp[n][0] = 1. This signifies that an empty suffix (s[n...n-1]) can be partitioned into 0 parts in one way. - Iteration: We fill the DP table column by column, for
jfrom 1 tok. For eachj, we iterateifromn-1down to0. - Transition: The recurrence relation is
dp[i][j] = sum(dp[p+1][j-1])over all valid split pointsp. A split pointpis valid ifs[i]is prime,s[p]is non-prime, and the partition length is at leastminLength. - Optimization: The summation
sum(dp[p+1][j-1])wheres[p]is non-prime can be computed efficiently. For each columnj, we first precompute asuffixSumarray based on the values from columnj-1.suffixSum[q]will storesum_{p=q}^{n-1, s[p] is non-prime} dp[p+1][j-1]. - This
suffixSumarray is computed inO(n)by iteratingpfromn-1down to0. The relation issuffixSum[p] = suffixSum[p+1], and ifs[p]is non-prime, we adddp[p+1][j-1]. - With the
suffixSumarray,dp[i][j]can be found inO(1):dp[i][j] = suffixSum[i + minLength - 1](ifs[i]is prime and other conditions met). - Final Answer: The result is
dp[0][k].
Walkthrough
Let dp[i][j] be the number of ways to partition the suffix s[i...n-1] into j beautiful partitions. Our goal is to find dp[0][k]. The base case is dp[n][0] = 1, as there's one way to partition an empty string into zero parts.
We iterate from j = 1 to k. For each j, we want to compute dp[i][j] for all i. A partition starting at i must have s[i] as a prime digit. If so, dp[i][j] is the sum of dp[p+1][j-1] for all valid end points p. A point p is a valid end if p >= i + minLength - 1 and s[p] is non-prime.
The crucial optimization is to avoid re-calculating this sum for every (i, j). For a fixed j, the sum sum_{p=q}^{n-1, s[p] is non-prime} dp[p+1][j-1] can be pre-calculated for all q in O(n) time. Let's call this suffixSum[q]. We can compute it by iterating q from n-1 down to 0. Then, dp[i][j] is simply suffixSum[i + minLength - 1]. This reduces the complexity of each state's computation from O(n) to O(1).
class Solution { private boolean isPrime(char c) { return c == '2' || c == '3' || c == '5' || c == '7'; } public int beautifulPartitions(String s, int k, int minLength) { int n = s.length(); int MOD = 1_000_000_007; if (!isPrime(s.charAt(0)) || isPrime(s.charAt(n - 1))) { return 0; } long[][] dp = new long[n + 1][k + 1]; dp[n][0] = 1; for (int j = 1; j <= k; j++) { long[] suffixSum = new long[n + 1]; for (int p = n - 1; p >= 0; p--) { suffixSum[p] = suffixSum[p + 1]; if (!isPrime(s.charAt(p))) { suffixSum[p] = (suffixSum[p] + dp[p + 1][j - 1]) % MOD; } } for (int i = n - 1; i >= 0; i--) { if (isPrime(s.charAt(i)) && i + minLength <= n) { dp[i][j] = suffixSum[i + minLength - 1]; } } } return (int) dp[0][k]; }}Complexity
Time
O(n * k). The outer loop runs `k` times. Inside, we compute the suffix sum array in `O(n)` and then fill the current DP column in `O(n)`. Total time is `k * (O(n) + O(n)) = O(n*k)`.
Space
O(n * k) to store the entire DP table.
Trade-offs
Pros
Efficient
O(n*k)time complexity, which is fast enough for the given constraints.Systematic bottom-up approach avoids recursion overhead.
Cons
The space complexity of
O(n*k)might be large, although it fits within typical memory limits for the given constraints.
This is the most optimized approach, improving upon the previous bottom-up DP solution by reducing its space complexity. We notice that the computation for the number of partitions j only depends on the results for j-1 partitions. This dependency allows us to discard older columns of the DP table that are no longer needed, significantly saving space.
Algorithm
- This approach builds upon the previous bottom-up DP method.
- Observe that to compute the values for
dp[...][j](thej-th column), we only need the values fromdp[...][j-1](thej-1-th column). - Instead of a full
O(n*k)DP table, we can use just two 1D arrays:dpof sizen+1for the current columnj, andprev_dpof sizen+1for the previous columnj-1. - Initialization:
prev_dpis initialized forj=0.prev_dp[n] = 1, and all other elements are 0. - Iteration: Loop
jfrom 1 tok.- Inside the loop, create a new array
dpfor the current columnj. - Compute the
suffixSumarray based onprev_dp(values from columnj-1), just like in the previous approach. - Fill the
dparray using thesuffixSumarray. - After computing all values for column
j, updateprev_dpto bedpfor the next iteration.
- Inside the loop, create a new array
- Final Answer: After the loops complete, the answer is
prev_dp[0].
Walkthrough
The logic is identical to the standard bottom-up DP approach, but we optimize memory usage. Instead of a 2D dp table of size (n+1) x (k+1), we maintain only two 1D arrays: prev_dp to hold the results for j-1 partitions, and dp to compute the results for j partitions.
In each iteration of the main loop over j, we treat prev_dp as read-only to compute the suffixSum and then the new dp array. Once the dp array for the current j is fully computed, it becomes the prev_dp for the next iteration j+1. This cycle continues until j=k. The final answer is stored in prev_dp[0] after the loop finishes.
class Solution { private boolean isPrime(char c) { return c == '2' || c == '3' || c == '5' || c == '7'; } public int beautifulPartitions(String s, int k, int minLength) { int n = s.length(); int MOD = 1_000_000_007; if (!isPrime(s.charAt(0)) || isPrime(s.charAt(n - 1))) { return 0; } long[] prev_dp = new long[n + 1]; prev_dp[n] = 1; for (int j = 1; j <= k; j++) { long[] dp = new long[n + 1]; long[] suffixSum = new long[n + 1]; for (int p = n - 1; p >= 0; p--) { suffixSum[p] = suffixSum[p + 1]; if (!isPrime(s.charAt(p))) { suffixSum[p] = (suffixSum[p] + prev_dp[p + 1]) % MOD; } } for (int i = n - 1; i >= 0; i--) { if (isPrime(s.charAt(i)) && i + minLength <= n) { dp[i] = suffixSum[i + minLength - 1]; } } prev_dp = dp; } return (int) prev_dp[0]; }}Complexity
Time
O(n * k). The time complexity remains the same as the previous approach, as the number of computations is unchanged.
Space
O(n). We use a few arrays of size `n+1`, but their usage is not dependent on `k`.
Trade-offs
Pros
Optimal time complexity of
O(n*k).Optimal space complexity of
O(n).
Cons
The logic can be slightly more complex to manage due to the array swapping/updating.
Solutions
Solution
class Solution { private static final int MOD = ( int ) 1 e9 + 7 ; public int beautifulPartitions ( String s , int k , int minLength ) { int n = s . length (); if (! prime ( s . charAt ( 0 )) || prime ( s . charAt ( n - 1 ))) { return 0 ; } int [][] f = new int [ n + 1 ][ k + 1 ]; int [][] g = new int [ n + 1 ][ k + 1 ]; f [ 0 ][ 0 ] = 1 ; g [ 0 ][ 0 ] = 1 ; for ( int i = 1 ; i <= n ; ++ i ) { if ( i >= minLength && ! prime ( s . charAt ( i - 1 )) && ( i == n || prime ( s . charAt ( i )))) { for ( int j = 1 ; j <= k ; ++ j ) { f [ i ][ j ] = g [ i - minLength ][ j - 1 ]; } } for ( int j = 0 ; j <= k ; ++ j ) { g [ i ][ j ] = ( g [ i - 1 ][ j ] + f [ i ][ j ]) % MOD ; } } return f [ n ][ k ]; } private boolean prime ( char c ) { return c == '2' || c == '3' || c == '5' || c == '7' ; } }class Solution { public: const int mod = 1e9 + 7 ; int beautifulPartitions ( string s , int k , int minLength ) { int n = s . size (); auto prime = []( char c ) { return c == '2' || c == '3' || c == '5' || c == '7' ; }; if ( ! prime ( s [ 0 ]) || prime ( s [ n - 1 ])) return 0 ; vector < vector < int >> f ( n + 1 , vector < int > ( k + 1 )); vector < vector < int >> g ( n + 1 , vector < int > ( k + 1 )); f [ 0 ][ 0 ] = g [ 0 ][ 0 ] = 1 ; for ( int i = 1 ; i <= n ; ++ i ) { if ( i >= minLength && ! prime ( s [ i - 1 ]) && ( i == n || prime ( s [ i ]))) { for ( int j = 1 ; j <= k ; ++ j ) { f [ i ][ j ] = g [ i - minLength ][ j - 1 ]; } } for ( int j = 0 ; j <= k ; ++ j ) { g [ i ][ j ] = ( g [ i - 1 ][ j ] + f [ i ][ j ]) % mod ; } } return f [ n ][ k ]; } };class Solution : def beautifulPartitions ( self , s : str , k : int , minLength : int ) -> int : primes = '2357' if s [ 0 ] not in primes or s [ - 1 ] in primes : return 0 mod = 10 ** 9 + 7 n = len ( s ) f = [[ 0 ] * ( k + 1 ) for _ in range ( n + 1 )] g = [[ 0 ] * ( k + 1 ) for _ in range ( n + 1 )] f [ 0 ][ 0 ] = g [ 0 ][ 0 ] = 1 for i , c in enumerate ( s , 1 ): if i >= minLength and c not in primes and ( i == n or s [ i ] in primes ): for j in range ( 1 , k + 1 ): f [ i ][ j ] = g [ i - minLength ][ j - 1 ] for j in range ( k + 1 ): g [ i ][ j ] = ( g [ i - 1 ][ j ] + f [ i ][ j ]) % mod return f [ n ][ k ]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.