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 array
  • one: the exact number of 1s that must appear in the array
  • limit: the maximum size threshold for subarrays

A binary array is considered stable if it satisfies all three conditions:

  1. It contains exactly zero number of 0s
  2. It contains exactly one number of 1s
  3. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 consecutive ks

For example, if k = 0, we calculate:

  • dfs(i - 1, j, 0): place another 0
  • dfs(i - 1, j, 1): place a 1 after the 0
  • Subtract dfs(i - limit - 1, j, 1): this represents the cases where we would have limit + 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 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

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 place
  • j: number of ones remaining to place
  • k: the last digit placed (0 or 1)

Base Cases:

  1. If i = 0 (no zeros left):
    • Return 1 if k = 1 and j ≤ limit (we can place all remaining ones consecutively)
    • Return 0 otherwise
  2. If j = 0 (no ones left):
    • Return 1 if k = 0 and i ≤ limit (we can place all remaining zeros consecutively)
    • Return 0 otherwise

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 0
  • dfs(i - 1, j, 1): place a 1 after the current 0
  • -dfs(i - limit - 1, j, 1): subtract invalid cases where we'd have limit + 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 1
  • dfs(i, j - 1, 1): place another 1 after the current 1
  • -dfs(i, j - limit - 1, 0): subtract invalid cases where we'd have limit + 1 consecutive 1s

Implementation Details:

  1. Use @cache decorator for memoization to avoid recalculating same states
  2. Handle edge cases where i - limit - 1 < 0 or j - limit - 1 < 0 by returning 0
  3. The final answer is (dfs(zero, one, 0) + dfs(zero, one, 1)) % mod, considering both starting with 0 or 1
  4. 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 Evaluator

Example 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.

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 from 0 to zero (zero + 1 possible values)
  • j: ranges from 0 to one (one + 1 possible values)
  • k: can be either 0 or 1 (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:

  1. Memoization cache: Stores all unique states visited, which is at most (zero + 1) * (one + 1) * 2 = O(zero * one) entries.
  2. Recursion stack: The maximum recursion depth is zero + one in the worst case (when we decrement either i or j by 1 in each recursive call until both reach 0). This contributes O(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 elements
  • dfs(i - limit - 1, j, 1) counts the INVALID cases where we'd have limit+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
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More