Facebook Pixel

2533. Number of Good Binary Strings 🔒

Problem Description

You need to count how many "good" binary strings exist within certain constraints.

A binary string consists only of 0s and 1s. When we look at these strings, consecutive identical digits form "blocks". For example, in the string 0011101111100, we have blocks of zeros [2, 1, 2] (at the beginning, middle, and end) and blocks of ones [3, 5].

A binary string is considered good if it meets three conditions:

  1. Length constraint: The string's length must be between minLength and maxLength (inclusive).

  2. Ones constraint: Every block of consecutive 1s must have a size that's a multiple of oneGroup. For instance, if oneGroup = 3, then blocks of 1s can only be of size 3, 6, 9, etc.

  3. Zeros constraint: Every block of consecutive 0s must have a size that's a multiple of zeroGroup. For instance, if zeroGroup = 2, then blocks of 0s can only be of size 2, 4, 6, etc.

Important note: The value 0 is considered a multiple of every number. This means an empty block (no consecutive 0s or 1s at all) is valid.

Your task is to find the total number of good binary strings. Since this number can be very large, return the result modulo 10^9 + 7.

For example, if minLength = 2, maxLength = 3, oneGroup = 1, and zeroGroup = 2:

  • The string 00 is good (one block of 2 zeros, which is a multiple of 2)
  • The string 11 is good (one block of 2 ones, which is a multiple of 1)
  • The string 001 is good (one block of 2 zeros and one block of 1 one)
  • And so on...

You need to count all such valid strings and return the count.

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

Intuition

Think about how we build a good binary string step by step. At any point, we can either add a complete block of consecutive 1s or a complete block of consecutive 0s. Since blocks must be multiples of oneGroup or zeroGroup, we can only add blocks of size oneGroup, 2*oneGroup, 3*oneGroup... for 1s, and similarly for 0s.

But here's the key insight: instead of thinking about adding variable-sized blocks, we can simplify by only considering adding blocks of the minimum valid size - exactly oneGroup 1s or exactly zeroGroup 0s at a time. Why? Because any larger valid block (like 2*oneGroup) can be formed by adding the minimum block multiple times consecutively.

This transforms our problem into something similar to the "climbing stairs" problem. If we want to form a string of length i, we can either:

  • Take a valid string of length i - oneGroup and add a block of oneGroup ones
  • Take a valid string of length i - zeroGroup and add a block of zeroGroup zeros

This leads us to dynamic programming. Let f[i] represent the number of ways to form a valid string of exactly length i. Then:

  • f[0] = 1 (empty string is valid)
  • For any i > 0: f[i] = f[i - oneGroup] + f[i - zeroGroup] (if the indices are valid)

The beauty of this approach is that it automatically handles all valid combinations. When we add a block to an existing string, we don't need to worry about whether we're extending an existing block or starting a new one - the math works out correctly either way because we're always adding complete valid blocks.

Finally, since we want strings of length between minLength and maxLength, we sum up all f[i] where minLength ≤ i ≤ maxLength.

Learn more about Dynamic Programming patterns.

Solution Approach

We implement the dynamic programming solution using a 1D array f where f[i] represents the number of valid binary strings of length i.

Initialization:

  • Create an array f of size maxLength + 1
  • Set f[0] = 1 (empty string is valid)
  • All other positions start at 0

State Transition: For each position i from 1 to maxLength, we calculate f[i] based on two possible transitions:

  1. Adding a block of ones: If i >= oneGroup, we can form a string of length i by taking any valid string of length i - oneGroup and appending oneGroup ones to it. This contributes f[i - oneGroup] ways.

  2. Adding a block of zeros: If i >= zeroGroup, we can form a string of length i by taking any valid string of length i - zeroGroup and appending zeroGroup zeros to it. This contributes f[i - zeroGroup] ways.

The recurrence relation is:

f[i] = f[i - oneGroup] + f[i - zeroGroup]

(where we only add each term if the index is valid)

Implementation Details:

f = [1] + [0] * maxLength  # f[0] = 1, rest are 0
for i in range(1, len(f)):
    if i - oneGroup >= 0:
        f[i] += f[i - oneGroup]
    if i - zeroGroup >= 0:
        f[i] += f[i - zeroGroup]
    f[i] %= mod  # Apply modulo to prevent overflow

Final Answer: Sum all values from f[minLength] to f[maxLength] (inclusive). This gives us the total count of valid strings with lengths in the required range.

return sum(f[minLength:]) % mod

The time complexity is O(maxLength) as we iterate through each position once. The space complexity is also O(maxLength) for storing the DP array.

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 minLength = 2, maxLength = 4, oneGroup = 2, and zeroGroup = 1.

We need to find all valid binary strings of length 2, 3, or 4 where:

  • Every block of 1s has size that's a multiple of 2 (so blocks of size 2, 4, 6...)
  • Every block of 0s has size that's a multiple of 1 (so blocks of size 1, 2, 3...)

Step 1: Initialize DP array Create array f where f[i] = number of valid strings of length i:

  • f[0] = 1 (empty string is valid)
  • f[1] = 0, f[2] = 0, f[3] = 0, f[4] = 0 (initially)

Step 2: Build up the DP table

For i = 1:

  • Can we add a block of 2 ones? No, because 1 - 2 = -1 (invalid index)
  • Can we add a block of 1 zero? Yes, from f[0]: f[1] = f[0] = 1
  • Valid strings of length 1: "0"

For i = 2:

  • Can we add a block of 2 ones? Yes, from f[0]: contributes f[0] = 1
  • Can we add a block of 1 zero? Yes, from f[1]: contributes f[1] = 1
  • f[2] = 1 + 1 = 2
  • Valid strings of length 2: "11" (from f[0] + two 1s), "00" (from f[1] + one 0)

For i = 3:

  • Can we add a block of 2 ones? Yes, from f[1]: contributes f[1] = 1
  • Can we add a block of 1 zero? Yes, from f[2]: contributes f[2] = 2
  • f[3] = 1 + 2 = 3
  • Valid strings of length 3: "011" (from "0" + two 1s), "110" (from "11" + one 0), "000" (from "00" + one 0)

For i = 4:

  • Can we add a block of 2 ones? Yes, from f[2]: contributes f[2] = 2
  • Can we add a block of 1 zero? Yes, from f[3]: contributes f[3] = 3
  • f[4] = 2 + 3 = 5
  • Valid strings of length 4: "1111" (from "11" + two 1s), "0011" (from "00" + two 1s), "1100" (from "110" + one 0), "0110" (from "011" + one 0), "0000" (from "000" + one 0)

Step 3: Calculate final answer Sum values for lengths in range [2, 4]:

  • f[2] + f[3] + f[4] = 2 + 3 + 5 = 10

The answer is 10 valid binary strings.

Solution Implementation

1class Solution:
2    def goodBinaryStrings(
3        self, minLength: int, maxLength: int, oneGroup: int, zeroGroup: int
4    ) -> int:
5        # Modulo value for large number handling
6        MOD = 10**9 + 7
7      
8        # Dynamic programming array where dp[i] represents the number of ways
9        # to form a valid binary string of length i
10        # dp[0] = 1 represents an empty string (base case)
11        dp = [1] + [0] * maxLength
12      
13        # Build up the dp array for all lengths from 1 to maxLength
14        for length in range(1, len(dp)):
15            # If we can append a group of ones (oneGroup consecutive 1s)
16            # to a string of length (length - oneGroup), add those possibilities
17            if length - oneGroup >= 0:
18                dp[length] += dp[length - oneGroup]
19          
20            # If we can append a group of zeros (zeroGroup consecutive 0s)
21            # to a string of length (length - zeroGroup), add those possibilities
22            if length - zeroGroup >= 0:
23                dp[length] += dp[length - zeroGroup]
24          
25            # Apply modulo to prevent integer overflow
26            dp[length] %= MOD
27      
28        # Sum up all valid strings with lengths between minLength and maxLength (inclusive)
29        # dp[minLength:] gives us all elements from index minLength to the end
30        result = sum(dp[minLength:]) % MOD
31      
32        return result
33
1class Solution {
2    public int goodBinaryStrings(int minLength, int maxLength, int oneGroup, int zeroGroup) {
3        // Modulo value for preventing integer overflow
4        final int MOD = 1_000_000_007;
5      
6        // Dynamic programming array where dp[i] represents the number of valid strings of length i
7        int[] dp = new int[maxLength + 1];
8      
9        // Base case: empty string (length 0) counts as one valid way
10        dp[0] = 1;
11      
12        // Build up the DP table for all lengths from 1 to maxLength
13        for (int currentLength = 1; currentLength <= maxLength; ++currentLength) {
14            // If we can append a group of ones (oneGroup length) to form current length
15            if (currentLength - oneGroup >= 0) {
16                dp[currentLength] = (dp[currentLength] + dp[currentLength - oneGroup]) % MOD;
17            }
18          
19            // If we can append a group of zeros (zeroGroup length) to form current length
20            if (currentLength - zeroGroup >= 0) {
21                dp[currentLength] = (dp[currentLength] + dp[currentLength - zeroGroup]) % MOD;
22            }
23        }
24      
25        // Sum up all valid strings with lengths between minLength and maxLength
26        int totalCount = 0;
27        for (int length = minLength; length <= maxLength; ++length) {
28            totalCount = (totalCount + dp[length]) % MOD;
29        }
30      
31        return totalCount;
32    }
33}
34
1class Solution {
2public:
3    int goodBinaryStrings(int minLength, int maxLength, int oneGroup, int zeroGroup) {
4        // Modulo value for preventing integer overflow
5        const int MOD = 1000000007;
6      
7        // dp[i] represents the number of ways to form a good binary string of length i
8        // We need array size of maxLength + 1 to include length from 0 to maxLength
9        int dp[maxLength + 1];
10        memset(dp, 0, sizeof(dp));
11      
12        // Base case: empty string has one way to be formed
13        dp[0] = 1;
14      
15        // Build up the DP table for all lengths from 1 to maxLength
16        for (int currentLength = 1; currentLength <= maxLength; ++currentLength) {
17            // If we can append a group of ones to reach current length
18            if (currentLength - oneGroup >= 0) {
19                dp[currentLength] = (dp[currentLength] + dp[currentLength - oneGroup]) % MOD;
20            }
21          
22            // If we can append a group of zeros to reach current length
23            if (currentLength - zeroGroup >= 0) {
24                dp[currentLength] = (dp[currentLength] + dp[currentLength - zeroGroup]) % MOD;
25            }
26        }
27      
28        // Sum up all valid string counts within the range [minLength, maxLength]
29        int totalCount = 0;
30        for (int length = minLength; length <= maxLength; ++length) {
31            totalCount = (totalCount + dp[length]) % MOD;
32        }
33      
34        return totalCount;
35    }
36};
37
1/**
2 * Counts the number of good binary strings within a given length range.
3 * A good binary string is one where consecutive 1s appear in groups of size oneGroup
4 * and consecutive 0s appear in groups of size zeroGroup.
5 * 
6 * @param minLength - Minimum length of binary strings to consider
7 * @param maxLength - Maximum length of binary strings to consider
8 * @param oneGroup - Required size of consecutive 1s groups
9 * @param zeroGroup - Required size of consecutive 0s groups
10 * @returns The count of good binary strings modulo 10^9 + 7
11 */
12function goodBinaryStrings(
13    minLength: number,
14    maxLength: number,
15    oneGroup: number,
16    zeroGroup: number,
17): number {
18    // Modulo value to prevent integer overflow
19    const MOD: number = 10 ** 9 + 7;
20  
21    // Dynamic programming array where dp[i] represents the number of good binary strings of length i
22    const dp: number[] = Array(maxLength + 1).fill(0);
23  
24    // Base case: empty string is considered valid
25    dp[0] = 1;
26  
27    // Build up the DP table for all lengths from 1 to maxLength
28    for (let length = 1; length <= maxLength; ++length) {
29        // If we can add a group of ones to form current length
30        if (length >= oneGroup) {
31            dp[length] = (dp[length] + dp[length - oneGroup]) % MOD;
32        }
33      
34        // If we can add a group of zeros to form current length
35        if (length >= zeroGroup) {
36            dp[length] = (dp[length] + dp[length - zeroGroup]) % MOD;
37        }
38    }
39  
40    // Sum up all valid string counts from minLength to maxLength
41    let totalCount: number = 0;
42    for (let i = minLength; i <= maxLength; ++i) {
43        totalCount = (totalCount + dp[i]) % MOD;
44    }
45  
46    return totalCount;
47}
48

Time and Space Complexity

The time complexity is O(n), where n = maxLength. The algorithm uses dynamic programming with a single loop that iterates from 1 to maxLength (inclusive). Within each iteration, it performs constant-time operations: at most two additions, two array accesses, and one modulo operation. Therefore, the overall time complexity is linear with respect to maxLength.

The space complexity is O(n), where n = maxLength. The algorithm creates an array f of size maxLength + 1 to store the dynamic programming states. This is the dominant space usage in the algorithm. The additional variables (mod, i, and the sum computation) use constant space. Thus, the total space complexity is linear with respect to maxLength.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Misunderstanding the DP State Transition

Pitfall: A common mistake is thinking that dp[i] represents strings ending with a specific digit (0 or 1), leading to incorrect state transitions like trying to track the last character or attempting to alternate between 0s and 1s.

Why it happens: The problem mentions "blocks" of consecutive digits, which might mislead you into thinking you need to track what digit the string ends with to ensure valid block formations.

Correct Understanding: The DP state dp[i] represents ALL valid binary strings of length i, regardless of what digit they end with. When we add oneGroup ones or zeroGroup zeros, we're adding a complete valid block to any valid string, and the block constraint is automatically satisfied because we only add blocks of the required sizes.

2. Incorrect Modulo Application

Pitfall: Forgetting to apply modulo during intermediate calculations, only applying it at the final return:

# WRONG - can cause integer overflow
for length in range(1, len(dp)):
    if length - oneGroup >= 0:
        dp[length] += dp[length - oneGroup]
    if length - zeroGroup >= 0:
        dp[length] += dp[length - zeroGroup]
    # Missing modulo here!

return sum(dp[minLength:]) % MOD  # Only applying modulo at the end

Solution: Apply modulo after each update to prevent overflow:

# CORRECT
for length in range(1, len(dp)):
    if length - oneGroup >= 0:
        dp[length] += dp[length - oneGroup]
    if length - zeroGroup >= 0:
        dp[length] += dp[length - zeroGroup]
    dp[length] %= MOD  # Apply modulo after each calculation

3. Edge Case: Same Group Sizes

Pitfall: Double-counting when oneGroup == zeroGroup. Some might think special handling is needed to avoid counting the same transitions twice.

Why it's not a problem: Even when oneGroup == zeroGroup, the transitions represent different actions (adding ones vs adding zeros), creating different binary strings. For example, if both groups are size 2, adding "11" vs "00" to an empty string creates different valid strings.

4. Overthinking Block Combinations

Pitfall: Trying to explicitly enumerate or track different combinations of blocks (like "first add 2 zeros, then 3 ones, then 2 zeros...").

Why it's unnecessary: The DP approach automatically handles all valid combinations. When we compute dp[i], it includes ALL possible ways to form valid strings of length i through any valid sequence of block additions. The beauty of this solution is that it abstracts away the complexity of block ordering and combinations.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following array represent a max heap?


Recommended Readings

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

Load More