1712. Ways to Split Array Into Three Subarrays
Problem Description
You are given an array nums
of non-negative integers. Your task is to find the number of ways to split this array into three non-empty contiguous subarrays (called left
, mid
, and right
from left to right) such that:
- Each subarray must be non-empty and contiguous
- The sum of elements in
left
≤ the sum of elements inmid
- The sum of elements in
mid
≤ the sum of elements inright
For example, if you have an array [1, 2, 2, 2, 5, 0]
, you need to find all valid ways to place two split points that divide it into three parts where the sum increases (or stays equal) from left to right.
Since the total number of valid splits can be very large, return the result modulo 10^9 + 7
.
The solution uses prefix sums combined with binary search. A prefix sum array s
is created where s[i]
represents the sum of elements from index 0 to i. For each possible ending position i
of the left
subarray, binary search finds:
- The leftmost valid starting position
j
for theright
subarray (wheres[j] - s[i] ≥ s[i]
) - The rightmost valid starting position
k
for theright
subarray (wheres[n-1] - s[k] ≥ s[k] - s[i]
)
The number of valid splits with left
ending at position i
is k - j
. The total count is the sum of all such valid splits across all possible positions of i
.
Intuition
The key insight is that we need to find valid positions for two split points that divide the array into three parts with non-decreasing sums. Since we're dealing with non-negative integers, the prefix sum array will be monotonically non-decreasing, which opens the door for binary search optimization.
Let's think about this step by step. If we fix the ending position of the left
subarray at index i
, we need to find all valid positions where we can place the second split (between mid
and right
). For a position j
to be valid as the start of the right
subarray:
- The sum of
mid
(fromi+1
toj-1
) must be ≥ sum ofleft
(from0
toi
) - The sum of
right
(fromj
ton-1
) must be ≥ sum ofmid
Using prefix sums, these conditions translate to:
s[j-1] - s[i] ≥ s[i]
, which simplifies tos[j-1] ≥ 2 * s[i]
s[n-1] - s[j-1] ≥ s[j-1] - s[i]
, which simplifies tos[j-1] ≤ (s[n-1] + s[i]) / 2
Since the prefix sum array is sorted, for a fixed i
, there exists a continuous range [j, k)
where all positions satisfy both conditions. The leftmost valid position can be found using binary search for the smallest value ≥ 2 * s[i]
, and the rightmost valid position can be found by searching for the largest value ≤ (s[n-1] + s[i]) / 2
.
This transforms our problem from checking every possible combination (which would be O(n²)) to efficiently finding valid ranges using binary search for each possible left
endpoint, reducing the complexity to O(n log n).
Learn more about Two Pointers, Binary Search and Prefix Sum patterns.
Solution Approach
The implementation follows a prefix sum + binary search strategy:
Step 1: Build Prefix Sum Array
s = list(accumulate(nums))
We create a prefix sum array where s[i]
represents the sum of elements from index 0 to i. This allows us to calculate any subarray sum in O(1) time: sum from index i
to j
equals s[j] - s[i-1]
.
Step 2: Enumerate Left Subarray Endpoints
for i in range(n - 2):
We iterate through all possible ending positions for the left
subarray. We stop at n-2
because we need at least one element for mid
and one for right
.
Step 3: Find Valid Range for Right Subarray Start
For each fixed left
endpoint at position i
:
-
Find minimum valid position
j
:j = bisect_left(s, s[i] << 1, i + 1, n - 1)
We need
s[j] ≥ 2 * s[i]
(written ass[i] << 1
for efficiency). This ensures the sum ofmid
is at least the sum ofleft
. We search fromi + 1
(afterleft
ends) ton - 1
. -
Find maximum valid position
k
:k = bisect_right(s, (s[-1] + s[i]) >> 1, j, n - 1)
We need
s[k] ≤ (s[n-1] + s[i]) / 2
(using>> 1
for integer division). This ensures the sum ofright
is at least the sum ofmid
. We search fromj
(the minimum valid position) ton - 1
.
Step 4: Count Valid Splits
ans += k - j
The number of valid ways to place the second split with the first split after position i
is k - j
. Each index in the range [j, k)
represents a valid starting position for the right
subarray.
Step 5: Return Result with Modulo
return ans % mod
Since the answer can be large, we return it modulo 10^9 + 7
.
The time complexity is O(n log n) where n is the length of the array - we iterate through n positions and perform two binary searches for each. The space complexity is O(n) for storing the prefix sum array.
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 the array nums = [1, 2, 2, 2, 5, 0]
.
Step 1: Build Prefix Sum Array
nums = [1, 2, 2, 2, 5, 0] s = [1, 3, 5, 7, 12, 12]
Each element in s
represents the cumulative sum up to that index.
Step 2: Iterate Through Possible Left Endpoints
Let's examine when i = 0
(left subarray is just [1]
):
- Sum of left =
s[0] = 1
- We need to find positions
j
where:- The mid subarray sum ≥ 1 (sum of left)
- The right subarray sum ≥ mid subarray sum
Finding valid range for j:
-
Minimum j: We need
s[j] ≥ 2 * s[0] = 2 * 1 = 2
- Using binary search from index 1 to 5: finds
j = 1
(sinces[1] = 3 ≥ 2
)
- Using binary search from index 1 to 5: finds
-
Maximum k: We need
s[k] ≤ (s[5] + s[0]) / 2 = (12 + 1) / 2 = 6
- Using binary search from index 1 to 5: finds
k = 3
(sinces[2] = 5 ≤ 6
buts[3] = 7 > 6
)
- Using binary search from index 1 to 5: finds
Valid positions for second split: [1, 2]
(k - j = 3 - 1 = 2 valid ways)
Let's verify these splits:
- Split at j=1:
[1] | [2] | [2,2,5,0]
→ sums: 1, 2, 9 ✓ (1 ≤ 2 ≤ 9) - Split at j=2:
[1] | [2,2] | [2,5,0]
→ sums: 1, 4, 7 ✓ (1 ≤ 4 ≤ 7)
When i = 1 (left subarray is [1,2]
):
- Sum of left =
s[1] = 3
- Minimum j: Need
s[j] ≥ 2 * 3 = 6
, findsj = 3
(sinces[3] = 7 ≥ 6
) - Maximum k: Need
s[k] ≤ (12 + 3) / 2 = 7
, findsk = 4
(sinces[3] = 7 ≤ 7
)
Valid positions: [3]
(k - j = 4 - 3 = 1 valid way)
- Split at j=3:
[1,2] | [2,2] | [5,0]
→ sums: 3, 4, 5 ✓ (3 ≤ 4 ≤ 5)
When i = 2 (left subarray is [1,2,2]
):
- Sum of left =
s[2] = 5
- Minimum j: Need
s[j] ≥ 2 * 5 = 10
, findsj = 4
(sinces[4] = 12 ≥ 10
) - Maximum k: Need
s[k] ≤ (12 + 5) / 2 = 8
, buts[4] = 12 > 8
No valid positions (k < j), so 0 valid ways.
When i = 3 (left subarray is [1,2,2,2]
):
- Sum of left =
s[3] = 7
- Minimum j: Need
s[j] ≥ 2 * 7 = 14
, but maxs[5] = 12 < 14
No valid positions, so 0 valid ways.
Total: 2 + 1 + 0 + 0 = 3 valid ways to split the array.
Solution Implementation
1from typing import List
2from itertools import accumulate
3from bisect import bisect_left, bisect_right
4
5
6class Solution:
7 def waysToSplit(self, nums: List[int]) -> int:
8 """
9 Count the number of ways to split the array into three non-empty parts
10 such that left_sum <= middle_sum <= right_sum.
11
12 Args:
13 nums: List of non-negative integers
14
15 Returns:
16 Number of valid ways to split the array, modulo 10^9 + 7
17 """
18 MOD = 10**9 + 7
19
20 # Calculate prefix sums for efficient range sum queries
21 prefix_sums = list(accumulate(nums))
22 total_sum = prefix_sums[-1]
23 n = len(nums)
24 result = 0
25
26 # Iterate through possible positions for the first split (end of left part)
27 # i is the last index of the left part
28 for i in range(n - 2): # Need at least 1 element for middle and 1 for right
29 left_sum = prefix_sums[i]
30
31 # Find the minimum valid position for the second split
32 # Middle sum >= left sum means: prefix_sums[j] - left_sum >= left_sum
33 # Which simplifies to: prefix_sums[j] >= 2 * left_sum
34 min_j = bisect_left(prefix_sums, left_sum * 2, i + 1, n - 1)
35
36 # Find the maximum valid position for the second split
37 # Right sum >= middle sum means: total_sum - prefix_sums[j] >= prefix_sums[j] - left_sum
38 # Which simplifies to: prefix_sums[j] <= (total_sum + left_sum) / 2
39 max_j = bisect_right(prefix_sums, (total_sum + left_sum) // 2, min_j, n - 1)
40
41 # Add the number of valid positions for the second split
42 result += max_j - min_j
43
44 return result % MOD
45
1class Solution {
2 private static final int MOD = (int) 1e9 + 7;
3
4 public int waysToSplit(int[] nums) {
5 int n = nums.length;
6
7 // Build prefix sum array
8 int[] prefixSum = new int[n];
9 prefixSum[0] = nums[0];
10 for (int i = 1; i < n; i++) {
11 prefixSum[i] = prefixSum[i - 1] + nums[i];
12 }
13
14 int result = 0;
15
16 // Iterate through all possible positions for the first split
17 // i represents the end index of the left subarray
18 for (int i = 0; i < n - 2; i++) {
19 // Find the leftmost valid position for the second split
20 // The middle subarray sum must be >= left subarray sum
21 // So prefixSum[j] >= 2 * prefixSum[i]
22 int leftmostSecondSplit = search(prefixSum, prefixSum[i] * 2, i + 1, n - 1);
23
24 // Find the rightmost valid position for the second split + 1
25 // The middle subarray sum must be <= right subarray sum
26 // So prefixSum[j] <= (prefixSum[n-1] + prefixSum[i]) / 2
27 int rightmostSecondSplitPlusOne = search(prefixSum, (prefixSum[n - 1] + prefixSum[i]) / 2 + 1, leftmostSecondSplit, n - 1);
28
29 // Add the number of valid positions for the second split
30 result = (result + rightmostSecondSplitPlusOne - leftmostSecondSplit) % MOD;
31 }
32
33 return result;
34 }
35
36 /**
37 * Binary search to find the leftmost index where prefixSum[index] >= target
38 * @param prefixSum the prefix sum array
39 * @param target the target value to search for
40 * @param left the left boundary of the search range (inclusive)
41 * @param right the right boundary of the search range (inclusive)
42 * @return the leftmost index where prefixSum[index] >= target
43 */
44 private int search(int[] prefixSum, int target, int left, int right) {
45 while (left < right) {
46 int mid = (left + right) / 2;
47 if (prefixSum[mid] >= target) {
48 right = mid;
49 } else {
50 left = mid + 1;
51 }
52 }
53 return left;
54 }
55}
56
1class Solution {
2public:
3 const int MOD = 1e9 + 7;
4
5 int waysToSplit(vector<int>& nums) {
6 int n = nums.size();
7
8 // Build prefix sum array
9 vector<int> prefixSum(n, nums[0]);
10 for (int i = 1; i < n; ++i) {
11 prefixSum[i] = prefixSum[i - 1] + nums[i];
12 }
13
14 int result = 0;
15
16 // Iterate through all possible positions for the first split
17 for (int i = 0; i < n - 2; ++i) {
18 // Find the minimum valid position for the second split
19 // The sum of the left part (0 to i) should be <= sum of the middle part
20 // prefixSum[i] <= prefixSum[j] - prefixSum[i]
21 // 2 * prefixSum[i] <= prefixSum[j]
22 int minSecondSplit = lower_bound(prefixSum.begin() + i + 1,
23 prefixSum.begin() + n - 1,
24 prefixSum[i] * 2) - prefixSum.begin();
25
26 // Find the maximum valid position for the second split
27 // The sum of the middle part should be <= sum of the right part
28 // prefixSum[k-1] - prefixSum[i] <= prefixSum[n-1] - prefixSum[k-1]
29 // 2 * prefixSum[k-1] <= prefixSum[n-1] + prefixSum[i]
30 // We use upper_bound to find the first position where this condition is violated
31 int maxSecondSplit = upper_bound(prefixSum.begin() + minSecondSplit,
32 prefixSum.begin() + n - 1,
33 (prefixSum[n - 1] + prefixSum[i]) / 2) - prefixSum.begin();
34
35 // Add the number of valid positions for the second split
36 result = (result + maxSecondSplit - minSecondSplit) % MOD;
37 }
38
39 return result;
40 }
41};
42
1/**
2 * @param {number[]} nums - Array of numbers to split
3 * @return {number} - Number of ways to split the array into three non-empty contiguous subarrays
4 */
5function waysToSplit(nums: number[]): number {
6 const MOD = 1e9 + 7;
7 const arrayLength = nums.length;
8
9 // Build prefix sum array where prefixSum[i] represents sum from index 0 to i
10 const prefixSum: number[] = new Array(arrayLength).fill(nums[0]);
11 for (let i = 1; i < arrayLength; ++i) {
12 prefixSum[i] = prefixSum[i - 1] + nums[i];
13 }
14
15 /**
16 * Binary search to find the leftmost index where prefixSum[index] >= target
17 * @param prefixSum - Prefix sum array
18 * @param target - Target value to search for
19 * @param left - Left boundary of search range
20 * @param right - Right boundary of search range
21 * @return The leftmost index where condition is met
22 */
23 function binarySearch(prefixSum: number[], target: number, left: number, right: number): number {
24 while (left < right) {
25 const mid = (left + right) >> 1;
26 if (prefixSum[mid] >= target) {
27 right = mid;
28 } else {
29 left = mid + 1;
30 }
31 }
32 return left;
33 }
34
35 let totalWays = 0;
36
37 // Iterate through possible positions for the first split
38 for (let i = 0; i < arrayLength - 2; ++i) {
39 // Find minimum valid position for second split
40 // Left subarray sum: prefixSum[i]
41 // Middle subarray must have sum >= left subarray sum
42 // So prefixSum[j] >= 2 * prefixSum[i]
43 const minSecondSplit = binarySearch(prefixSum, prefixSum[i] << 1, i + 1, arrayLength - 1);
44
45 // Find maximum valid position for second split
46 // Right subarray sum: prefixSum[n-1] - prefixSum[k-1]
47 // Middle subarray sum: prefixSum[k-1] - prefixSum[i]
48 // Middle sum <= right sum means: prefixSum[k-1] <= (prefixSum[n-1] + prefixSum[i]) / 2
49 // We search for the first position where this condition is violated
50 const maxSecondSplitPlusOne = binarySearch(
51 prefixSum,
52 ((prefixSum[arrayLength - 1] + prefixSum[i]) >> 1) + 1,
53 minSecondSplit,
54 arrayLength - 1
55 );
56
57 // Add the number of valid positions for the second split
58 totalWays = (totalWays + maxSecondSplitPlusOne - minSecondSplit) % MOD;
59 }
60
61 return totalWays;
62}
63
Time and Space Complexity
Time Complexity: O(n × log n)
, where n
is the length of the array nums
.
- Creating the prefix sum array using
accumulate(nums)
takesO(n)
time. - The outer loop runs
n - 2
times, iterating through each possible position for the first split. - Inside each iteration:
bisect_left
performs binary search on the prefix sum array, takingO(log n)
time.bisect_right
performs binary search on the prefix sum array, takingO(log n)
time.
- Therefore, the total time complexity is
O(n - 2) × O(log n + log n) = O(n × log n)
.
Space Complexity: O(n)
- The prefix sum array
s
storesn
elements, requiringO(n)
space. - Other variables (
ans
,i
,j
,k
,mod
) use constant spaceO(1)
. - The overall space complexity is
O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Errors in Binary Search Boundaries
One of the most common mistakes is incorrectly setting the search boundaries for bisect_left
and bisect_right
.
Pitfall Example:
# Incorrect: searching up to n instead of n-1 min_j = bisect_left(prefix_sums, left_sum * 2, i + 1, n) # Wrong!
Why it's wrong: The second split must leave at least one element for the right subarray. If we allow j
to be n-1
, then the right subarray would be empty.
Correct Solution:
# Correct: ensure at least one element remains for the right subarray min_j = bisect_left(prefix_sums, left_sum * 2, i + 1, n - 1)
2. Integer Overflow When Calculating Middle Point
When finding the maximum valid position for the second split, we need to calculate (total_sum + left_sum) / 2
.
Pitfall Example:
# Potential overflow in other languages, or precision issues with division max_j = bisect_right(prefix_sums, (total_sum + left_sum) / 2, min_j, n - 1) # Wrong!
Why it's wrong: Using regular division /
returns a float, which can cause precision issues when comparing with integer prefix sums. Additionally, in languages like Java or C++, the sum might overflow.
Correct Solution:
# Use integer division to avoid precision issues max_j = bisect_right(prefix_sums, (total_sum + left_sum) // 2, min_j, n - 1) # Or use bit shift for efficiency max_j = bisect_right(prefix_sums, (total_sum + left_sum) >> 1, min_j, n - 1)
3. Forgetting to Handle Edge Cases with Invalid Ranges
When min_j
exceeds the valid range, we might still try to calculate max_j
with invalid parameters.
Pitfall Example:
for i in range(n - 2):
left_sum = prefix_sums[i]
min_j = bisect_left(prefix_sums, left_sum * 2, i + 1, n - 1)
max_j = bisect_right(prefix_sums, (total_sum + left_sum) // 2, min_j, n - 1)
result += max_j - min_j # Could be negative if min_j is out of bounds!
Correct Solution:
for i in range(n - 2):
left_sum = prefix_sums[i]
min_j = bisect_left(prefix_sums, left_sum * 2, i + 1, n - 1)
# Check if min_j is within valid range
if min_j < n - 1:
max_j = bisect_right(prefix_sums, (total_sum + left_sum) // 2, min_j, n - 1)
result += max_j - min_j
4. Misunderstanding the Inequality Conditions
The problem requires left_sum ≤ mid_sum ≤ right_sum
. A common mistake is using strict inequalities or reversing the conditions.
Pitfall Example:
# Wrong: using strict inequality (should be ≤, not <) min_j = bisect_left(prefix_sums, left_sum * 2 + 1, i + 1, n - 1) # Wrong!
Correct Solution:
# Correct: finding the first position where prefix_sums[j] >= 2 * left_sum min_j = bisect_left(prefix_sums, left_sum * 2, i + 1, n - 1)
5. Not Applying Modulo at the Right Time
While Python handles large integers well, forgetting to apply modulo can still cause issues in other languages or when the problem explicitly requires it.
Pitfall Example:
for i in range(n - 2):
# ... calculation ...
result += max_j - min_j
result %= MOD # Applying modulo inside the loop is inefficient
Correct Solution:
for i in range(n - 2):
# ... calculation ...
result += max_j - min_j
# Apply modulo once at the end (more efficient)
return result % MOD
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Want a Structured Path to Master System Design Too? Don’t Miss This!