Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 position i+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 least minLength characters before i (since each partition needs at least minLength characters)
  • There are j-1 valid partitions up to position t

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 first i characters into exactly j beautiful partitions
  • g[i][j]: Prefix sum array where g[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:

  1. Check if position i can be a partition endpoint:

    • Position i must have at least minLength characters (i >= minLength)
    • Character at position i-1 (0-indexed) must be non-prime
    • Either i = n (last position) OR character at position i must be prime (start of next partition)
  2. If valid, calculate f[i][j] for all j from 1 to k:

    f[i][j] = g[i-minLength][j-1]

    This formula works because:

    • We need the sum of all f[t][j-1] where t ranges from 0 to i-minLength
    • The prefix sum g[i-minLength][j-1] gives us exactly this sum
  3. 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 Evaluator

Example 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:

  1. "2354" + "218" + "5131"
  2. "2354" + "21851" + "31"
  3. "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, running n times
  • For each position i, we iterate through all possible partition counts from 1 to k
  • Both the update of f[i][j] and g[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 position i with exactly j 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, and n

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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which type of traversal does breadth first search do?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More