3129. Find All Possible Stable Binary Arrays I
Problem Description
You need to count the number of stable binary arrays given three positive integers: zero
, one
, and limit
.
A binary array arr
is considered stable if it meets 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 both 0 and 1
The task is to find how many different stable binary arrays can be formed with these constraints. Since the result can be very large, return it modulo 10^9 + 7
.
For example, if you have zero = 2
, one = 1
, and limit = 1
, you need to create arrays with exactly 2 zeros and 1 one. The third condition means that any subarray of length greater than 1 must have both 0 and 1 in it. This prevents having consecutive runs of the same digit that are too long.
The solution uses dynamic programming with memoization. The function dfs(i, j, k)
represents the number of valid arrays when there are i
zeros and j
ones remaining to place, with k
indicating what the next element to place should be (0 or 1).
The key insight is that when placing a digit, we need to ensure we don't create a subarray of length limit + 1
containing only that digit. This is handled by subtracting invalid configurations where we would have more than limit
consecutive identical digits.
Intuition
The key observation is that we need to build arrays where no consecutive sequence of the same digit exceeds limit
in length. This suggests we should think about the problem recursively - at each position, we decide whether to place a 0 or 1, while keeping track of constraints.
Let's think about this step by step. When building a valid array, at any point we need to know:
- How many 0s we still need to place
- How many 1s we still need to place
- What digit we're about to place next
Why does the last point matter? Because if we're placing a 0, we need to ensure we don't create a run of 0s longer than limit
. The same applies for 1s.
Now here's the clever part: instead of tracking the entire array built so far, we can use inclusion-exclusion principle. When we place a digit k
, the total valid arrangements equals:
- All arrangements where we place
k
at this position - MINUS the arrangements that would create an invalid run of length
limit + 1
For example, if k = 0
, we count all ways to place this 0 and continue building (dfs(i-1, j, 0)
+ dfs(i-1, j, 1)
). But then we subtract cases where placing this 0 would create a run of limit + 1
consecutive 0s. This happens when the last limit + 1
positions are all 0s, which we can calculate as dfs(i - limit - 1, j, 1)
- representing the state after placing limit + 1
zeros in a row.
The base cases are straightforward: when we have only one type of digit left, we can place them all consecutively only if their count doesn't exceed limit
. Otherwise, we'd violate the constraint about maximum consecutive digits.
This approach elegantly handles the constraint without explicitly tracking consecutive counts, making the solution both efficient and clean.
Learn more about Dynamic Programming and Prefix Sum patterns.
Solution Approach
The solution uses memoization (dynamic programming with caching) to efficiently count valid stable arrays. Let's break down the implementation:
Core Function: dfs(i, j, k)
The function represents the number of stable arrays when:
i
= number of 0s remaining to placej
= number of 1s remaining to placek
= the next digit to place (0 or 1)
Base Cases
-
When only 1s remain (
i = 0
):- Return
1
ifk = 1
andj ≤ limit
(we can place all remaining 1s consecutively) - Otherwise return
0
- Return
-
When only 0s remain (
j = 0
):- Return
1
ifk = 0
andi ≤ limit
(we can place all remaining 0s consecutively) - Otherwise return
0
- Return
Recursive Cases with Inclusion-Exclusion
When placing a 0 (k = 0
):
result = dfs(i - 1, j, 0) + dfs(i - 1, j, 1) - invalid_cases
dfs(i - 1, j, 0)
: Continue with another 0dfs(i - 1, j, 1)
: Switch to placing a 1- Subtract invalid cases where we'd have
limit + 1
consecutive 0s:- This occurs when
i - limit - 1 ≥ 0
- Invalid count =
dfs(i - limit - 1, j, 1)
- This occurs when
When placing a 1 (k = 1
):
result = dfs(i, j - 1, 0) + dfs(i, j - 1, 1) - invalid_cases
dfs(i, j - 1, 0)
: Switch to placing a 0dfs(i, j - 1, 1)
: Continue with another 1- Subtract invalid cases where we'd have
limit + 1
consecutive 1s:- This occurs when
j - limit - 1 ≥ 0
- Invalid count =
dfs(i, j - limit - 1, 0)
- This occurs when
Final Answer
The total number of stable arrays is:
(dfs(zero, one, 0) + dfs(zero, one, 1)) % (10^9 + 7)
This accounts for arrays starting with either 0 or 1. The @cache
decorator ensures each state is computed only once, giving us an efficient O(zero × one × 2) time complexity with O(zero × one × 2) space for memoization.
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 = 2
, one = 1
, and limit = 1
.
We need to find all arrays with exactly 2 zeros and 1 one, where no subarray of length > 1 can have only 0s or only 1s. This means we cannot have consecutive identical digits.
Starting the recursion:
- Total =
dfs(2, 1, 0) + dfs(2, 1, 1)
Evaluating dfs(2, 1, 0)
- placing a 0 first:
- We place a 0, leaving us with 1 zero and 1 one remaining
- Result =
dfs(1, 1, 0) + dfs(1, 1, 1) - invalid_cases
- Since
2 - 1 - 1 = 0 ≥ 0
, we subtractdfs(0, 1, 1)
(placing 2 consecutive 0s) dfs(0, 1, 1)
: Only 1s remain, and we're placing a 1. Since1 ≤ limit
, return 1- So:
dfs(2, 1, 0) = dfs(1, 1, 0) + dfs(1, 1, 1) - 1
Evaluating dfs(1, 1, 0)
- placing another 0:
- Result =
dfs(0, 1, 0) + dfs(0, 1, 1) - invalid_cases
- Since
1 - 1 - 1 = -1 < 0
, no invalid cases to subtract dfs(0, 1, 0)
: Only 1s remain but we're placing a 0, return 0dfs(0, 1, 1)
: Only 1s remain and we're placing a 1, return 1- So:
dfs(1, 1, 0) = 0 + 1 = 1
Evaluating dfs(1, 1, 1)
- switching to place a 1:
- Result =
dfs(1, 0, 0) + dfs(1, 0, 1) - invalid_cases
- Since
1 - 1 - 1 = -1 < 0
, no invalid cases to subtract dfs(1, 0, 0)
: Only 0s remain and we're placing a 0, return 1dfs(1, 0, 1)
: Only 0s remain but we're placing a 1, return 0- So:
dfs(1, 1, 1) = 1 + 0 = 1
Back to dfs(2, 1, 0)
:
dfs(2, 1, 0) = 1 + 1 - 1 = 1
Evaluating dfs(2, 1, 1)
- placing a 1 first:
- Result =
dfs(2, 0, 0) + dfs(2, 0, 1) - invalid_cases
- Since
1 - 1 - 1 = -1 < 0
, no invalid cases to subtract dfs(2, 0, 0)
: Only 0s remain and we're placing a 0. Since2 > limit
, return 0dfs(2, 0, 1)
: Only 0s remain but we're placing a 1, return 0- So:
dfs(2, 1, 1) = 0 + 0 = 0
Final answer:
- Total =
dfs(2, 1, 0) + dfs(2, 1, 1) = 1 + 0 = 1
The only valid array is [0, 1, 0], which satisfies all constraints:
- Has exactly 2 zeros and 1 one
- No two consecutive identical digits (respecting the limit of 1)
Solution Implementation
1class Solution:
2 def numberOfStableArrays(self, zero: int, one: int, limit: int) -> int:
3 """
4 Count the number of stable arrays with 'zero' zeros and 'one' ones,
5 where no more than 'limit' consecutive identical elements are allowed.
6
7 Args:
8 zero: Number of zeros to place in the array
9 one: Number of ones to place in the array
10 limit: Maximum consecutive identical elements allowed
11
12 Returns:
13 Number of valid stable arrays modulo 10^9 + 7
14 """
15 from functools import cache
16
17 MOD = 10**9 + 7
18
19 @cache
20 def count_arrays(zeros_left: int, ones_left: int, last_element: int) -> int:
21 """
22 Dynamic programming function to count valid arrays.
23
24 Args:
25 zeros_left: Remaining zeros to place
26 ones_left: Remaining ones to place
27 last_element: Last element placed (0 for zero, 1 for one)
28
29 Returns:
30 Number of valid arrays from this state
31 """
32 # Base case: no zeros left
33 if zeros_left == 0:
34 # Valid only if last element was 1 and remaining ones don't exceed limit
35 return 1 if (last_element == 1 and ones_left <= limit) else 0
36
37 # Base case: no ones left
38 if ones_left == 0:
39 # Valid only if last element was 0 and remaining zeros don't exceed limit
40 return 1 if (last_element == 0 and zeros_left <= limit) else 0
41
42 # If last element was 0, we're placing another 0
43 if last_element == 0:
44 # Add one more 0: sum of all ways to place previous element (0 or 1)
45 total_ways = count_arrays(zeros_left - 1, ones_left, 0) + \
46 count_arrays(zeros_left - 1, ones_left, 1)
47
48 # Subtract invalid cases where we exceed limit consecutive 0s
49 # This happens when we have more than limit consecutive 0s
50 if zeros_left - limit - 1 >= 0:
51 total_ways -= count_arrays(zeros_left - limit - 1, ones_left, 1)
52
53 return total_ways
54
55 # If last element was 1, we're placing another 1
56 else:
57 # Add one more 1: sum of all ways to place previous element (0 or 1)
58 total_ways = count_arrays(zeros_left, ones_left - 1, 0) + \
59 count_arrays(zeros_left, ones_left - 1, 1)
60
61 # Subtract invalid cases where we exceed limit consecutive 1s
62 # This happens when we have more than limit consecutive 1s
63 if ones_left - limit - 1 >= 0:
64 total_ways -= count_arrays(zeros_left, ones_left - limit - 1, 0)
65
66 return total_ways
67
68 # Calculate total: arrays ending with 0 plus arrays ending with 1
69 result = (count_arrays(zero, one, 0) + count_arrays(zero, one, 1)) % MOD
70
71 # Clear cache to free memory
72 count_arrays.cache_clear()
73
74 return result
75
1class Solution {
2 // Modulo value for large number calculations
3 private final int MOD = (int) 1e9 + 7;
4
5 // Memoization table: dp[zeros][ones][lastDigit]
6 // lastDigit: 0 means last digit is 0, 1 means last digit is 1
7 private Long[][][] dp;
8
9 // Maximum consecutive same digits 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 result = (dfs(zero, one, 0) + dfs(zero, one, 1)) % MOD;
19 return (int) result;
20 }
21
22 /**
23 * Dynamic programming with memoization to count stable arrays
24 * @param zerosLeft - number of zeros remaining to place
25 * @param onesLeft - number of ones remaining to place
26 * @param lastDigit - the last digit placed (0 or 1)
27 * @return number of valid stable arrays with given constraints
28 */
29 private long dfs(int zerosLeft, int onesLeft, int lastDigit) {
30 // Base case: invalid state with negative counts
31 if (zerosLeft < 0 || onesLeft < 0) {
32 return 0;
33 }
34
35 // Base case: only zeros left
36 if (zerosLeft == 0) {
37 // Valid only if last digit is 1 and remaining ones don't exceed limit
38 return (lastDigit == 1 && onesLeft <= maxConsecutive) ? 1 : 0;
39 }
40
41 // Base case: only ones left
42 if (onesLeft == 0) {
43 // Valid only if last digit is 0 and remaining zeros don't exceed limit
44 return (lastDigit == 0 && zerosLeft <= maxConsecutive) ? 1 : 0;
45 }
46
47 // Return memoized result if already calculated
48 if (dp[zerosLeft][onesLeft][lastDigit] != null) {
49 return dp[zerosLeft][onesLeft][lastDigit];
50 }
51
52 // Calculate result based on last digit placed
53 if (lastDigit == 0) {
54 // If last digit is 0, we're placing a 0 at current position
55 // Total = arrays with one less 0 ending in 0 + arrays with one less 0 ending in 1
56 // Subtract invalid arrays where we would have more than limit consecutive 0s
57 long totalArrays = dfs(zerosLeft - 1, onesLeft, 0) + dfs(zerosLeft - 1, onesLeft, 1);
58 long invalidArrays = dfs(zerosLeft - maxConsecutive - 1, onesLeft, 1);
59 dp[zerosLeft][onesLeft][lastDigit] = (totalArrays - invalidArrays + MOD) % MOD;
60 } else {
61 // If last digit is 1, we're placing a 1 at current position
62 // Total = arrays with one less 1 ending in 0 + arrays with one less 1 ending in 1
63 // Subtract invalid arrays where we would have more than limit consecutive 1s
64 long totalArrays = dfs(zerosLeft, onesLeft - 1, 0) + dfs(zerosLeft, onesLeft - 1, 1);
65 long invalidArrays = dfs(zerosLeft, onesLeft - maxConsecutive - 1, 0);
66 dp[zerosLeft][onesLeft][lastDigit] = (totalArrays - invalidArrays + MOD) % MOD;
67 }
68
69 return dp[zerosLeft][onesLeft][lastDigit];
70 }
71}
72
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, j ones
8 // where k indicates the last element type (0 for zero, 1 for one)
9 // Initialize with -1 for memoization
10 vector<vector<array<ll, 2>>> dp(zero + 1, vector<array<ll, 2>>(one + 1, {-1, -1}));
11
12 // Recursive function with memoization to count stable arrays
13 auto countStableArrays = [&](this auto&& countStableArrays, int zerosLeft, int onesLeft, int lastType) -> ll {
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 consecutive ones don't exceed limit
22 return lastType == 1 && onesLeft <= limit;
23 }
24
25 // Base case: only zeros remaining
26 if (onesLeft == 0) {
27 // Valid if last element is 0 and consecutive zeros don't exceed limit
28 return lastType == 0 && zerosLeft <= limit;
29 }
30
31 // Check memoization
32 ll& result = dp[zerosLeft][onesLeft][lastType];
33 if (result != -1) {
34 return result;
35 }
36
37 // Calculate result based on the last element type
38 if (lastType == 0) {
39 // Last element is 0, so we add a 0 to previous arrays
40 // Sum arrays ending with 0 and arrays ending with 1
41 // Subtract invalid cases where adding this 0 exceeds the 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 previous arrays
47 // Sum arrays ending with 0 and arrays ending with 1
48 // Subtract invalid cases where adding this 1 exceeds the limit
49 result = (countStableArrays(zerosLeft, onesLeft - 1, 0) +
50 countStableArrays(zerosLeft, onesLeft - 1, 1) -
51 countStableArrays(zerosLeft, onesLeft - limit - 1, 0) + MOD) % MOD;
52 }
53
54 return result;
55 };
56
57 // Calculate total by considering arrays ending with 0 and arrays ending with 1
58 return (countStableArrays(zero, one, 0) + countStableArrays(zero, one, 1)) % MOD;
59 }
60};
61
1/**
2 * Calculates the number of stable arrays with given zeros, ones, and limit constraint
3 * @param zero - Number of zeros to place
4 * @param one - Number of ones to place
5 * @param limit - Maximum consecutive elements of the same type allowed
6 * @returns Number of valid stable arrays modulo 10^9 + 7
7 */
8function numberOfStableArrays(zero: number, one: number, limit: number): number {
9 const MOD = 1e9 + 7;
10
11 // Create 3D memoization array: [zeros count][ones count][last element type (0 or 1)]
12 // Initialize with -1 to indicate uncomputed states
13 const memoization: number[][][] = Array.from(
14 { length: zero + 1 },
15 () => Array.from(
16 { length: one + 1 },
17 () => [-1, -1]
18 )
19 );
20
21 /**
22 * Dynamic programming with memoization to count valid arrays
23 * @param zerosRemaining - Number of zeros left to place
24 * @param onesRemaining - Number of ones left to place
25 * @param lastElement - Type of the last element placed (0 for zero, 1 for one)
26 * @returns Number of valid arrays from this state
27 */
28 const countValidArrays = (zerosRemaining: number, onesRemaining: number, lastElement: number): number => {
29 // Invalid state: negative count of elements
30 if (zerosRemaining < 0 || onesRemaining < 0) {
31 return 0;
32 }
33
34 // Base case: all zeros used, check if remaining ones form valid sequence
35 if (zerosRemaining === 0) {
36 return (lastElement === 1 && onesRemaining <= limit) ? 1 : 0;
37 }
38
39 // Base case: all ones used, check if remaining zeros form valid sequence
40 if (onesRemaining === 0) {
41 return (lastElement === 0 && zerosRemaining <= limit) ? 1 : 0;
42 }
43
44 // Check memoization cache
45 let result = memoization[zerosRemaining][onesRemaining][lastElement];
46 if (result !== -1) {
47 return result;
48 }
49
50 // Calculate result based on last element type
51 if (lastElement === 0) {
52 // Last element was 0, so we're adding more zeros
53 // Sum arrays ending with any number of consecutive zeros (1 to limit)
54 // Subtract invalid cases where we would have more than limit consecutive zeros
55 result = (
56 countValidArrays(zerosRemaining - 1, onesRemaining, 0) +
57 countValidArrays(zerosRemaining - 1, onesRemaining, 1) -
58 countValidArrays(zerosRemaining - limit - 1, onesRemaining, 1) +
59 MOD
60 ) % MOD;
61 } else {
62 // Last element was 1, so we're adding more ones
63 // Sum arrays ending with any number of consecutive ones (1 to limit)
64 // Subtract invalid cases where we would have more than limit consecutive ones
65 result = (
66 countValidArrays(zerosRemaining, onesRemaining - 1, 0) +
67 countValidArrays(zerosRemaining, onesRemaining - 1, 1) -
68 countValidArrays(zerosRemaining, onesRemaining - limit - 1, 0) +
69 MOD
70 ) % MOD;
71 }
72
73 // Store in cache and return
74 memoization[zerosRemaining][onesRemaining][lastElement] = result;
75 return result;
76 };
77
78 // Calculate total by considering arrays ending with either 0 or 1
79 return (countValidArrays(zero, one, 0) + countValidArrays(zero, one, 1)) % MOD;
80}
81
Time and Space Complexity
Time Complexity: O(zero * one * limit)
The time complexity is determined by the memoized recursive function dfs(i, j, k)
:
- Parameter
i
can take values from0
tozero
(inclusive), giving uszero + 1
possible values - Parameter
j
can take values from0
toone
(inclusive), giving usone + 1
possible values - Parameter
k
can only be0
or1
, giving us2
possible values
Each unique state (i, j, k)
is computed at most once due to the @cache
decorator. The total number of unique states is (zero + 1) * (one + 1) * 2
.
However, when analyzing the recursive calls more carefully:
- Each state makes at most 3 recursive calls (2 additions and 1 subtraction)
- The subtraction involves jumping back by
limit + 1
positions
The effective time complexity is O(zero * one)
for the state space, but considering that each state might need to look back up to limit
positions in certain cases, the overall time complexity is O(zero * one * limit)
.
Space Complexity: O(zero * one)
The space complexity consists of:
- Cache storage: The memoization cache stores all computed states. Since there are
(zero + 1) * (one + 1) * 2
possible states, the cache requiresO(zero * one)
space - Recursion stack: In the worst case, the recursion depth can go up to
O(zero + one)
when we traverse from(zero, one, k)
down to base cases
The dominant factor is the cache storage, giving us a total space complexity of O(zero * one)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Understanding of the Inclusion-Exclusion Logic
The most common pitfall is misunderstanding why we subtract dfs(i - limit - 1, j, 1)
when placing zeros (or the equivalent for ones). Many developers incorrectly think this represents "placing exactly limit+1 consecutive zeros," but it actually represents "having at least limit+1 consecutive zeros ending at the current position."
Why this matters: When we compute dfs(i-1, j, 0) + dfs(i-1, j, 1)
, we're counting all ways to build arrays. Some of these ways will violate the constraint by having limit+1 or more consecutive zeros. The subtraction removes these invalid configurations.
Solution: Think of it as: "If we place a zero now and had already placed limit zeros before this, we're creating an invalid sequence of limit+1 consecutive zeros. The number of such invalid sequences equals the number of ways to place the remaining elements after forcing limit+1 consecutive zeros."
2. Forgetting to Handle Negative Indices in Subtraction
A critical bug occurs when developers forget to check if i - limit - 1 >= 0
before subtracting invalid cases. Without this check, you might call dfs(-1, j, 1)
which either causes an error or returns incorrect results.
Incorrect code:
# Missing boundary check - will cause errors! total_ways -= count_arrays(zeros_left - limit - 1, ones_left, 1)
Correct code:
if zeros_left - limit - 1 >= 0: total_ways -= count_arrays(zeros_left - limit - 1, ones_left, 1)
3. Applying Modulo Operation Incorrectly
Another common mistake is applying the modulo operation at the wrong place or forgetting that subtraction can produce negative numbers in modular arithmetic.
Incorrect approach:
# This can produce negative results! return (total_ways - invalid_cases) % MOD
Correct approach:
# Add MOD before taking modulo to handle negative results return ((total_ways - invalid_cases) % MOD + MOD) % MOD
Or handle it at the final step only since Python handles negative modulo correctly:
# Python's modulo handles negatives correctly result = (count_arrays(zero, one, 0) + count_arrays(zero, one, 1)) % MOD
4. Misunderstanding the Parameter k
(last_element)
Some developers confuse k
as "the element we just placed" rather than "the element we're about to place." This leads to inverted logic in the recursive calls.
Wrong interpretation: If k=0
, we just placed a 0, so now we decide what to place next.
Correct interpretation: If k=0
, we are about to place a 0, and the recursive calls determine what came before it in the array construction process.
5. Incorrect Base Cases
A subtle error is returning 1 whenever one type of digit runs out, without checking if the remaining digits can be placed consecutively within the limit.
Incorrect base case:
if zeros_left == 0: return 1 # Wrong! What if ones_left > limit?
Correct base case:
if zeros_left == 0: return 1 if (last_element == 1 and ones_left <= limit) else 0
The check ensures that:
- We're continuing with the same type of element (maintaining consistency)
- The remaining count doesn't exceed the limit for consecutive elements
How does merge sort divide the problem into subproblems?
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!