Facebook Pixel

3388. Count Beautiful Splits in an Array

Problem Description

You are given an array nums. Your task is to find the number of ways to split this array into three non-empty subarrays that form a "beautiful" split.

A split of array nums into three subarrays nums1, nums2, and nums3 is considered beautiful if both of the following conditions are met:

  1. The three subarrays are consecutive parts of the original array. When concatenated in order (nums1 + nums2 + nums3), they reconstruct the original array nums.

  2. At least one of these relationships holds:

    • nums1 is a prefix of nums2 (meaning nums2 starts with all the elements of nums1)
    • nums2 is a prefix of nums3 (meaning nums3 starts with all the elements of nums2)

For example, if nums = [1, 2, 1, 2, 3], one beautiful split could be:

  • nums1 = [1, 2]
  • nums2 = [1, 2, 3]
  • nums3 = [] (empty in this case would be invalid as all subarrays must be non-empty)

Here nums1 is a prefix of nums2 since nums2 starts with [1, 2].

The goal is to count all possible ways to create such beautiful splits of the given array.

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

Intuition

To solve this problem, we need to efficiently check if one subarray is a prefix of another. The key insight is that checking if array A is a prefix of array B is equivalent to checking if the first len(A) elements of B match exactly with A.

For any split at positions i and j, we get three subarrays:

  • nums1 = nums[0:i] with length i
  • nums2 = nums[i:j] with length j - i
  • nums3 = nums[j:n] with length n - j

For nums1 to be a prefix of nums2, we need:

  • Length condition: i <= j - i (nums1 cannot be longer than nums2)
  • Content condition: nums[0:i] must match nums[i:i+i] exactly

For nums2 to be a prefix of nums3, we need:

  • Length condition: j - i <= n - j (nums2 cannot be longer than nums3)
  • Content condition: nums[i:j] must match nums[j:j+(j-i)] exactly

The challenge is efficiently checking these prefix relationships. If we naively compare elements for each possible split, it would be too slow.

This is where the Longest Common Prefix (LCP) technique comes in. By precomputing LCP[i][j] - the length of the longest common prefix starting at positions i and j - we can check prefix relationships in constant time. If LCP[i][j] >= k, then the subarray starting at position i with length k matches the subarray starting at position j with the same length k.

The LCP array can be built using dynamic programming: if nums[i] == nums[j], then LCP[i][j] = LCP[i+1][j+1] + 1. We build this bottom-up by iterating in reverse order.

With the LCP array ready, we can enumerate all possible splits and check the beautiful conditions in constant time using our precomputed values.

Learn more about Dynamic Programming patterns.

Solution Approach

The solution implements the LCP (Longest Common Prefix) technique combined with enumeration to count beautiful splits efficiently.

Step 1: Build the LCP Table

We create a 2D array lcp where lcp[i][j] represents the length of the longest common prefix between subarrays starting at index i and index j.

lcp = [[0] * (n + 1) for _ in range(n + 1)]
for i in range(n - 1, -1, -1):
    for j in range(n - 1, i - 1, -1):
        if nums[i] == nums[j]:
            lcp[i][j] = lcp[i + 1][j + 1] + 1

We iterate in reverse order (from n-1 to 0 for i, and from n-1 to i for j) because:

  • If nums[i] == nums[j], then the common prefix starting at positions i and j extends by 1 plus whatever common prefix exists at positions i+1 and j+1
  • This creates a bottom-up dynamic programming approach where we build longer prefixes from shorter ones

Step 2: Enumerate All Possible Splits

We enumerate all valid split positions where:

  • i represents the end position of the first subarray (exclusive), so nums1 = nums[0:i]
  • j represents the end position of the second subarray (exclusive), so nums2 = nums[i:j] and nums3 = nums[j:n]
for i in range(1, n - 1):
    for j in range(i + 1, n):

The ranges ensure that all three subarrays are non-empty: i >= 1, j >= i + 1, and j < n.

Step 3: Check Beautiful Conditions

For each split, we check if it's beautiful by verifying either:

  1. Condition A: nums1 is a prefix of nums2

    • Length check: i <= j - i (nums1's length shouldn't exceed nums2's length)
    • Content check: lcp[0][i] >= i (the common prefix between positions 0 and i should be at least i elements long)
  2. Condition B: nums2 is a prefix of nums3

    • Length check: j - i <= n - j (nums2's length shouldn't exceed nums3's length)
    • Content check: lcp[i][j] >= j - i (the common prefix between positions i and j should be at least j-i elements long)
a = i <= j - i and lcp[0][i] >= i
b = j - i <= n - j and lcp[i][j] >= j - i
ans += int(a or b)

If either condition is satisfied, we increment our answer counter.

The time complexity is O(n²) for building the LCP table and O(n²) for enumeration, resulting in overall O(n²) complexity. The space complexity is O(n²) for storing the LCP table.

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 the solution with nums = [1, 1, 2, 1].

Step 1: Build the LCP Table

We create a 4×4 LCP table where lcp[i][j] stores the longest common prefix length starting at indices i and j.

Building the table from bottom-right to top-left:

  • At positions (3,3): Both point to index 3 (value 1), lcp[3][3] = 1
  • At positions (2,3): nums[2]=2, nums[3]=1, different values, so lcp[2][3] = 0
  • At positions (2,2): Both point to index 2 (value 2), lcp[2][2] = 1
  • At positions (1,3): nums[1]=1, nums[3]=1, same! So lcp[1][3] = lcp[2][4] + 1 = 0 + 1 = 1
  • At positions (1,2): nums[1]=1, nums[2]=2, different values, so lcp[1][2] = 0
  • At positions (1,1): Both point to index 1 (value 1), lcp[1][1] = lcp[2][2] + 1 = 1 + 1 = 2
  • At positions (0,3): nums[0]=1, nums[3]=1, same! So lcp[0][3] = lcp[1][4] + 1 = 0 + 1 = 1
  • At positions (0,2): nums[0]=1, nums[2]=2, different values, so lcp[0][2] = 0
  • At positions (0,1): nums[0]=1, nums[1]=1, same! So lcp[0][1] = lcp[1][2] + 1 = 0 + 1 = 1
  • At positions (0,0): Both point to index 0, lcp[0][0] = lcp[1][1] + 1 = 2 + 1 = 3

Final LCP table (relevant portions):

    0  1  2  3
0 [ 3  1  0  1 ]
1 [ -  2  0  1 ]
2 [ -  -  1  0 ]
3 [ -  -  -  1 ]

Step 2: Enumerate All Possible Splits

We check all valid (i, j) pairs where 1 ≤ i < 3 and i+1 ≤ j < 4:

Split 1: i=1, j=2

  • nums1 = [1] (length 1)
  • nums2 = [1] (length 1)
  • nums3 = [2, 1] (length 2)

Check Condition A: Is nums1 a prefix of nums2?

  • Length check: 1 ≤ 1 ✓
  • Content check: lcp[0][1] = 1 >= 1
  • Condition A is TRUE

Check Condition B: Is nums2 a prefix of nums3?

  • Length check: 1 ≤ 2 ✓
  • Content check: lcp[1][2] = 0 >= 1
  • Condition B is FALSE

Result: Beautiful (Condition A is true) ✓

Split 2: i=1, j=3

  • nums1 = [1] (length 1)
  • nums2 = [1, 2] (length 2)
  • nums3 = [1] (length 1)

Check Condition A: Is nums1 a prefix of nums2?

  • Length check: 1 ≤ 2 ✓
  • Content check: lcp[0][1] = 1 >= 1
  • Condition A is TRUE

Check Condition B: Is nums2 a prefix of nums3?

  • Length check: 2 ≤ 1 ✗
  • Condition B is FALSE (fails length check)

Result: Beautiful (Condition A is true) ✓

Split 3: i=2, j=3

  • nums1 = [1, 1] (length 2)
  • nums2 = [2] (length 1)
  • nums3 = [1] (length 1)

Check Condition A: Is nums1 a prefix of nums2?

  • Length check: 2 ≤ 1 ✗
  • Condition A is FALSE (fails length check)

Check Condition B: Is nums2 a prefix of nums3?

  • Length check: 1 ≤ 1 ✓
  • Content check: lcp[2][3] = 0 >= 1
  • Condition B is FALSE

Result: Not beautiful ✗

Final Answer: 2 (Splits at (1,2) and (1,3) are beautiful)

Solution Implementation

1class Solution:
2    def beautifulSplits(self, nums: List[int]) -> int:
3        """
4        Count the number of beautiful splits of the array.
5        A split at positions i and j creates three parts: nums[0:i], nums[i:j], nums[j:n].
6        A split is beautiful if either:
7        - The first part is a prefix of the second part, OR
8        - The second part is a prefix of the third part
9        """
10        n = len(nums)
11      
12        # Build LCP (Longest Common Prefix) table
13        # lcp[i][j] represents the length of the longest common prefix
14        # starting at index i and index j in the original array
15        lcp = [[0] * (n + 1) for _ in range(n + 1)]
16      
17        # Fill the LCP table using dynamic programming
18        # Work backwards to utilize previously computed values
19        for i in range(n - 1, -1, -1):
20            for j in range(n - 1, i - 1, -1):
21                if nums[i] == nums[j]:
22                    # If current elements match, extend the common prefix length
23                    lcp[i][j] = lcp[i + 1][j + 1] + 1
24      
25        # Count beautiful splits
26        beautiful_count = 0
27      
28        # Try all possible split positions
29        # i: end of first part (exclusive), start of second part
30        # j: end of second part (exclusive), start of third part
31        for i in range(1, n - 1):  # First part must have at least 1 element, third part must exist
32            for j in range(i + 1, n):  # Second part must have at least 1 element
33                # Check if first part is a prefix of second part
34                # Conditions: length of first part <= length of second part
35                #            AND common prefix length >= length of first part
36                first_is_prefix = (i <= j - i) and (lcp[0][i] >= i)
37              
38                # Check if second part is a prefix of third part
39                # Conditions: length of second part <= length of third part
40                #            AND common prefix length >= length of second part
41                second_is_prefix = (j - i <= n - j) and (lcp[i][j] >= j - i)
42              
43                # A split is beautiful if either condition is satisfied
44                if first_is_prefix or second_is_prefix:
45                    beautiful_count += 1
46      
47        return beautiful_count
48
1class Solution {
2    public int beautifulSplits(int[] nums) {
3        int n = nums.length;
4      
5        // Build LCP (Longest Common Prefix) array
6        // lcp[i][j] represents the length of common prefix starting at index i and j
7        int[][] lcp = new int[n + 1][n + 1];
8      
9        // Fill LCP array from bottom-right to top-left
10        // This ensures we can use previously computed values
11        for (int i = n - 1; i >= 0; i--) {
12            for (int j = n - 1; j > i; j--) {
13                if (nums[i] == nums[j]) {
14                    // If current elements match, extend the common prefix length
15                    lcp[i][j] = lcp[i + 1][j + 1] + 1;
16                }
17                // If elements don't match, lcp[i][j] remains 0 (default value)
18            }
19        }
20      
21        int beautifulSplitCount = 0;
22      
23        // Try all possible split positions
24        // i represents the end of first part (exclusive), start of second part
25        // j represents the end of second part (exclusive), start of third part
26        for (int i = 1; i < n - 1; i++) {
27            for (int j = i + 1; j < n; j++) {
28                // Check if first part is a prefix of second part
29                // Conditions: length of first part <= length of second part
30                //            AND common prefix length >= length of first part
31                boolean isFirstPartPrefixOfSecond = (i <= j - i) && (lcp[0][i] >= i);
32              
33                // Check if second part is a prefix of third part
34                // Conditions: length of second part <= length of third part
35                //            AND common prefix length >= length of second part
36                boolean isSecondPartPrefixOfThird = (j - i <= n - j) && (lcp[i][j] >= j - i);
37              
38                // Count this split if either condition is satisfied
39                if (isFirstPartPrefixOfSecond || isSecondPartPrefixOfThird) {
40                    beautifulSplitCount++;
41                }
42            }
43        }
44      
45        return beautifulSplitCount;
46    }
47}
48
1class Solution {
2public:
3    int beautifulSplits(vector<int>& nums) {
4        int arraySize = nums.size();
5      
6        // Build LCP (Longest Common Prefix) table
7        // lcp[i][j] stores the length of common prefix starting at positions i and j
8        vector<vector<int>> longestCommonPrefix(arraySize + 1, vector<int>(arraySize + 1, 0));
9      
10        // Fill the LCP table using dynamic programming
11        // Work backwards to use previously computed values
12        for (int startPos1 = arraySize - 1; startPos1 >= 0; startPos1--) {
13            for (int startPos2 = arraySize - 1; startPos2 > startPos1; startPos2--) {
14                if (nums[startPos1] == nums[startPos2]) {
15                    // If elements match, extend the common prefix length
16                    longestCommonPrefix[startPos1][startPos2] = 
17                        longestCommonPrefix[startPos1 + 1][startPos2 + 1] + 1;
18                }
19            }
20        }
21      
22        int beautifulSplitCount = 0;
23      
24        // Try all possible split positions
25        // firstSplitPos: end of first part (exclusive)
26        // secondSplitPos: end of second part (exclusive)
27        for (int firstSplitPos = 1; firstSplitPos < arraySize - 1; firstSplitPos++) {
28            for (int secondSplitPos = firstSplitPos + 1; secondSplitPos < arraySize; secondSplitPos++) {
29                // Check if first part is a prefix of second part
30                // Conditions: length of first part <= length of second part
31                //            AND common prefix length >= length of first part
32                int firstPartLength = firstSplitPos;
33                int secondPartLength = secondSplitPos - firstSplitPos;
34                bool isFirstPartPrefixOfSecond = 
35                    (firstPartLength <= secondPartLength) && 
36                    (longestCommonPrefix[0][firstSplitPos] >= firstPartLength);
37              
38                // Check if second part is a prefix of third part
39                // Conditions: length of second part <= length of third part
40                //            AND common prefix length >= length of second part
41                int thirdPartLength = arraySize - secondSplitPos;
42                bool isSecondPartPrefixOfThird = 
43                    (secondPartLength <= thirdPartLength) && 
44                    (longestCommonPrefix[firstSplitPos][secondSplitPos] >= secondPartLength);
45              
46                // A split is beautiful if either condition is satisfied
47                if (isFirstPartPrefixOfSecond || isSecondPartPrefixOfThird) {
48                    beautifulSplitCount++;
49                }
50            }
51        }
52      
53        return beautifulSplitCount;
54    }
55};
56
1/**
2 * Counts the number of beautiful splits in an array where at least one pair
3 * of adjacent subarrays has the property that the first is a prefix of the second
4 * @param nums - The input array of numbers
5 * @returns The count of beautiful splits
6 */
7function beautifulSplits(nums: number[]): number {
8    const arrayLength: number = nums.length;
9  
10    // Build LCP (Longest Common Prefix) table
11    // lcp[i][j] represents the length of common prefix starting at indices i and j
12    const longestCommonPrefix: number[][] = Array.from(
13        { length: arrayLength + 1 }, 
14        () => Array(arrayLength + 1).fill(0)
15    );
16
17    // Fill the LCP table using dynamic programming
18    // Work backwards to utilize previously computed values
19    for (let i = arrayLength - 1; i >= 0; i--) {
20        for (let j = arrayLength - 1; j > i; j--) {
21            if (nums[i] === nums[j]) {
22                // If elements match, extend the common prefix length
23                longestCommonPrefix[i][j] = longestCommonPrefix[i + 1][j + 1] + 1;
24            }
25        }
26    }
27
28    let beautifulSplitCount: number = 0;
29  
30    // Try all possible ways to split the array into 3 parts
31    // i: end of first subarray (exclusive)
32    // j: end of second subarray (exclusive)
33    for (let i = 1; i < arrayLength - 1; i++) {
34        for (let j = i + 1; j < arrayLength; j++) {
35            // Check if first subarray is a prefix of second subarray
36            // Condition: length of first <= length of second AND common prefix covers entire first part
37            const isFirstPrefixOfSecond: boolean = i <= j - i && longestCommonPrefix[0][i] >= i;
38          
39            // Check if second subarray is a prefix of third subarray
40            // Condition: length of second <= length of third AND common prefix covers entire second part
41            const isSecondPrefixOfThird: boolean = j - i <= arrayLength - j && longestCommonPrefix[i][j] >= j - i;
42          
43            // Count this split if either condition is satisfied
44            if (isFirstPrefixOfSecond || isSecondPrefixOfThird) {
45                beautifulSplitCount++;
46            }
47        }
48    }
49
50    return beautifulSplitCount;
51}
52

Time and Space Complexity

The time complexity of this code is O(n^2), where n is the length of the array nums.

The analysis breaks down as follows:

  • The first nested loop (building the LCP table) iterates from i = n-1 down to 0, and for each i, the inner loop iterates from j = n-1 down to i. This creates approximately n^2/2 iterations, which is O(n^2).
  • The second nested loop iterates with i from 1 to n-2, and for each i, j iterates from i+1 to n-1. This also results in approximately n^2/2 iterations, which is O(n^2).
  • Inside both nested loops, all operations (array access, comparisons, arithmetic) are O(1).
  • Therefore, the overall time complexity is O(n^2) + O(n^2) = O(n^2).

The space complexity is O(n^2), where n is the length of the array nums.

The analysis is:

  • The LCP (Longest Common Prefix) table is a 2D array of size (n+1) × (n+1), which requires O(n^2) space.
  • Other variables (i, j, a, b, ans) use constant space O(1).
  • Therefore, the overall space complexity is O(n^2).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Off-by-One Errors in Split Boundaries

A critical pitfall is misunderstanding how the split positions work. Many developers incorrectly assume that i and j represent inclusive endpoints or make errors in calculating subarray lengths.

Incorrect Implementation:

# Wrong: Using inclusive ranges or incorrect length calculations
for i in range(1, n):  # This could include i = n-1, leaving no room for third part
    for j in range(i, n):  # This could make j = i, creating empty second part
        first_part_length = i + 1  # Wrong calculation
        second_part_length = j - i + 1  # Wrong calculation

Correct Implementation:

for i in range(1, n - 1):  # Ensures third part exists
    for j in range(i + 1, n):  # Ensures second part is non-empty
        first_part_length = i  # nums[0:i] has length i
        second_part_length = j - i  # nums[i:j] has length j-i
        third_part_length = n - j  # nums[j:n] has length n-j

2. Incorrect LCP Table Construction Order

Building the LCP table in the wrong order leads to using uninitialized values, resulting in incorrect prefix length calculations.

Incorrect Implementation:

# Wrong: Forward iteration doesn't allow using future values
for i in range(n):
    for j in range(i, n):
        if nums[i] == nums[j]:
            lcp[i][j] = lcp[i + 1][j + 1] + 1  # Uses uncomputed values!

Correct Implementation:

# Correct: Backward iteration ensures dependent values are already computed
for i in range(n - 1, -1, -1):
    for j in range(n - 1, i - 1, -1):
        if nums[i] == nums[j]:
            lcp[i][j] = lcp[i + 1][j + 1] + 1  # Values already computed

3. Confusing Prefix Check Logic

The prefix relationship requires both length and content checks. A common mistake is only checking one condition or mixing up which indices to use in the LCP table.

Incorrect Implementation:

# Wrong: Only checking LCP without length constraint
first_is_prefix = lcp[0][i] >= i  # Missing length check

# Wrong: Using wrong indices for second prefix check
second_is_prefix = lcp[0][j] >= j - i  # Should start from i, not 0

Correct Implementation:

# Must check both length constraint and actual prefix match
first_is_prefix = (i <= j - i) and (lcp[0][i] >= i)
second_is_prefix = (j - i <= n - j) and (lcp[i][j] >= j - i)

4. Edge Case: Small Arrays

Failing to handle arrays with fewer than 3 elements, though the problem constraints typically guarantee n >= 3.

Safe Implementation with Guard:

def beautifulSplits(self, nums: List[int]) -> int:
    n = len(nums)
    if n < 3:
        return 0  # Cannot split into 3 non-empty parts
    # ... rest of the solution

5. Memory Optimization Oversight

While not incorrect, using a full n x n LCP table when we only need the upper triangle can waste memory in large inputs.

Memory-Optimized Approach:

# Only compute and store necessary LCP values
lcp = [[0] * (n + 1) for _ in range(n + 1)]
for i in range(n - 1, -1, -1):
    for j in range(max(i, n - 1), i - 1, -1):  # Only compute j >= i
        if nums[i] == nums[j]:
            lcp[i][j] = lcp[i + 1][j + 1] + 1
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the minimum element can be found in:


Recommended Readings

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

Load More