2478. Number of Beautiful Partitions
Problem Description
You are given a string s
containing only digits from '1' to '9', along with two integers k
and minLength
. Your task is to find the number of ways to partition the string into exactly k
non-overlapping substrings where each partition is considered "beautiful".
A partition is beautiful if it satisfies all of these conditions:
- The string is divided into exactly
k
substrings - Each substring has a length of at least
minLength
characters - Each substring must start with a prime digit ('2', '3', '5', or '7')
- Each substring must end with a non-prime digit ('1', '4', '6', '8', or '9')
For example, if we have a substring "241", it would be valid because:
- It starts with '2' (prime)
- It ends with '1' (non-prime)
The problem asks you to count all possible beautiful partitions of the given string s
. Since the answer can be very large, return the result modulo 10^9 + 7
.
Key points to note:
- The substrings must be contiguous (you cannot rearrange characters)
- The substrings cannot overlap
- All
k
substrings must satisfy the prime/non-prime conditions - Each substring must have at least
minLength
characters
If it's impossible to create a beautiful partition (for instance, if the first character is non-prime or the last character is prime), the answer would be 0.
Intuition
To solve this problem, we need to think about how to count valid ways to partition the string. The key insight is that this is a classic dynamic programming problem where we build up solutions for smaller subproblems.
Let's think step by step. We need to partition string s
into exactly k
parts, where each part satisfies certain conditions. At any position i
in the string, we need to decide: "Can this position be the end of a partition?"
For position i
to be a valid ending position of a partition, it must satisfy:
- The character at position
i
must be non-prime (since partitions end with non-prime digits) - Either
i
is the last position in the string, OR the next character at positioni+1
must be prime (since the next partition must start with a prime digit)
This naturally leads us to define f[i][j]
as the number of ways to partition the first i
characters into exactly j
valid partitions.
For each valid ending position i
and partition count j
, we need to consider where the previous partition ended. The previous partition could have ended at any position t
where:
t
is at leastminLength
characters beforei
(since each partition needs at leastminLength
characters)- There are
j-1
valid partitions up to positiont
This gives us the recurrence relation: f[i][j] = sum of all f[t][j-1]
where t
ranges from 0
to i-minLength
.
However, computing this sum repeatedly would be inefficient. Here's where the optimization comes in - we can use a prefix sum array g[i][j]
to store the cumulative sum of f[0][j]
through f[i][j]
. This way, instead of summing up multiple values each time, we can directly access g[i-minLength][j-1]
to get the sum we need.
The base case is f[0][0] = 1
(one way to partition zero characters into zero parts), and we build up from there until we reach f[n][k]
, which gives us the final answer.
Learn more about Dynamic Programming patterns.
Solution Approach
The implementation uses dynamic programming with prefix sum optimization. Let's walk through the solution step by step:
Initial Validation: First, we check if a beautiful partition is even possible. If the first character is not prime or the last character is prime, we immediately return 0 since no valid partition exists.
State Definition: We define two 2D arrays:
f[i][j]
: Number of ways to partition the firsti
characters into exactlyj
beautiful partitionsg[i][j]
: Prefix sum array whereg[i][j] = f[0][j] + f[1][j] + ... + f[i][j]
Both arrays have dimensions (n+1) × (k+1)
where n
is the length of string s
.
Base Case:
Initialize f[0][0] = g[0][0] = 1
, representing one way to partition zero characters into zero parts.
State Transition:
For each position i
from 1 to n
, we:
-
Check if position
i
can be a partition endpoint:- Position
i
must have at leastminLength
characters (i >= minLength
) - Character at position
i-1
(0-indexed) must be non-prime - Either
i = n
(last position) OR character at positioni
must be prime (start of next partition)
- Position
-
If valid, calculate
f[i][j]
for allj
from 1 tok
:f[i][j] = g[i-minLength][j-1]
This formula works because:
- We need the sum of all
f[t][j-1]
wheret
ranges from 0 toi-minLength
- The prefix sum
g[i-minLength][j-1]
gives us exactly this sum
- We need the sum of all
-
Update the prefix sum array:
g[i][j] = (g[i-1][j] + f[i][j]) % mod
This maintains the cumulative sum for future calculations.
Time Complexity:
O(n × k)
where n
is the length of the string and k
is the number of partitions.
Space Complexity:
O(n × k)
for the two 2D arrays.
Final Answer:
Return f[n][k]
, which represents the number of ways to partition all n
characters into exactly k
beautiful partitions.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Example: s = "23542185131"
, k = 3
, minLength = 2
Step 1: Initial Validation
- First character '2' is prime ✓
- Last character '1' is non-prime ✓
- Beautiful partition is possible!
Step 2: Identify Valid Partition Endpoints We need to find positions where a partition can end:
- Character at that position must be non-prime
- Next character (if exists) must be prime
Looking at our string with 0-based indexing:
Index: 0 1 2 3 4 5 6 7 8 9 10 String: 2 3 5 4 2 1 8 5 1 3 1 Type: P P P N P N N P N P N
(P = Prime, N = Non-prime)
Valid endpoints (1-indexed for DP array):
- Position 4: s[3]='4' is non-prime, s[4]='2' is prime ✓
- Position 6: s[5]='1' is non-prime, s[6]='8' is non-prime ✗
- Position 7: s[6]='8' is non-prime, s[7]='5' is prime ✓
- Position 9: s[8]='1' is non-prime, s[9]='3' is prime ✓
- Position 11: s[10]='1' is non-prime, end of string ✓
Step 3: Build DP Table
Initialize:
f[0][0] = 1
(base case)g[0][0] = 1
(prefix sum)
For each valid endpoint i
and partition count j
:
At position 4 (can form 1st partition "2354"):
- Length = 4 ≥ minLength(2) ✓
f[4][1] = g[4-2][0] = g[2][0] = 1
- This means: 1 way to partition first 4 chars into 1 partition
At position 7 (can form 2nd partition):
- For j=1:
f[7][1] = g[7-2][0] = g[5][0] = 1
(partition: "2354218") - For j=2:
f[7][2] = g[7-2][1] = g[5][1] = 1
(partitions: "2354" + "218")
At position 9 (can form 3rd partition):
- For j=1:
f[9][1] = g[9-2][0] = g[7][0] = 1
- For j=2:
f[9][2] = g[9-2][1] = g[7][1] = 2
(either "2354" + "21851" or "2354218" + "51") - For j=3:
f[9][3] = g[9-2][2] = g[7][2] = 1
(partitions: "2354" + "218" + "51")
At position 11 (final position):
- For j=1:
f[11][1] = g[11-2][0] = g[9][0] = 1
- For j=2:
f[11][2] = g[11-2][1] = g[9][1] = 3
- For j=3:
f[11][3] = g[11-2][2] = g[9][2] = 3
Step 4: Final Answer
f[11][3] = 3
The three valid partitions are:
- "2354" + "218" + "5131"
- "2354" + "21851" + "31"
- "2354218" + "51" + "31"
Each partition satisfies:
- Starts with prime digit
- Ends with non-prime digit
- Has at least minLength characters
- Total of k=3 partitions
The answer is 3 (modulo 10^9 + 7).
Solution Implementation
1class Solution:
2 def beautifulPartitions(self, s: str, k: int, minLength: int) -> int:
3 # Define prime digits for checking partition boundaries
4 PRIME_DIGITS = '2357'
5 MOD = 10**9 + 7
6
7 # Early validation: first char must be prime, last char must be non-prime
8 if s[0] not in PRIME_DIGITS or s[-1] in PRIME_DIGITS:
9 return 0
10
11 n = len(s)
12
13 # dp[i][j] = number of ways to partition s[0:i] into j beautiful partitions
14 # where position i is a valid partition end
15 dp = [[0] * (k + 1) for _ in range(n + 1)]
16
17 # prefix_sum[i][j] = cumulative sum of dp values up to position i with j partitions
18 # Used for optimization to avoid recalculating sums
19 prefix_sum = [[0] * (k + 1) for _ in range(n + 1)]
20
21 # Base case: empty string with 0 partitions has 1 way
22 dp[0][0] = prefix_sum[0][0] = 1
23
24 # Iterate through each position in the string
25 for i in range(1, n + 1):
26 current_char = s[i - 1]
27
28 # Check if position i can be a valid partition endpoint:
29 # 1. Must have at least minLength characters
30 # 2. Current character must be non-prime
31 # 3. Either at the end of string OR next character is prime
32 is_valid_end = (i >= minLength and
33 current_char not in PRIME_DIGITS and
34 (i == n or s[i] in PRIME_DIGITS))
35
36 if is_valid_end:
37 # For each number of partitions j
38 for j in range(1, k + 1):
39 # Sum all valid ways to form j-1 partitions ending at least
40 # minLength positions before current position
41 dp[i][j] = prefix_sum[i - minLength][j - 1]
42
43 # Update prefix sums for current position
44 for j in range(k + 1):
45 prefix_sum[i][j] = (prefix_sum[i - 1][j] + dp[i][j]) % MOD
46
47 # Return number of ways to partition entire string into exactly k parts
48 return dp[n][k]
49
1class Solution {
2 /**
3 * Counts the number of ways to partition a string into k beautiful substrings.
4 * A beautiful substring must start with a prime digit and end with a non-prime digit.
5 *
6 * @param s the input string containing digits
7 * @param k the number of partitions required
8 * @param minLength the minimum length of each partition
9 * @return the number of beautiful partitions modulo 10^9 + 7
10 */
11 public int beautifulPartitions(String s, int k, int minLength) {
12 int stringLength = s.length();
13
14 // Check if the string can form beautiful partitions
15 // First character must be prime (start of first partition)
16 // Last character must be non-prime (end of last partition)
17 if (!isPrimeDigit(s.charAt(0)) || isPrimeDigit(s.charAt(stringLength - 1))) {
18 return 0;
19 }
20
21 // dp[i][j] represents the number of ways to partition s[0...i-1] into j beautiful parts
22 int[][] dp = new int[stringLength + 1][k + 1];
23
24 // prefixSum[i][j] represents the cumulative sum of dp[0][j] to dp[i][j]
25 // Used for optimization to avoid recalculating sums
26 int[][] prefixSum = new int[stringLength + 1][k + 1];
27
28 // Base case: empty string with 0 partitions has 1 way
29 dp[0][0] = 1;
30 prefixSum[0][0] = 1;
31
32 final int MOD = (int) 1e9 + 7;
33
34 // Iterate through each position in the string
35 for (int i = 1; i <= stringLength; ++i) {
36 // Check if position i can be the end of a beautiful partition
37 // Conditions:
38 // 1. Current position is at least minLength from the start
39 // 2. Character at position i-1 is non-prime (end of partition)
40 // 3. Either we're at the end of string or next character is prime (start of next partition)
41 if (i >= minLength &&
42 !isPrimeDigit(s.charAt(i - 1)) &&
43 (i == stringLength || isPrimeDigit(s.charAt(i)))) {
44
45 // Update dp values for all possible partition counts
46 for (int j = 1; j <= k; ++j) {
47 // Use prefix sum to get all valid starting positions
48 // that are at least minLength away
49 dp[i][j] = prefixSum[i - minLength][j - 1];
50 }
51 }
52
53 // Update prefix sums for current position
54 for (int j = 0; j <= k; ++j) {
55 prefixSum[i][j] = (prefixSum[i - 1][j] + dp[i][j]) % MOD;
56 }
57 }
58
59 // Return the number of ways to partition the entire string into k parts
60 return dp[stringLength][k];
61 }
62
63 /**
64 * Checks if a character represents a prime digit (2, 3, 5, or 7).
65 *
66 * @param c the character to check
67 * @return true if the character is a prime digit, false otherwise
68 */
69 private boolean isPrimeDigit(char c) {
70 return c == '2' || c == '3' || c == '5' || c == '7';
71 }
72}
73
1class Solution {
2public:
3 int beautifulPartitions(string s, int k, int minLength) {
4 int n = s.size();
5
6 // Lambda function to check if a character is a prime digit
7 auto isPrime = [](char c) {
8 return c == '2' || c == '3' || c == '5' || c == '7';
9 };
10
11 // Base case: string must start with prime and end with non-prime
12 if (!isPrime(s[0]) || isPrime(s[n - 1])) {
13 return 0;
14 }
15
16 // dp[i][j] = number of ways to partition s[0...i-1] into j beautiful partitions
17 vector<vector<int>> dp(n + 1, vector<int>(k + 1, 0));
18
19 // prefixSum[i][j] = prefix sum of dp values up to position i with j partitions
20 // Used for optimization to avoid recalculating sums
21 vector<vector<int>> prefixSum(n + 1, vector<int>(k + 1, 0));
22
23 // Initialize: empty string with 0 partitions = 1 way
24 dp[0][0] = 1;
25 prefixSum[0][0] = 1;
26
27 const int MOD = 1e9 + 7;
28
29 // Iterate through each position in the string
30 for (int i = 1; i <= n; ++i) {
31 // Check if position i can be the end of a beautiful partition:
32 // 1. Length constraint: i >= minLength
33 // 2. Current char (s[i-1]) must be non-prime
34 // 3. Either we're at the end OR next char (s[i]) must be prime
35 if (i >= minLength && !isPrime(s[i - 1]) && (i == n || isPrime(s[i]))) {
36 // Try to form j partitions ending at position i
37 for (int j = 1; j <= k; ++j) {
38 // Use prefix sum to get all valid starting positions
39 // that are at least minLength away from current position
40 dp[i][j] = prefixSum[i - minLength][j - 1];
41 }
42 }
43
44 // Update prefix sums for current position
45 for (int j = 0; j <= k; ++j) {
46 prefixSum[i][j] = (prefixSum[i - 1][j] + dp[i][j]) % MOD;
47 }
48 }
49
50 // Return number of ways to partition entire string into k parts
51 return dp[n][k];
52 }
53};
54
1/**
2 * Counts the number of ways to partition a string into k beautiful partitions
3 * A beautiful partition starts with a prime digit and ends with a non-prime digit
4 * @param s - The input string containing only digits
5 * @param k - The number of partitions required
6 * @param minLength - The minimum length of each partition
7 * @returns The number of beautiful partitions modulo 10^9 + 7
8 */
9function beautifulPartitions(s: string, k: number, minLength: number): number {
10 /**
11 * Checks if a character represents a prime digit (2, 3, 5, or 7)
12 * @param char - The character to check
13 * @returns true if the character is a prime digit, false otherwise
14 */
15 const isPrimeDigit = (char: string): boolean => {
16 return char === '2' || char === '3' || char === '5' || char === '7';
17 };
18
19 const stringLength: number = s.length;
20
21 // Early return: invalid if string doesn't start with prime or ends with prime
22 if (!isPrimeDigit(s[0]) || isPrimeDigit(s[stringLength - 1])) {
23 return 0;
24 }
25
26 // dp[i][j] represents the number of ways to partition s[0...i-1] into j beautiful partitions
27 // where position i is a valid partition end
28 const dp: number[][] = new Array(stringLength + 1)
29 .fill(0)
30 .map(() => new Array(k + 1).fill(0));
31
32 // prefixSum[i][j] represents the cumulative sum of dp values up to position i with j partitions
33 // Used for optimization to avoid recalculating sums
34 const prefixSum: number[][] = new Array(stringLength + 1)
35 .fill(0)
36 .map(() => new Array(k + 1).fill(0));
37
38 const MOD: number = 1e9 + 7;
39
40 // Base case: empty string with 0 partitions has 1 way
41 dp[0][0] = 1;
42 prefixSum[0][0] = 1;
43
44 // Iterate through each position in the string
45 for (let i = 1; i <= stringLength; ++i) {
46 // Check if position i can be a valid partition endpoint
47 // Valid endpoint conditions:
48 // 1. Current position has at least minLength characters from start
49 // 2. Character at position i-1 (0-indexed) is not prime
50 // 3. Either we're at the end of string OR next character is prime
51 const isValidPartitionEnd: boolean =
52 i >= minLength &&
53 !isPrimeDigit(s[i - 1]) &&
54 (i === stringLength || isPrimeDigit(s[i]));
55
56 if (isValidPartitionEnd) {
57 // Update dp values for all possible partition counts
58 for (let j = 1; j <= k; ++j) {
59 // Number of ways equals the prefix sum at position (i - minLength)
60 // with (j - 1) partitions, ensuring minimum length requirement
61 dp[i][j] = prefixSum[i - minLength][j - 1];
62 }
63 }
64
65 // Update prefix sums for current position
66 for (let j = 0; j <= k; ++j) {
67 prefixSum[i][j] = (prefixSum[i - 1][j] + dp[i][j]) % MOD;
68 }
69 }
70
71 // Return the number of ways to partition entire string into k partitions
72 return dp[stringLength][k];
73}
74
Time and Space Complexity
Time Complexity: O(n × k)
The algorithm uses nested loops where:
- The outer loop iterates through each character in string
s
, runningn
times - For each position
i
, we iterate through all possible partition counts from1
tok
- Both the update of
f[i][j]
andg[i][j]
involve constant-time operations
Therefore, the total time complexity is O(n × k)
, where n
is the length of string s
and k
is the number of partitions.
Space Complexity: O(n × k)
The algorithm allocates:
- A 2D array
f
of size(n + 1) × (k + 1)
to store the number of valid partitions ending at positioni
with exactlyj
partitions - A 2D array
g
of size(n + 1) × (k + 1)
to store the prefix sum of valid partitions - A few constant space variables like
primes
,mod
, andn
The dominant space usage comes from the two 2D arrays, resulting in a space complexity of O(n × k)
, where n
is the length of string s
and k
is the number of partitions.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Errors in Index Validation
One of the most common mistakes is incorrectly checking whether a position can be a valid partition endpoint. Developers often confuse 0-based and 1-based indexing when validating partition boundaries.
Pitfall Example:
# WRONG: Checking the wrong character for the next position is_valid_end = (i >= minLength and s[i] not in PRIME_DIGITS and # Wrong! Should be s[i-1] (i == n or s[i+1] in PRIME_DIGITS)) # Wrong! Should be s[i]
Solution:
Remember that when using 1-based DP indexing (where i
ranges from 1 to n), the corresponding character in the 0-based string is s[i-1]
. The next character would be s[i]
, not s[i+1]
.
2. Forgetting to Handle Edge Cases in Validation
Another frequent error is not properly handling the boundary condition when checking if the next character is prime.
Pitfall Example:
# WRONG: May cause index out of bounds is_valid_end = (i >= minLength and s[i-1] not in PRIME_DIGITS and s[i] in PRIME_DIGITS) # Fails when i == n
Solution: Always check if you're at the string's end before accessing the next character:
is_valid_end = (i >= minLength and s[i-1] not in PRIME_DIGITS and (i == n or s[i] in PRIME_DIGITS))
3. Incorrect Prefix Sum Range
A subtle but critical mistake is using the wrong range when calculating the sum of previous DP states.
Pitfall Example:
# WRONG: Including positions that are too close dp[i][j] = prefix_sum[i - 1][j - 1] # Should ensure minLength gap
Solution:
The correct approach ensures that the previous partition ended at least minLength
positions before:
dp[i][j] = prefix_sum[i - minLength][j - 1]
This guarantees that the current partition has at least minLength
characters.
4. Not Applying Modulo Consistently
Forgetting to apply the modulo operation can lead to integer overflow in languages with fixed integer sizes, or incorrect results even in Python.
Pitfall Example:
# WRONG: Missing modulo operation prefix_sum[i][j] = prefix_sum[i - 1][j] + dp[i][j]
Solution: Always apply modulo when updating cumulative values:
prefix_sum[i][j] = (prefix_sum[i - 1][j] + dp[i][j]) % MOD
5. Misunderstanding the Problem Constraints
Developers sometimes misinterpret what constitutes a "beautiful" partition, particularly regarding which digits are prime.
Pitfall Example:
# WRONG: Including '1' as prime or forgetting '2' PRIME_DIGITS = '357' # Missing '2' # or PRIME_DIGITS = '12357' # '1' is not prime
Solution: Carefully verify the prime digits according to mathematical definition:
PRIME_DIGITS = '2357' # Exactly these four single-digit primes
Which type of traversal does breadth first search do?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!