3130. Find All Possible Stable Binary Arrays II
Problem Description
You need to count the number of valid binary arrays that meet specific requirements.
Given three positive integers:
zero
: the exact number of 0s that must appear in the arrayone
: the exact number of 1s that must appear in the arraylimit
: the maximum size threshold for subarrays
A binary array is considered stable if it satisfies all three conditions:
- It contains exactly
zero
number of 0s - It contains exactly
one
number of 1s - Every subarray with size greater than
limit
must contain at least one 0 and at least one 1
The third condition ensures that you cannot have long consecutive sequences of the same digit. For example, if limit = 2
, then you cannot have subarrays like [0,0,0]
or [1,1,1]
since these subarrays of size 3 only contain one type of digit.
Your task is to find the total number of different stable binary arrays that can be formed. Since this number can be very large, return the result modulo 10^9 + 7
.
For instance, if zero = 1
, one = 1
, and limit = 2
, the valid arrays would be [0,1]
and [1,0]
, giving an answer of 2.
Intuition
The key insight is to think about building the array position by position while keeping track of what digit we place at each position. We need to ensure that no consecutive sequence of the same digit exceeds the limit
.
Let's think recursively: if we have i
zeros and j
ones remaining to place, and we know the last digit we placed was k
(where k
is either 0 or 1), how many valid arrays can we form?
When placing the next digit, we have two choices - place a 0 or place a 1. However, we need to be careful about the constraint that no subarray larger than limit
can contain only one type of digit.
Here's the crucial observation: if we place a digit k
at the current position, we need to ensure that within the last limit + 1
positions, there's at least one digit that's different from k
. This means if we've placed more than limit
consecutive same digits, the array becomes invalid.
To handle this constraint efficiently, we can use the principle of inclusion-exclusion. When calculating dfs(i, j, k)
:
- We first count all possible ways by considering both placing another
k
and placing the opposite digit - Then we subtract the invalid cases where we would have more than
limit
consecutivek
s
For example, if k = 0
, we calculate:
dfs(i - 1, j, 0)
: place another 0dfs(i - 1, j, 1)
: place a 1 after the 0- Subtract
dfs(i - limit - 1, j, 1)
: this represents the cases where we would havelimit + 1
consecutive 0s
The subtraction works because if we're at position with i
zeros remaining and we place limit + 1
consecutive zeros, we'd transition from a state where we had i + limit + 1
zeros with the last digit being 1, to a state with i
zeros. By subtracting these cases, we eliminate arrangements that violate the constraint.
The base cases are straightforward: when we have only zeros or only ones left, we can place them all consecutively only if their count doesn't exceed limit
.
Learn more about Dynamic Programming and Prefix Sum patterns.
Solution Approach
The solution uses dynamic programming with memoization to count valid stable arrays. We implement a recursive function dfs(i, j, k)
that represents the number of stable arrays when we have i
zeros and j
ones remaining to place, and the last placed digit is k
(0 or 1).
State Definition:
i
: number of zeros remaining to placej
: number of ones remaining to placek
: the last digit placed (0 or 1)
Base Cases:
- If
i = 0
(no zeros left):- Return 1 if
k = 1
andj ≤ limit
(we can place all remaining ones consecutively) - Return 0 otherwise
- Return 1 if
- If
j = 0
(no ones left):- Return 1 if
k = 0
andi ≤ limit
(we can place all remaining zeros consecutively) - Return 0 otherwise
- Return 1 if
Recursive Transition:
For k = 0
(last placed digit was 0):
dfs(i, j, 0) = dfs(i - 1, j, 0) + dfs(i - 1, j, 1) - dfs(i - limit - 1, j, 1)
dfs(i - 1, j, 0)
: place another 0 after the current 0dfs(i - 1, j, 1)
: place a 1 after the current 0-dfs(i - limit - 1, j, 1)
: subtract invalid cases where we'd havelimit + 1
consecutive 0s
For k = 1
(last placed digit was 1):
dfs(i, j, 1) = dfs(i, j - 1, 0) + dfs(i, j - 1, 1) - dfs(i, j - limit - 1, 0)
dfs(i, j - 1, 0)
: place a 0 after the current 1dfs(i, j - 1, 1)
: place another 1 after the current 1-dfs(i, j - limit - 1, 0)
: subtract invalid cases where we'd havelimit + 1
consecutive 1s
Implementation Details:
- Use
@cache
decorator for memoization to avoid recalculating same states - Handle edge cases where
i - limit - 1 < 0
orj - limit - 1 < 0
by returning 0 - The final answer is
(dfs(zero, one, 0) + dfs(zero, one, 1)) % mod
, considering both starting with 0 or 1 - Clear the cache after computation to free memory
The time complexity is O(zero × one × 2)
for the number of unique states, and space complexity is O(zero × one × 2)
for memoization storage.
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 a small example with zero = 3
, one = 2
, and limit = 1
.
Since limit = 1
, we cannot have more than 1 consecutive digit of the same type. This means our array must alternate between 0s and 1s.
Let's trace through the recursive calls starting with dfs(3, 2, 0)
(3 zeros left, 2 ones left, last digit placed was 0):
Step 1: Calculate dfs(3, 2, 0)
- We can place another 0:
dfs(2, 2, 0)
- We can place a 1:
dfs(2, 2, 1)
- Subtract invalid cases with 2 consecutive 0s:
-dfs(1, 2, 1)
Step 2: Calculate dfs(2, 2, 0)
- Place another 0:
dfs(1, 2, 0)
- Place a 1:
dfs(1, 2, 1)
- Subtract invalid:
-dfs(0, 2, 1)
When we reach dfs(0, 2, 1)
, this is a base case. Since we have 0 zeros left and 2 ones remaining, and 2 > limit
, we return 0 (cannot place 2 consecutive ones).
Step 3: Calculate dfs(1, 2, 1)
- Place a 0:
dfs(0, 2, 0)
- Place another 1:
dfs(0, 1, 1)
- Subtract invalid:
-dfs(0, 0, 0)
(returns 0 since j = 0)
For dfs(0, 2, 0)
, we have only ones left. Since 2 > limit
, we return 0.
For dfs(0, 1, 1)
, we have 1 one left and last digit is 1. Since 1 ≤ limit
, we return 1.
Step 4: Continue unwinding Working backward through our calculations:
dfs(1, 2, 1) = 0 + 1 - 0 = 1
dfs(1, 2, 0) = dfs(0, 2, 0) + dfs(0, 2, 1) - dfs(-1, 2, 1) = 0 + 0 - 0 = 0
dfs(2, 2, 0) = 0 + 1 - 0 = 1
Similarly, we calculate dfs(3, 2, 1)
starting with a 1.
The valid arrays for this example would be patterns like:
[0, 1, 0, 1, 0]
[1, 0, 1, 0, 0]
Each valid array must alternate digits since we cannot have consecutive same digits (limit = 1). The total count would be (dfs(3, 2, 0) + dfs(3, 2, 1)) % mod
.
Solution Implementation
1class Solution:
2 def numberOfStableArrays(self, zero: int, one: int, limit: int) -> int:
3 from functools import cache
4
5 MOD = 10**9 + 7
6
7 @cache
8 def count_arrays(zeros_left: int, ones_left: int, last_digit: int) -> int:
9 """
10 Count valid stable arrays with given zeros and ones remaining.
11
12 Args:
13 zeros_left: Number of zeros still to place
14 ones_left: Number of ones still to place
15 last_digit: The last digit placed (0 or 1)
16
17 Returns:
18 Number of valid stable arrays
19 """
20 # Base case: only ones left
21 if zeros_left == 0:
22 # Valid if last digit was 1 and remaining ones don't exceed limit
23 return int(last_digit == 1 and ones_left <= limit)
24
25 # Base case: only zeros left
26 if ones_left == 0:
27 # Valid if last digit was 0 and remaining zeros don't exceed limit
28 return int(last_digit == 0 and zeros_left <= limit)
29
30 # If last digit was 0, we're placing another 0
31 if last_digit == 0:
32 # Add one more 0 to the sequence
33 total = count_arrays(zeros_left - 1, ones_left, 0) + count_arrays(zeros_left - 1, ones_left, 1)
34
35 # Subtract invalid cases where we exceed limit consecutive zeros
36 # This happens when we have more than 'limit' consecutive zeros
37 if zeros_left - limit - 1 >= 0:
38 total -= count_arrays(zeros_left - limit - 1, ones_left, 1)
39
40 return total
41
42 # If last digit was 1, we're placing another 1
43 else:
44 # Add one more 1 to the sequence
45 total = count_arrays(zeros_left, ones_left - 1, 0) + count_arrays(zeros_left, ones_left - 1, 1)
46
47 # Subtract invalid cases where we exceed limit consecutive ones
48 # This happens when we have more than 'limit' consecutive ones
49 if ones_left - limit - 1 >= 0:
50 total -= count_arrays(zeros_left, ones_left - limit - 1, 0)
51
52 return total
53
54 # Calculate total by considering both starting with 0 and starting with 1
55 result = (count_arrays(zero, one, 0) + count_arrays(zero, one, 1)) % MOD
56
57 # Clear the cache to free memory
58 count_arrays.cache_clear()
59
60 return result
61
1class Solution {
2 // Modulo value for preventing integer overflow
3 private static final int MOD = 1_000_000_007;
4
5 // Memoization table: dp[zeroCount][oneCount][lastElement]
6 // where lastElement: 0 means last element is 0, 1 means last element is 1
7 private Long[][][] dp;
8
9 // Maximum consecutive elements allowed
10 private int maxConsecutive;
11
12 public int numberOfStableArrays(int zero, int one, int limit) {
13 // Initialize memoization table
14 dp = new Long[zero + 1][one + 1][2];
15 this.maxConsecutive = limit;
16
17 // Calculate total stable arrays ending with either 0 or 1
18 long totalArrays = (dfs(zero, one, 0) + dfs(zero, one, 1)) % MOD;
19 return (int) totalArrays;
20 }
21
22 /**
23 * Recursively calculates the number of stable arrays
24 * @param zerosRemaining - number of zeros left to place
25 * @param onesRemaining - number of ones left to place
26 * @param lastElementType - type of the last element placed (0 or 1)
27 * @return number of valid stable arrays for given parameters
28 */
29 private long dfs(int zerosRemaining, int onesRemaining, int lastElementType) {
30 // Base case: invalid state if negative counts
31 if (zerosRemaining < 0 || onesRemaining < 0) {
32 return 0;
33 }
34
35 // Base case: only zeros remaining
36 if (zerosRemaining == 0) {
37 // Valid only if last element was 0 and remaining ones don't exceed limit
38 return (lastElementType == 1 && onesRemaining <= maxConsecutive) ? 1 : 0;
39 }
40
41 // Base case: only ones remaining
42 if (onesRemaining == 0) {
43 // Valid only if last element was 1 and remaining zeros don't exceed limit
44 return (lastElementType == 0 && zerosRemaining <= maxConsecutive) ? 1 : 0;
45 }
46
47 // Return memoized result if already computed
48 if (dp[zerosRemaining][onesRemaining][lastElementType] != null) {
49 return dp[zerosRemaining][onesRemaining][lastElementType];
50 }
51
52 long result;
53
54 if (lastElementType == 0) {
55 // If last element is 0, we're placing a 0 at current position
56 // Sum of arrays with one less 0, ending with either 0 or 1
57 long totalWays = dfs(zerosRemaining - 1, onesRemaining, 0) +
58 dfs(zerosRemaining - 1, onesRemaining, 1);
59
60 // Subtract invalid arrays where we'd have more than 'limit' consecutive 0s
61 long invalidWays = dfs(zerosRemaining - maxConsecutive - 1, onesRemaining, 1);
62
63 // Apply modulo arithmetic to handle negative values correctly
64 result = (totalWays - invalidWays + MOD) % MOD;
65 } else {
66 // If last element is 1, we're placing a 1 at current position
67 // Sum of arrays with one less 1, ending with either 0 or 1
68 long totalWays = dfs(zerosRemaining, onesRemaining - 1, 0) +
69 dfs(zerosRemaining, onesRemaining - 1, 1);
70
71 // Subtract invalid arrays where we'd have more than 'limit' consecutive 1s
72 long invalidWays = dfs(zerosRemaining, onesRemaining - maxConsecutive - 1, 0);
73
74 // Apply modulo arithmetic to handle negative values correctly
75 result = (totalWays - invalidWays + MOD) % MOD;
76 }
77
78 // Memoize and return the result
79 dp[zerosRemaining][onesRemaining][lastElementType] = result;
80 return result;
81 }
82}
83
1class Solution {
2public:
3 int numberOfStableArrays(int zero, int one, int limit) {
4 const int MOD = 1e9 + 7;
5 using ll = long long;
6
7 // dp[i][j][k] represents the number of stable arrays with i zeros and j ones
8 // where k indicates the last element type (0 for zero, 1 for one)
9 vector<vector<array<ll, 2>>> dp(zero + 1, vector<array<ll, 2>>(one + 1, {-1, -1}));
10
11 // Recursive function with memoization to count stable arrays
12 auto countStableArrays = [&](this auto&& countStableArrays, int zerosLeft, int onesLeft, int lastElement) -> ll {
13 // Base case: invalid state with negative counts
14 if (zerosLeft < 0 || onesLeft < 0) {
15 return 0;
16 }
17
18 // Base case: only ones remaining
19 if (zerosLeft == 0) {
20 // Valid if last element is 1 and ones count doesn't exceed limit
21 return lastElement == 1 && onesLeft <= limit;
22 }
23
24 // Base case: only zeros remaining
25 if (onesLeft == 0) {
26 // Valid if last element is 0 and zeros count doesn't exceed limit
27 return lastElement == 0 && zerosLeft <= limit;
28 }
29
30 // Check memoization
31 ll& result = dp[zerosLeft][onesLeft][lastElement];
32 if (result != -1) {
33 return result;
34 }
35
36 // Calculate based on the last element type
37 if (lastElement == 0) {
38 // Last element is 0, so we add a 0 to arrays ending with both 0 and 1
39 // Subtract invalid cases where consecutive 0s exceed limit
40 result = (countStableArrays(zerosLeft - 1, onesLeft, 0) +
41 countStableArrays(zerosLeft - 1, onesLeft, 1) -
42 countStableArrays(zerosLeft - limit - 1, onesLeft, 1) + MOD) % MOD;
43 } else {
44 // Last element is 1, so we add a 1 to arrays ending with both 0 and 1
45 // Subtract invalid cases where consecutive 1s exceed limit
46 result = (countStableArrays(zerosLeft, onesLeft - 1, 0) +
47 countStableArrays(zerosLeft, onesLeft - 1, 1) -
48 countStableArrays(zerosLeft, onesLeft - limit - 1, 0) + MOD) % MOD;
49 }
50
51 return result;
52 };
53
54 // Sum up arrays ending with 0 and arrays ending with 1
55 return (countStableArrays(zero, one, 0) + countStableArrays(zero, one, 1)) % MOD;
56 }
57};
58
1function numberOfStableArrays(zero: number, one: number, limit: number): number {
2 const MOD = 1e9 + 7;
3
4 // dp[i][j][k] represents the number of stable arrays with i zeros and j ones
5 // where k indicates the last element type (0 for zero, 1 for one)
6 const dp: number[][][] = Array(zero + 1)
7 .fill(null)
8 .map(() => Array(one + 1)
9 .fill(null)
10 .map(() => [-1, -1]));
11
12 // Recursive function with memoization to count stable arrays
13 const countStableArrays = (zerosLeft: number, onesLeft: number, lastElement: number): number => {
14 // Base case: invalid state with negative counts
15 if (zerosLeft < 0 || onesLeft < 0) {
16 return 0;
17 }
18
19 // Base case: only ones remaining
20 if (zerosLeft === 0) {
21 // Valid if last element is 1 and ones count doesn't exceed limit
22 return lastElement === 1 && onesLeft <= limit ? 1 : 0;
23 }
24
25 // Base case: only zeros remaining
26 if (onesLeft === 0) {
27 // Valid if last element is 0 and zeros count doesn't exceed limit
28 return lastElement === 0 && zerosLeft <= limit ? 1 : 0;
29 }
30
31 // Check memoization
32 if (dp[zerosLeft][onesLeft][lastElement] !== -1) {
33 return dp[zerosLeft][onesLeft][lastElement];
34 }
35
36 let result: number;
37
38 // Calculate based on the last element type
39 if (lastElement === 0) {
40 // Last element is 0, so we add a 0 to arrays ending with both 0 and 1
41 // Subtract invalid cases where consecutive 0s exceed limit
42 result = (countStableArrays(zerosLeft - 1, onesLeft, 0) +
43 countStableArrays(zerosLeft - 1, onesLeft, 1) -
44 countStableArrays(zerosLeft - limit - 1, onesLeft, 1) + MOD) % MOD;
45 } else {
46 // Last element is 1, so we add a 1 to arrays ending with both 0 and 1
47 // Subtract invalid cases where consecutive 1s exceed limit
48 result = (countStableArrays(zerosLeft, onesLeft - 1, 0) +
49 countStableArrays(zerosLeft, onesLeft - 1, 1) -
50 countStableArrays(zerosLeft, onesLeft - limit - 1, 0) + MOD) % MOD;
51 }
52
53 // Store result in memoization table
54 dp[zerosLeft][onesLeft][lastElement] = result;
55 return result;
56 };
57
58 // Sum up arrays ending with 0 and arrays ending with 1
59 return (countStableArrays(zero, one, 0) + countStableArrays(zero, one, 1)) % MOD;
60}
61
Time and Space Complexity
Time Complexity: O(zero * one * 2)
= O(zero * one)
The function uses memoization with @cache
decorator. The state space is defined by three parameters:
i
: ranges from0
tozero
(zero + 1 possible values)j
: ranges from0
toone
(one + 1 possible values)k
: can be either0
or1
(2 possible values)
Each unique state (i, j, k)
is computed at most once due to memoization. The total number of unique states is (zero + 1) * (one + 1) * 2
. For each state, the function performs constant time operations (basic arithmetic and at most 2 recursive calls that are either cached or lead to new states).
Therefore, the time complexity is O(zero * one * 2)
= O(zero * one)
.
Space Complexity: O(zero * one)
The space complexity consists of:
- Memoization cache: Stores all unique states visited, which is at most
(zero + 1) * (one + 1) * 2
=O(zero * one)
entries. - Recursion stack: The maximum recursion depth is
zero + one
in the worst case (when we decrement eitheri
orj
by 1 in each recursive call until both reach 0). This contributesO(zero + one)
to space complexity.
Since O(zero * one)
dominates O(zero + one)
for non-trivial inputs, the overall space complexity is O(zero * one)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Subtraction Logic for Consecutive Elements
The Pitfall:
A common mistake is misunderstanding why we subtract dfs(i - limit - 1, j, 1)
when the last digit is 0. Many developers incorrectly think this represents "having exactly limit+1 consecutive zeros," but it actually represents the count of arrays where we would create MORE than limit consecutive zeros.
Why This Happens:
When we have i
zeros left and the last placed digit was 0, calling dfs(i-1, j, 0)
means we're placing another 0. If we've already placed some zeros, and now we're adding the (limit+1)
-th consecutive zero, we're violating the constraint. The subtraction removes cases where placing all remaining zeros would create an invalid sequence.
Correct Understanding:
dfs(i-1, j, 0) + dfs(i-1, j, 1)
counts ALL ways to place remaining elementsdfs(i - limit - 1, j, 1)
counts the INVALID cases where we'd havelimit+1
consecutive zeros- The subtraction gives us only the VALID arrangements
2. Forgetting to Handle Negative Indices
The Pitfall:
Not checking if i - limit - 1 < 0
or j - limit - 1 < 0
before making the subtraction. This leads to incorrect recursive calls with negative parameters.
Solution:
if last_digit == 0: total = count_arrays(zeros_left - 1, ones_left, 0) + count_arrays(zeros_left - 1, ones_left, 1) # Only subtract if the index is valid if zeros_left - limit - 1 >= 0: total -= count_arrays(zeros_left - limit - 1, ones_left, 1)
3. Applying Modulo Operation Incorrectly
The Pitfall: Applying modulo only at the final return, not during intermediate calculations. This can cause integer overflow in languages with fixed integer sizes, or lead to incorrect results when subtracting.
Solution: Apply modulo after each operation to keep numbers manageable:
if last_digit == 0: total = (count_arrays(zeros_left - 1, ones_left, 0) + count_arrays(zeros_left - 1, ones_left, 1)) % MOD if zeros_left - limit - 1 >= 0: # Handle potential negative results from subtraction total = (total - count_arrays(zeros_left - limit - 1, ones_left, 1) + MOD) % MOD
4. Misunderstanding the Initial Call
The Pitfall:
Thinking that dfs(zero, one, 0)
means "start the array with a 0." It actually means "we have zero zeros and one ones left to place, and the last placed digit was 0."
Correct Interpretation:
dfs(zero, one, 0)
represents: "Count arrays where we need to place 'zero' zeros and 'one' ones, assuming the previous (imaginary) digit was 0"dfs(zero, one, 1)
represents: "Count arrays where we need to place 'zero' zeros and 'one' ones, assuming the previous (imaginary) digit was 1"- The sum gives us all possible valid arrays regardless of what the first actual digit is
5. Memory Leak with @cache
The Pitfall: Not clearing the cache after computation, which can cause memory issues when the function is called multiple times (e.g., in a testing environment).
Solution: Always clear the cache after getting the result:
result = (count_arrays(zero, one, 0) + count_arrays(zero, one, 1)) % MOD count_arrays.cache_clear() # Important for memory management return result
Which of the following shows the order of node visit in a Breadth-first Search?
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
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
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!