3129. Find All Possible Stable Binary Arrays I
Problem Description
You are given 3 positive integers zero
, one
, and limit
.
A binary array arr
is called stable if:
- The number of occurrences of 0 in
arr
is exactlyzero
. - The number of occurrences of 1 in
arr
is exactlyone
. - Each subarray of
arr
with a size greater thanlimit
must contain both 0 and 1.
Return the total number of stable binary arrays.
Since the answer may be very large, return it modulo (10^9 + 7).
Intuition
The problem asks for counting binary arrays that meet specific conditions on the number of 0s and 1s and the composition of subarrays beyond a certain size. This naturally lends itself to a dynamic programming or recursive solution, where we attempt to build the solution progressively while ensuring stability at every step.
To solve this, we employ a memoization technique combined with depth-first search (DFS). We define a recursive function dfs(i, j, k)
, where:
i
indicates how many 0s are left to place.j
indicates how many 1s are left to place.k
indicates the next number to be placed, either 0 or 1.
The recursive formulation is structured as follows:
-
When
i
is 0, we can only place 1s, and we check if placing them maintains the stability requirement. -
When
j
is 0, only 0s can be placed, similarly ensuring the stability condition is met. -
If
k
is 0, we need to evaluate two scenarios:- The previous number placed could be 0, giving rise to a call
dfs(i - 1, j, 0)
. - The previous number could have been 1, leading to a call
dfs(i - 1, j, 1)
.
For the stable condition, if placing another 0 would cause a subarray of more than
limit
length to consist solely of 0s, we must backtrack and remove the invalid count. - The previous number placed could be 0, giving rise to a call
By memoizing these subproblems, we effectively manage the potential overlap and ensure efficiency. The final result is derived by combining the counts from initiating with either a 0 or a 1, modulo (10^9 + 7).
Learn more about Dynamic Programming and Prefix Sum patterns.
Solution Approach
The solution employs a dynamic programming approach through memoization combined with depth-first search to efficiently count the number of stable binary arrays. Here's a breakdown of the implemented algorithm:
-
Recursive Function Definition:
- We define a recursive function
dfs(i, j, k)
:i
represents the remaining count of 0s to place.j
represents the remaining count of 1s to place.k
indicates the type of number to be placed next (0 or 1).
- We define a recursive function
-
Base Conditions:
- If
i
is 0, meaning we have no more 0s to place, check if placing only 1s would violate the subarraylimit
constraint. Only return a count if placing the remaining 1s directly is valid. - If
j
is 0, verify if placing only 0s doesn't breach the subarray constraint. Return a count only if valid placement of the remaining 0s is feasible.
- If
-
Recursive Case:
- When the next number to place is 0 (
k = 0
):- Call
dfs(i - 1, j, 0)
for placing another 0. - Call
dfs(i - 1, j, 1)
for placing a 1 instead and switching the number type. - Adjust counts to ensure that any longer subarrays formed do not contain more than
limit
consecutive 0s by subtracting unnecessary recursive calls.
- Call
- When the next number to be placed is 1 (
k = 1
), employ a similar strategy:- Call
dfs(i, j - 1, 0)
anddfs(i, j - 1, 1)
. - Ensure not exceeding
limit
consecutive 1s in the subarrays by adjusting recursion results.
- Call
- When the next number to place is 0 (
-
Memoization:
- The
@cache
decorator is utilized to store previously computed results for specific states(i, j, k)
, reducing redundant computations and improving efficiency.
- The
-
Final Result:
- The solution is computed by adding results from starting with either 0 or 1:
(dfs(zero, one, 0) + dfs(zero, one, 1)) % (10^9 + 7)
to account for large numbers. - Clear the cache post calculation with
dfs.cache_clear()
to avoid memory issues.
- The solution is computed by adding results from starting with either 0 or 1:
The algorithm carefully constructs all possible binary arrays adhering to the given constraints, leveraging memoization to ensure efficiency in handling overlapping subproblems.
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 consider a small example to understand the solution approach for counting stable binary arrays. Suppose we have:
zero = 2
: The binary array must contain exactly 2 zeros.one = 2
: The binary array must contain exactly 2 ones.limit = 2
: Any subarray longer than 2 elements must contain both 0 and 1.
Our goal is to count all possible "stable" binary arrays meeting these criteria.
Step-by-step Breakdown:
-
Initial State: We start by considering two primary options: beginning the array with either a 0 or a 1.
-
Recursive Exploration:
-
Starting with 0:
- Place a 0, resulting in one less zero to place. This leads us into state
(1, 2, 1)
, where 1 denotes the next number should be 1. - Follow with a 1, resulting in state
(1, 1, 0)
. Now we have one 0 and one 1 remaining. - We now consider the placements from two possible endings:
- Follow with another 0, reaching
(0, 1, 1)
. - Or a 1, resulting in
(1, 0, 0)
.
- Follow with another 0, reaching
- Check each case:
- If
(0, 1, 1)
: Only a 1 can be placed next, consistent with ending the array, fulfilling stability. - If
(1, 0, 0)
: Only a 0 can be placed next; this satisfies stability.
- If
- Place a 0, resulting in one less zero to place. This leads us into state
-
Starting with 1:
- Place a 1 to reach state
(2, 1, 0)
. - Place a 0, resulting in
(1, 1, 1)
. - Continue by ending with placements from two options:
- Follow with another 1, reaching
(1, 0, 0)
. - Or a 0, leading to
(0, 1, 1)
.
- Follow with another 1, reaching
- Verify each arrangement:
- For
(1, 0, 0)
: A 0 can be placed next respectfully attaining stability. - For
(0, 1, 1)
: Place a 1 respecting the constraints.
- For
- Place a 1 to reach state
-
-
Memoization and Counting: Each transition point along these paths is cached. It avoids recomputing identical states, streamlines solving.
-
Result Aggregation: Finally, the number of valid stable arrays is derived by combining outcomes starting with 0 or 1. Each sum is taken modulo (10^9 + 7).
For this example, valid stable arrays can be [0, 1, 0, 1]
, [0, 1, 1, 0]
, [1, 0, 1, 0]
, and [1, 0, 0, 1]
. Using the recursive and memoized approach efficiently evaluates all feasible configurations.
Solution Implementation
1from functools import cache
2
3class Solution:
4 def numberOfStableArrays(self, zero: int, one: int, limit: int) -> int:
5 # Memoized recursive function to find the number of stable arrays
6 @cache
7 def dfs(i: int, j: int, k: int) -> int:
8 # Base case: if no zeros left, check if the remaining ones fit within the limit
9 if i == 0:
10 return int(k == 1 and j <= limit)
11 # Base case: if no ones left, check if the remaining zeros fit within the limit
12 if j == 0:
13 return int(k == 0 and i <= limit)
14
15 # Case when the last added element is zero
16 if k == 0:
17 # Possible next steps: add zero or add one, subtract over-counted solutions
18 return (
19 dfs(i - 1, j, 0) # Add another zero
20 + dfs(i - 1, j, 1) # Switch to adding one
21 - (0 if i - limit - 1 < 0 else dfs(i - limit - 1, j, 1)) # Adjust for constraint violations
22 )
23
24 # Case when the last added element is one
25 return (
26 dfs(i, j - 1, 0) # Add a zero
27 + dfs(i, j - 1, 1) # Add another one
28 - (0 if j - limit - 1 < 0 else dfs(i, j - limit - 1, 0)) # Adjust for constraint violations
29 )
30
31 mod = 10**9 + 7 # Define the modulus for the final answer
32 # Calculate the number of stable arrays starting with either a zero or a one
33 ans = (dfs(zero, one, 0) + dfs(zero, one, 1)) % mod
34 dfs.cache_clear() # Clear cache after computation
35 return ans
36
1class Solution {
2 private static final int MOD = (int) 1e9 + 7; // constant for modulo computations
3 private Long[][][] dp; // memoization array
4 private int limit;
5
6 /**
7 * Computes the number of stable arrays consisting of a given number of zeros and ones,
8 * with a specific subsequence length limit.
9 *
10 * @param zero number of zero elements available
11 * @param one number of one elements available
12 * @param limit maximum length of subsequence consisting of all 1s or all 0s
13 * @return number of stable arrays that can be formed
14 */
15 public int numberOfStableArrays(int zero, int one, int limit) {
16 dp = new Long[zero + 1][one + 1][2]; // initialize the memoization array
17 this.limit = limit; // store limit for use in recursive method
18 // Compute results starting with either 0 or 1 and sum them
19 return (int) ((dfs(zero, one, 0) + dfs(zero, one, 1)) % MOD);
20 }
21
22 /**
23 * Recursive helper function with memoization to count stable arrays.
24 *
25 * @param i remaining zero elements
26 * @param j remaining one elements
27 * @param k last element type (0 for zero, 1 for one)
28 * @return number of stable arrays that can be formed
29 */
30 private long dfs(int i, int j, int k) {
31 // If more zeros or ones than available, return 0
32 if (i < 0 || j < 0) {
33 return 0;
34 }
35 // Base case: check if it's valid to end with a 1 and enough 1s for spacing
36 if (i == 0) {
37 return k == 1 && j <= limit ? 1 : 0;
38 }
39 // Base case: check if it's valid to end with a 0 and there are enough 0s
40 if (j == 0) {
41 return k == 0 && i <= limit ? 1 : 0;
42 }
43 // If already computed, return cached value
44 if (dp[i][j][k] != null) {
45 return dp[i][j][k];
46 }
47 // Calculate possible arrays:
48 // If last number is a 0, decrement zero count and consider both next options
49 if (k == 0) {
50 dp[i][j][k]
51 = (dfs(i - 1, j, 0) + dfs(i - 1, j, 1) - dfs(i - limit - 1, j, 1) + MOD) % MOD;
52 }
53 // If last number is a 1, decrement one count and consider both next options
54 else {
55 dp[i][j][k]
56 = (dfs(i, j - 1, 0) + dfs(i, j - 1, 1) - dfs(i, j - limit - 1, 0) + MOD) % MOD;
57 }
58 return dp[i][j][k];
59 }
60}
61
1#include <vector>
2#include <array>
3
4class Solution {
5public:
6 int numberOfStableArrays(int zero, int one, int limit) {
7 const int MODULO = 1e9 + 7; // Define the modulo for results
8 using ll = long long; // Define an alias for long long
9
10 // Initialize a 3D vector to store intermediate results for the dynamic programming approach
11 std::vector<std::vector<std::array<ll, 2>>> dp(zero + 1, std::vector<std::array<ll, 2>>(one + 1, { -1, -1 }));
12
13 // Recursive lambda for depth-first search with memoization
14 auto dfs = [&](auto &&dfs, int remainingZeros, int remainingOnes, int currentState) -> ll {
15 if (remainingZeros < 0 || remainingOnes < 0) {
16 return 0; // Base case: negative indices are invalid
17 }
18 if (remainingZeros == 0) {
19 return currentState == 1 && remainingOnes <= limit; // Check if current state satisfies the limit
20 }
21 if (remainingOnes == 0) {
22 return currentState == 0 && remainingZeros <= limit; // Check if current state satisfies the limit
23 }
24
25 // Reference to the result stored in dp for current state
26 ll &result = dp[remainingZeros][remainingOnes][currentState];
27 if (result != -1) {
28 return result; // If result is already calculated, return it to avoid recomputation
29 }
30
31 // Calculate the result based on current state
32 if (currentState == 0) {
33 // If in state 0 (zero), try transitioning from previous state
34 result = (dfs(dfs, remainingZeros - 1, remainingOnes, 0) +
35 dfs(dfs, remainingZeros - 1, remainingOnes, 1) -
36 dfs(dfs, remainingZeros - limit - 1, remainingOnes, 1) + MODULO) % MODULO;
37 } else {
38 // If in state 1 (one), try transitioning from previous state
39 result = (dfs(dfs, remainingZeros, remainingOnes - 1, 0) +
40 dfs(dfs, remainingZeros, remainingOnes - 1, 1) -
41 dfs(dfs, remainingZeros, remainingOnes - limit - 1, 0) + MODULO) % MODULO;
42 }
43
44 return result; // Return the calculated result
45 };
46
47 // Compute and return the total number of stable arrays by summing over all initial states
48 return (dfs(dfs, zero, one, 0) + dfs(dfs, zero, one, 1)) % MODULO;
49 }
50};
51
1// Function to calculate the number of stable arrays based on given parameters.
2function numberOfStableArrays(zero: number, one: number, limit: number): number {
3 const MODULO = 1e9 + 7;
4
5 // 3-dimensional array to store intermediate results for memoization.
6 const dp: number[][][] = Array.from({ length: zero + 1 }, () =>
7 Array.from({ length: one + 1 }, () => [-1, -1])
8 );
9
10 // Depth-first search function with memoization.
11 const dfs = (remainingZero: number, remainingOne: number, lastDigit: number): number => {
12 // Base case: if there are more zeros or ones used than allowed, return 0.
13 if (remainingZero < 0 || remainingOne < 0) {
14 return 0;
15 }
16
17 // Base case: if no more zeros are needed, check validity.
18 if (remainingZero === 0) {
19 return lastDigit === 1 && remainingOne <= limit ? 1 : 0;
20 }
21
22 // Base case: if no more ones are needed, check validity.
23 if (remainingOne === 0) {
24 return lastDigit === 0 && remainingZero <= limit ? 1 : 0;
25 }
26
27 // Check if result is already computed and stored in dp table.
28 let result = dp[remainingZero][remainingOne][lastDigit];
29 if (result !== -1) {
30 return result;
31 }
32
33 // Calculate result based on the last used digit.
34 if (lastDigit === 0) {
35 // Compute possibilities starting with 0 as last digit.
36 result = (
37 dfs(remainingZero - 1, remainingOne, 0) +
38 dfs(remainingZero - 1, remainingOne, 1) -
39 dfs(remainingZero - limit - 1, remainingOne, 1) +
40 MODULO
41 ) % MODULO;
42 } else {
43 // Compute possibilities starting with 1 as last digit.
44 result = (
45 dfs(remainingZero, remainingOne - 1, 0) +
46 dfs(remainingZero, remainingOne - 1, 1) -
47 dfs(remainingZero, remainingOne - limit - 1, 0) +
48 MODULO
49 ) % MODULO;
50 }
51
52 // Store the computed result in the dp table.
53 return (dp[remainingZero][remainingOne][lastDigit] = result);
54 };
55
56 // Calculate the total number of stable arrays by starting from both possible last digits (0 and 1).
57 return (dfs(zero, one, 0) + dfs(zero, one, 1)) % MODULO;
58}
59
Time and Space Complexity
The time complexity of the provided numberOfStableArrays
function is determined by the recursive depth of the dfs
function with caching. The state of the recursive calls is defined by three parameters: i
, j
, and k
. This results in a time complexity of O(zero * one * 2)
, which simplifies to O(zero * one)
, since the third parameter k
can only be 0 or 1, indicating two possible states for each pair of (i, j)
.
The space complexity is mainly due to the storage of memoized states in the caching mechanism. The space required to store the results of these states is also O(zero * one * 2)
, simplifying again to O(zero * one)
. This represents all combinations of i
and j
with each value of k
.
Learn more about how to find time and space complexity quickly.
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
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!