2750. Ways to Split Array Into Good Subarrays
Problem Description
You have a binary array nums
containing only 0s and 1s.
A subarray is considered good if it contains exactly one element with value 1
.
Your task is to find the total number of ways to split the entire array nums
into consecutive good subarrays. Each element must belong to exactly one subarray, and the subarrays must cover the entire array without overlapping.
Since the answer can be very large, return it modulo 10^9 + 7
.
For example, if nums = [0,1,0,0,1]
, one valid split would be [0,1,0]
and [0,1]
, where each subarray contains exactly one 1
.
The key insight is that between any two consecutive 1
s in the array, you can place a dividing line at any position between them. If two 1
s are at indices j
and i
, there are i - j
different positions where you can place the dividing line. The total number of ways to split the array is the product of all such possibilities between consecutive pairs of 1
s.
If the array contains no 1
s, it's impossible to create any good subarrays, so the answer would be 0
.
Intuition
Let's think about what makes a valid split. Each subarray must contain exactly one 1
. This means that every 1
in the array acts as an "anchor" - it must belong to exactly one subarray, and that subarray cannot contain any other 1
.
Consider what happens between two consecutive 1
s. The zeros between them can go either with the left 1
or the right 1
. For example, if we have [1, 0, 0, 0, 1]
, we need to decide where to "cut" between these two 1
s:
- We could cut after the first
1
:[1] | [0, 0, 0, 1]
- We could cut after the first zero:
[1, 0] | [0, 0, 1]
- We could cut after the second zero:
[1, 0, 0] | [0, 1]
- We could cut after the third zero:
[1, 0, 0, 0] | [1]
Notice that if the two 1
s are at positions j
and i
, we have exactly i - j
positions where we can place the cut (any position between j
and i
).
Now here's the key insight: these decisions are independent. The choice of where to cut between the first and second 1
doesn't affect where we can cut between the second and third 1
. This independence means we can use the multiplication principle - the total number of ways is the product of the number of choices at each decision point.
So the algorithm becomes: find all consecutive pairs of 1
s, calculate the number of ways to split between each pair (i - j
), and multiply all these numbers together. If there are no 1
s in the array, return 0
since we can't form any good subarrays.
Learn more about Math and Dynamic Programming patterns.
Solution Approach
Based on the multiplication principle we identified, let's implement the solution step by step:
-
Initialize variables:
ans = 1
: This will store our final answer (product of all possibilities)j = -1
: This tracks the index of the previous1
we encounteredmod = 10^9 + 7
: For taking modulo of the result
-
Iterate through the array: For each element at index
i
:- If the element is
0
, skip it (continue to next iteration) - If the element is
1
:- Check if we've seen a
1
before (i.e.,j > -1
) - If yes, multiply
ans
by(i - j)
which represents the number of ways to split between the previous1
at indexj
and current1
at indexi
- Update
j = i
to mark the current1
as the new "previous1
" for the next iteration
- Check if we've seen a
- If the element is
-
Handle edge cases:
- If
j == -1
after the loop, it means we never found any1
in the array, so return0
- Otherwise, return
ans
- If
Why this works:
- Between any two consecutive
1
s at positionsj
andi
, we can place the dividing line at any of thei - j
positions - Each placement decision is independent of others
- By the multiplication principle, total ways = product of individual possibilities
- We use modulo operation at each multiplication step to prevent overflow
Time Complexity: O(n)
where n
is the length of the array (single pass)
Space Complexity: O(1)
as we only use a constant amount of extra space
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 = [0,1,0,0,1,0,1]
.
Step 1: Initialize variables
ans = 1
(will store our product)j = -1
(tracks previous 1's index)mod = 10^9 + 7
Step 2: Iterate through the array
-
i = 0:
nums[0] = 0
→ Skip (it's not a 1) -
i = 1:
nums[1] = 1
→ First 1 found!- Check if
j > -1
? No (j = -1
), so this is our first 1 - Update
j = 1
ans
remains 1
- Check if
-
i = 2:
nums[2] = 0
→ Skip -
i = 3:
nums[3] = 0
→ Skip -
i = 4:
nums[4] = 1
→ Second 1 found!- Check if
j > -1
? Yes (j = 1
) - Calculate ways to split between indices 1 and 4:
4 - 1 = 3
- This means we can cut at positions 2, 3, or 4:
- Position 2:
[0,1] | [0,0,1,0,1]
- Position 3:
[0,1,0] | [0,1,0,1]
- Position 4:
[0,1,0,0] | [1,0,1]
- Position 2:
- Update
ans = 1 × 3 = 3
- Update
j = 4
- Check if
-
i = 5:
nums[5] = 0
→ Skip -
i = 6:
nums[6] = 1
→ Third 1 found!- Check if
j > -1
? Yes (j = 4
) - Calculate ways to split between indices 4 and 6:
6 - 4 = 2
- This means we can cut at positions 5 or 6:
- Position 5:
...previous... | [1] | [0,1]
- Position 6:
...previous... | [1,0] | [1]
- Position 5:
- Update
ans = 3 × 2 = 6
- Update
j = 6
- Check if
Step 3: Return result
- Since
j = 6 > -1
, we found at least one 1 - Return
ans = 6
The 6 total ways to split the array are:
[0,1] | [0,0,1] | [0,1]
[0,1] | [0,0,1,0] | [1]
[0,1,0] | [0,1] | [0,1]
[0,1,0] | [0,1,0] | [1]
[0,1,0,0] | [1] | [0,1]
[0,1,0,0] | [1,0] | [1]
Each subarray contains exactly one 1, validating our solution!
Solution Implementation
1class Solution:
2 def numberOfGoodSubarraySplits(self, nums: List[int]) -> int:
3 # Define modulo for large number handling
4 MOD = 10**9 + 7
5
6 # Initialize result and last position of 1
7 result = 1
8 last_one_position = -1
9
10 # Iterate through the array with index and value
11 for current_index, value in enumerate(nums):
12 # Skip if current element is 0
13 if value == 0:
14 continue
15
16 # If this is not the first 1 encountered
17 if last_one_position > -1:
18 # Multiply result by the distance between consecutive 1s
19 # This represents the number of ways to split between them
20 result = (result * (current_index - last_one_position)) % MOD
21
22 # Update the position of the last 1
23 last_one_position = current_index
24
25 # Return 0 if no 1s found, otherwise return the result
26 return 0 if last_one_position == -1 else result
27
1class Solution {
2 public int numberOfGoodSubarraySplits(int[] nums) {
3 // Define modulo constant for preventing integer overflow
4 final int MODULO = (int) 1e9 + 7;
5
6 // Initialize result to 1 (multiplicative identity)
7 int result = 1;
8
9 // Track the index of the previous 1 encountered (-1 means no 1 found yet)
10 int previousOneIndex = -1;
11
12 // Iterate through the array to find all 1s
13 for (int currentIndex = 0; currentIndex < nums.length; currentIndex++) {
14 // Skip if current element is 0
15 if (nums[currentIndex] == 0) {
16 continue;
17 }
18
19 // Current element is 1
20 // If this is not the first 1, multiply result by the gap between consecutive 1s
21 if (previousOneIndex > -1) {
22 // The gap between two 1s determines the number of valid split points
23 // Gap = currentIndex - previousOneIndex gives us the number of ways to split
24 long product = (long) result * (currentIndex - previousOneIndex);
25 result = (int) (product % MODULO);
26 }
27
28 // Update the position of the most recent 1
29 previousOneIndex = currentIndex;
30 }
31
32 // If no 1 was found in the array, return 0 (no valid splits possible)
33 // Otherwise, return the calculated result
34 return previousOneIndex == -1 ? 0 : result;
35 }
36}
37
1class Solution {
2public:
3 int numberOfGoodSubarraySplits(vector<int>& nums) {
4 // Define modulo constant for preventing integer overflow
5 const int MOD = 1e9 + 7;
6
7 // Initialize result to 1 (multiplicative identity)
8 // Initialize lastOneIndex to -1 (indicates no 1 has been found yet)
9 int result = 1;
10 int lastOneIndex = -1;
11
12 // Iterate through the array to find all 1s
13 for (int currentIndex = 0; currentIndex < nums.size(); ++currentIndex) {
14 // Skip if current element is 0
15 if (nums[currentIndex] == 0) {
16 continue;
17 }
18
19 // If we found a 1 and this is not the first 1
20 if (lastOneIndex > -1) {
21 // Multiply result by the number of ways to split between consecutive 1s
22 // The number of ways equals the distance between current and previous 1
23 // Use 1LL to cast to long long to prevent overflow during multiplication
24 result = static_cast<long long>(result) * (currentIndex - lastOneIndex) % MOD;
25 }
26
27 // Update the position of the last found 1
28 lastOneIndex = currentIndex;
29 }
30
31 // If no 1 was found in the array, return 0 (no valid splits possible)
32 // Otherwise, return the calculated result
33 return lastOneIndex == -1 ? 0 : result;
34 }
35};
36
1/**
2 * Counts the number of ways to split an array into good subarrays.
3 * A good subarray must contain at least one 1.
4 * The split positions can only be between consecutive 1s.
5 *
6 * @param nums - The input binary array containing only 0s and 1s
7 * @returns The number of ways to split the array into good subarrays, modulo 10^9 + 7
8 */
9function numberOfGoodSubarraySplits(nums: number[]): number {
10 // Initialize the result to 1 (base case for multiplication)
11 let result: number = 1;
12
13 // Track the index of the previous 1 encountered
14 let previousOneIndex: number = -1;
15
16 // Define the modulo value to prevent integer overflow
17 const MODULO: number = 10 ** 9 + 7;
18
19 // Get the length of the input array
20 const arrayLength: number = nums.length;
21
22 // Iterate through each element in the array
23 for (let currentIndex = 0; currentIndex < arrayLength; ++currentIndex) {
24 // Skip if the current element is 0
25 if (nums[currentIndex] === 0) {
26 continue;
27 }
28
29 // If we found a 1 and there was a previous 1
30 if (previousOneIndex > -1) {
31 // Multiply the result by the number of positions between consecutive 1s
32 // This represents the number of ways to split between these two 1s
33 result = (result * (currentIndex - previousOneIndex)) % MODULO;
34 }
35
36 // Update the index of the most recent 1
37 previousOneIndex = currentIndex;
38 }
39
40 // If no 1 was found in the array, return 0 (no valid splits possible)
41 // Otherwise, return the calculated result
42 return previousOneIndex === -1 ? 0 : result;
43}
44
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the array nums
. The algorithm iterates through the array exactly once using the enumerate function, performing constant-time operations (comparisons, arithmetic operations, and modulo operations) for each element.
The space complexity is O(1)
. The algorithm only uses a fixed amount of extra space for variables mod
, ans
, j
, i
, and x
, regardless of the input size. No additional data structures that grow with the input size are created.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Handle the No-1s Case
Pitfall: Not checking if the array contains any 1s at all. If you don't handle this case, the code might return 1 instead of 0 for arrays like [0, 0, 0]
.
Solution: Always check if last_one_position
remains -1 after iteration, which indicates no 1s were found:
return 0 if last_one_position == -1 else result
2. Integer Overflow Without Modulo
Pitfall: Forgetting to apply modulo operation during multiplication. For large arrays with many 1s, the product can exceed integer limits before the final return statement.
Wrong approach:
result = result * (current_index - last_one_position) # May overflow # Then applying modulo only at the end return result % MOD
Correct approach:
result = (result * (current_index - last_one_position)) % MOD # Apply modulo immediately
3. Off-by-One Error in Distance Calculation
Pitfall: Incorrectly calculating the number of split positions between consecutive 1s. Some might think to use (current_index - last_one_position - 1)
or (current_index - last_one_position + 1)
.
Why i - j
is correct: If 1s are at indices 2 and 5, the positions between them are 3, 4, and 5 (right before index 5). That's exactly 5 - 2 = 3
positions.
4. Processing 0s Unnecessarily
Pitfall: Not skipping 0s and performing unnecessary checks or calculations for them, which doesn't affect correctness but impacts efficiency.
Inefficient:
for i, num in enumerate(nums):
if num == 1:
# process
else:
# unnecessary else block or checks
Efficient:
for i, num in enumerate(nums):
if num == 0:
continue # Skip immediately
# Process only 1s
5. Using Wrong Initial Value
Pitfall: Initializing result
to 0 instead of 1. Since we're multiplying values, starting with 0 would make the final result always 0.
Wrong: result = 0
Correct: result = 1
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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
Want a Structured Path to Master System Design Too? Don’t Miss This!