Facebook Pixel

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

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 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings

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

Load More