2466. Count Ways to Build Good Strings - Explanation

Problem Link



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Dynamic Programming - Using memoization (top-down) or tabulation (bottom-up) to solve counting problems
  • Recursion - Breaking down the problem into smaller subproblems by exploring different choices
  • Modular Arithmetic - Applying modulo operations during intermediate calculations to prevent overflow

1. Recursion

Intuition

We can think of building a string as making a series of choices. At each step, we either append zero number of '0's or one number of '1's. The recursion naturally explores all possible combinations by branching at each decision point. A string is "good" when its length falls within the range [low, high], so we count it whenever we reach a valid length.

Algorithm

  1. Start with an empty string (length 0).
  2. At each recursive call, if the current length exceeds high, stop and return 0.
  3. If the current length is at least low, count this as one valid string.
  4. Recursively explore two branches: one where we add zero characters and one where we add one characters.
  5. Sum up the counts from both branches and return the result modulo 10^9 + 7.
class Solution:
    def countGoodStrings(self, low: int, high: int, zero: int, one: int) -> int:
        mod = 10**9 + 7

        def dfs(length):
            if length > high:
                return 0
            res = 1 if length >= low else 0
            res += dfs(length + zero) + dfs(length + one)
            return res % mod

        return dfs(0)

Time & Space Complexity

  • Time complexity: O(2n)O(2 ^ n)
  • Space complexity: O(n)O(n) for recursion stack.

Where nn is equal to the given highhigh value.


2. Dynamic Programming (Top-Down)

Intuition

The recursive solution has overlapping subproblems because we may reach the same string length through different paths. For example, adding zero then one might give the same length as adding one then zero. Memoization stores the result for each length we have already computed, avoiding redundant calculations.

Algorithm

  1. Create a memoization structure (hash map or array) to store results for each length.
  2. Use the same recursive approach as before, but before computing, check if the result for the current length is already cached.
  3. If cached, return the stored value immediately.
  4. Otherwise, compute the result by exploring both branches, store it in the cache, and return.
  5. Return the final count modulo 10^9 + 7.
class Solution:
    def countGoodStrings(self, low: int, high: int, zero: int, one: int) -> int:
        mod = 10**9 + 7
        dp = {}

        def dfs(length):
            if length > high:
                return 0
            if length in dp:
                return dp[length]

            dp[length] = 1 if length >= low else 0
            dp[length] += dfs(length + zero) + dfs(length + one)
            return dp[length] % mod

        return dfs(0)

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

Where nn is equal to the given highhigh value.


3. Dynamic Programming (Bottom-Up)

Intuition

Instead of starting from length 0 and recursing forward, we can build the solution iteratively. For each length i, the number of ways to reach it equals the ways to reach length i - zero (then add zeros) plus the ways to reach length i - one (then add ones). This is similar to the classic coin change problem where we count combinations.

Algorithm

  1. Initialize a DP array where dp[0] = 1 (one way to have an empty string).
  2. Iterate through lengths from 1 to high.
  3. For each length i, set dp[i] = dp[i - zero] + dp[i - one] (checking bounds).
  4. Sum up all dp[i] values for lengths in the range [low, high].
  5. Return the sum modulo 10^9 + 7.
class Solution:
    def countGoodStrings(self, low: int, high: int, zero: int, one: int) -> int:
        dp = { 0 : 1 }
        mod = 10**9 + 7

        for i in range(1, high + 1):
            dp[i] = (dp.get(i - one, 0) + dp.get(i - zero, 0)) % mod

        return sum(dp[i] for i in range(low, high + 1)) % mod

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

Where nn is equal to the given highhigh value.


Common Pitfalls

Forgetting the Modulo Operation

When counting the number of good strings, the result can grow very large. Forgetting to apply the modulo 10^9 + 7 during intermediate calculations leads to integer overflow.

# Wrong: Only applying mod at the end
dp[i] = dp[i - zero] + dp[i - one]  # Can overflow before mod is applied

# Correct: Apply mod after each addition
dp[i] = (dp[i - zero] + dp[i - one]) % mod

Off-by-One Errors in Range Check

The problem asks for strings with length in the range [low, high] inclusive. A common mistake is using exclusive bounds or checking > high instead of >= high when counting valid strings.

# Wrong: Missing strings of length exactly equal to low
if length > low:  # Should be length >= low

# Correct
if length >= low:
    count += 1

Not Initializing the Base Case Correctly

In the bottom-up DP approach, forgetting to set dp[0] = 1 (representing one way to build an empty string) causes all subsequent values to be zero.

# Wrong: Missing base case
dp = [0] * (high + 1)
for i in range(1, high + 1):
    dp[i] = dp.get(i - zero, 0) + dp.get(i - one, 0)  # All zeros!

# Correct: Initialize base case
dp = [0] * (high + 1)
dp[0] = 1  # One way to have an empty string