1712. Ways to Split Array Into Three Subarrays
Problem Description
In this problem, we need to find out how many ways we can split an array of non-negative integers into three non-empty contiguous subarrays wherein each subarray is named as left
, mid
, and right
from left to right. The splitting must satisfy the condition that the sum of the elements in left
is less than or equal to the sum of the elements in mid
, and in turn, the sum in mid
should be less than or equal to the sum of the elements in right
. If we find a way to split the array that meets these conditions, we call it a "good" split.
Our goal is to count the total number of these good splits and return that count modulo 10^9 + 7
as the number can be very large.
Intuition
The intuition behind the solution is based on the concept of prefix sums and binary search. The array nums
is processed to create another array, say s
, which is a cumulative sum array (using accumulate(nums)
). The cumulative sum array helps us quickly calculate the sum of any contiguous subarray.
Given the prefix sum array s
, for each index i
that could potentially be the end of the left
subarray, we want to find suitable indices j
and k
where j
marks the end of the mid
subarray, and k
represents the first index such that the sum of elements in the right
subarray is not less than the sum of mid
.
We use binary search (with bisect_left
and bisect_right
) to find the positions j
and k
based on the sum of left
.
- We start by fixing the end of the
left
subarray at indexi
. - We search for the smallest index
j
afteri
where the sum ofmid
is at least twice the sum ofleft
. This guarantees that sum(left
) ≤ sum(mid
). - We then search for the largest index
k
where the sum ofmid
is still less than or equal to the sum ofright
. This is ensured by the sum of the entire array minus the sum up toi
, divided by 2. - The number of positions for
k
minus the number of positions forj
gives us the count of good splits for a fixedi
. - We loop over all possible ends for the
left
subarray and sum up the counts to get the answer. - Finally, we return the answer modulo
10^9 + 7
to respect the constraints of the problem.
This approach is efficient as it trims down the search space using binary search rather than checking every possible split by brute-force which would be too slow.
Learn more about Two Pointers, Binary Search and Prefix Sum patterns.
Solution Approach
The solution approach is a combination of algorithmic strategies involving prefix sums, binary search, and modular arithmetic. Let's go through the steps of the implementation:
-
Prefix Sum Calculation: The first step involves calculating the prefix sums of the
nums
array and storing it in the arrays
. This is done usingaccumulate(nums)
. Prefix sums allow us to calculate the sum of any continuous subarray in constant time. -
Modular Arithmetic Constant: A constant
mod = 10**9 + 7
is defined at the beginning of the solution. This is used to perform modular arithmetic to prevent integer overflow and to return the final result modulo10^9 + 7
as required by the problem statement. -
Initialization: We initialize
ans
to store the count of good ways to split the array, andn
to store the length of thenums
array. -
Iterating Over Potential Left Subarray Ends: Using a
for
loop, we iterate over each indexi
which could be the end of theleft
subarray. We are careful to stop atn-2
because we need at least two more elements to form themid
andright
subarrays as they must be non-empty. -
Finding the Lower and Upper Bounds for Mid Subarray Ends:
- To find the lower bound
j
formid
's end, we usebisect_left
. We search the prefix sum arrays
for the smallest indexj
such that2 * s[i] <= s[j]
. This ensures that the sum ofmid
is at least as much as the sum ofleft
. - Similarly, to find the upper bound
k
formid
's end, we usebisect_right
. Here we look for the last indexk
such thats[k] <= (s[-1] + s[i]) / 2
. This ensures that sum(mid
) ≤ sum(right
).
- To find the lower bound
-
Counting Good Splits: For each position of
i
, the good splits number is given byk - j
, since for each index fromj
tok-1
, we can form a validmid
subarray maintaining the condition of good split. -
Accumulating and Modulo Operation: The number of good splits calculated for each
i
is added toans
. After the loop is finished,ans
may be a very large number, so we returnans % mod
to keep the result within the specified numeric range.
This solution is efficient due to binary search, which performs O(log n)
comparisons rather than linear scans, reducing the overall complexity to O(n log n)
for the problem. Without binary search, a brute-force approach would have a complexity of O(n^2)
, which would not be practical for large arrays.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let’s walk through a small example to illustrate the solution approach described above. Suppose we have the following array nums
of non-negative integers:
nums = [1, 2, 2, 2, 5, 0]
Following the steps of the solution:
- Prefix Sum Calculation: Create a prefix sum array
s
usingaccumulate
:
s = [1, 3, 5, 7, 12, 12] // The cumulative sum of `nums`
-
Modular Arithmetic Constant: Define
mod = 10**9 + 7
for later use. -
Initialization: Initialize
ans = 0
andn = 6
(length ofnums
). -
Iterating Over Potential Left Subarray Ends: We consider the potential ends of the
left
subarray. Indexi
can go from 0 ton-3
(inclusive) to leave room formid
andright
:
For i = 0, s[i] = 1 For i = 1, s[i] = 3 For i = 2, s[i] = 5 For i = 3, s[i] = 7
We stop at i = 3
because i = n-2
would not leave enough elements for mid
and right
.
-
Finding the Lower and Upper Bounds for Mid Subarray Ends:
- For each
i
, findj
andk
using binary search:
For i = 0 (s[i] = 1): - Search `j` such that `2 * s[i] <= s[j]`: `j = 2` - Search `k` such that `s[k] <= (s[-1] + s[i]) / 2`: `k = 4` - Good splits for i = 0: `k - j` = `4 - 2` = 2 For i = 1 (s[i] = 3): - `j = 3` - `k = 5` - Good splits for i = 1: `k - j` = `5 - 3` = 2 For i = 2 (s[i] = 5): - `j = 4` - `k = 5` - Good splits for i = 2: `k - j` = `5 - 4` = 1 For i = 3 (s[i] = 7): - `j = 4` - `k = 5` - Good splits for i = 3: `k - j` = `5 - 4` = 1
- For each
-
Counting Good Splits: Sum up all the good splits:
ans = 2 + 2 + 1 + 1 = 6
- Accumulating and Modulo Operation: Since our
ans
is small,ans % mod
is trivially6
. Ifans
were large, the modulo operation would ensure we get a result within the numeric limits of10^9 + 7
.
Therefore, there are 6
good splits of the array nums
following the rules set out by the problem.
Solution Implementation
1from itertools import accumulate
2from bisect import bisect_left, bisect_right
3from typing import List
4
5class Solution:
6 def ways_to_split(self, nums: List[int]) -> int:
7 # Constant for modulo operation to prevent overflow
8 MOD = 10**9 + 7
9
10 # Calculate the prefix sum of numbers to efficiently compute sums of subarrays
11 prefix_sums = list(accumulate(nums))
12
13 answer = 0 # Initialize answer which will hold the number of valid ways to split
14 num_elements = len(nums) # Total number of elements in nums
15
16 # Iterate through the array (except the last two elements, as those are needed for the second and third part)
17 for i in range(num_elements - 2):
18 # Find the left boundary 'j' for the second part where the sum of the second part is not less than the sum of the first part
19 j = bisect_left(prefix_sums, 2 * prefix_sums[i], i + 1, num_elements - 1)
20
21 # Find the right boundary 'k' for the second part where the sum of the third part is not less than the sum of the second part
22 k = bisect_right(prefix_sums, (prefix_sums[-1] + prefix_sums[i]) // 2, j, num_elements - 1)
23
24 # Increment the answer by the number of valid ways to split given the current first part
25 answer += k - j
26
27 # Return the total number of ways to split modulo MOD to handle large numbers
28 return answer % MOD
29
1class Solution {
2 private static final int MODULO = (int) 1e9 + 7;
3
4 public int waysToSplit(int[] nums) {
5 int length = nums.length;
6 // Create a prefix sum array
7 int[] prefixSum = new int[length];
8 prefixSum[0] = nums[0];
9 for (int i = 1; i < length; ++i) {
10 prefixSum[i] = prefixSum[i - 1] + nums[i];
11 }
12
13 int countWays = 0; // This will store the number of ways to split the array
14 // Loop through each possible index to split the array after
15 for (int i = 0; i < length - 2; ++i) {
16 // Find the earliest index to make the second part's sum at least equal to the first part
17 int earliest = binarySearchLeft(prefixSum, 2 * prefixSum[i], i + 1, length - 1);
18 // Find the latest index where the third part's sum would be at least as much as the second
19 int latest = binarySearchRight(prefixSum, ((prefixSum[length - 1] + prefixSum[i]) / 2) + 1, earliest, length - 1);
20 // Add the number of ways to split between these two indices
21 countWays = (countWays + latest - earliest) % MODULO;
22 }
23 return countWays;
24 }
25
26 private int binarySearchLeft(int[] prefixSum, int target, int leftIndex, int rightIndex) {
27 // Standard binary search to find the left-most index where prefixSum[index] >= target
28 while (leftIndex < rightIndex) {
29 int mid = (leftIndex + rightIndex) / 2;
30 if (prefixSum[mid] >= target) {
31 rightIndex = mid;
32 } else {
33 leftIndex = mid + 1;
34 }
35 }
36 return leftIndex;
37 }
38
39 private int binarySearchRight(int[] prefixSum, int target, int leftIndex, int rightIndex) {
40 // Binary search to find the right-most position to meet the constraints
41 while (leftIndex < rightIndex) {
42 int mid = (leftIndex + rightIndex) / 2;
43 if (prefixSum[mid] < target) {
44 leftIndex = mid + 1;
45 } else {
46 rightIndex = mid;
47 }
48 }
49 // We might overshoot by one, so we adjust if necessary
50 if (leftIndex == rightIndex && prefixSum[leftIndex] >= target) {
51 return leftIndex;
52 }
53 return leftIndex - 1;
54 }
55}
56
1class Solution {
2public:
3 const int MOD = 1e9 + 7; // Defining the modulo constant for operations
4
5 int waysToSplit(vector<int>& nums) {
6 int numCount = nums.size(); // n is the total number of elements in nums
7 // Create a prefix sum array where each element s[i] is the sum of nums[0] to nums[i]
8 vector<int> prefixSum(numCount, nums[0]);
9 for (int i = 1; i < numCount; ++i) {
10 prefixSum[i] = prefixSum[i - 1] + nums[i];
11 }
12 int answer = 0; // To store the final count of ways to split the vector
13 // Iterate through each element to find valid splits
14 for (int i = 0; i < numCount - 2; ++i) {
15 // Use binary search to find the smallest j where the sum of the first part is less than or equal to the sum of the second part
16 int j = lower_bound(prefixSum.begin() + i + 1, prefixSum.begin() + numCount - 1, 2 * prefixSum[i]) - prefixSum.begin();
17 // Use binary search to find the largest k where the sum of the second part is less than or equal to the sum of the third part
18 int k = upper_bound(prefixSum.begin() + j, prefixSum.begin() + numCount - 1, (prefixSum[numCount - 1] + prefixSum[i]) / 2) - prefixSum.begin();
19 // Add the count of ways between j and k to the answer
20 answer = (answer + k - j) % MOD;
21 }
22 return answer; // Return the final answer
23 }
24};
25
1/**
2 * Calculate the number of ways to split an array into three non-empty contiguous subarrays
3 * where the sum of the first subarray is less than or equal to the sum of the second subarray,
4 * which is less than or equal to the sum of the third subarray.
5 *
6 * @param {number[]} nums - The input array
7 * @return {number} - Number of ways to split the array
8 */
9function waysToSplit(nums: number[]): number {
10 const mod: number = 1e9 + 7;
11 const n: number = nums.length;
12 const prefixSums: number[] = new Array(n).fill(nums[0]);
13
14 // Compute the prefix sum of the array
15 for (let i: number = 1; i < n; ++i) {
16 prefixSums[i] = prefixSums[i - 1] + nums[i];
17 }
18
19 // Binary search function to find the leftmost (or rightmost) index
20 function binarySearch(prefixSums: number[], target: number, left: number, right: number): number {
21 while (left < right) {
22 // Find the middle index
23 const mid: number = (left + right) >> 1;
24
25 // Narrow the search range based on the sum comparison
26 if (prefixSums[mid] >= target) {
27 right = mid;
28 } else {
29 left = mid + 1;
30 }
31 }
32 return left;
33 }
34
35 // Initialize the answer variable
36 let answer: number = 0;
37
38 // Iterate through the array and count valid splits
39 for (let i: number = 0; i < n - 2; ++i) {
40 // Binary searches to find the eligible split indices j and k
41 const j = binarySearch(prefixSums, prefixSums[i] * 2, i + 1, n - 1);
42 const k = binarySearch(prefixSums, Math.floor((prefixSums[n - 1] + prefixSums[i]) / 2) + 1, j, n - 1);
43
44 // Update the number of ways to split
45 answer = (answer + k - j) % mod;
46 }
47
48 // Return the final answer
49 return answer;
50}
51
Time and Space Complexity
Time Complexity
The time complexity of the waysToSplit
method can be broken down into the following parts:
-
Calculating the prefix sum of the
nums
list usingaccumulate
. This takesO(n)
time, wheren
is the length ofnums
. -
The outer for-loop runs from
0
ton - 3
inclusive, which gives usO(n)
iterations. -
Inside the for-loop, it uses
bisect_left
andbisect_right
methods to find indicesj
andk
. Both of these binary search operations run inO(log n)
time.
Therefore, with every iteration of the for-loop, we perform two O(log n)
operations. Since we iterate O(n)
times, this leads to a compound time complexity of O(n log n)
for this part of the function.
The total time complexity of the function is therefore O(n) + O(n log n)
= O(n log n)
.
Space Complexity
The space complexity is determined by the additional space used by the algorithm, which includes:
-
The space used by the prefix sum array
s
. This array is the same length asnums
, so it requiresO(n)
space. -
Variables used for iteration and indexing, such as
i
,j
,k
,ans
,mod
, andn
, which useO(1)
space.
The dominant term here is the space used by the prefix sum array, so the total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly using problem constraints.
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!