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:
-
The three subarrays are consecutive parts of the original array. When concatenated in order (
nums1
+nums2
+nums3
), they reconstruct the original arraynums
. -
At least one of these relationships holds:
nums1
is a prefix ofnums2
(meaningnums2
starts with all the elements ofnums1
)nums2
is a prefix ofnums3
(meaningnums3
starts with all the elements ofnums2
)
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.
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 lengthi
nums2 = nums[i:j]
with lengthj - i
nums3 = nums[j:n]
with lengthn - 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 matchnums[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 matchnums[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 positionsi
andj
extends by 1 plus whatever common prefix exists at positionsi+1
andj+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), sonums1 = nums[0:i]
j
represents the end position of the second subarray (exclusive), sonums2 = nums[i:j]
andnums3 = 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:
-
Condition A:
nums1
is a prefix ofnums2
- 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)
- Length check:
-
Condition B:
nums2
is a prefix ofnums3
- 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)
- Length check:
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 EvaluatorExample 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, solcp[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! Solcp[1][3] = lcp[2][4] + 1 = 0 + 1 = 1
- At positions (1,2):
nums[1]=1
,nums[2]=2
, different values, solcp[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! Solcp[0][3] = lcp[1][4] + 1 = 0 + 1 = 1
- At positions (0,2):
nums[0]=1
,nums[2]=2
, different values, solcp[0][2] = 0
- At positions (0,1):
nums[0]=1
,nums[1]=1
, same! Solcp[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 to0
, and for eachi
, the inner loop iterates fromj = n-1
down toi
. This creates approximatelyn^2/2
iterations, which isO(n^2)
. - The second nested loop iterates with
i
from1
ton-2
, and for eachi
,j
iterates fromi+1
ton-1
. This also results in approximatelyn^2/2
iterations, which isO(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 requiresO(n^2)
space. - Other variables (
i
,j
,a
,b
,ans
) use constant spaceO(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
In a binary min heap, the minimum element can be found in:
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!