2466. Count Ways to Build Good Strings - Explanation
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
- Start with an empty string (length
0). - At each recursive call, if the current length exceeds
high, stop and return0. - If the current length is at least
low, count this as one valid string. - Recursively explore two branches: one where we add
zerocharacters and one where we addonecharacters. - 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:
- Space complexity: for recursion stack.
Where is equal to the given 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
- Create a memoization structure (hash map or array) to store results for each length.
- Use the same recursive approach as before, but before computing, check if the result for the current length is already cached.
- If cached, return the stored value immediately.
- Otherwise, compute the result by exploring both branches, store it in the cache, and return.
- 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:
- Space complexity:
Where is equal to the given 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
- Initialize a DP array where
dp[0] = 1(one way to have an empty string). - Iterate through lengths from
1tohigh. - For each length
i, setdp[i] = dp[i - zero] + dp[i - one](checking bounds). - Sum up all
dp[i]values for lengths in the range[low, high]. - 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)) % modTime & Space Complexity
- Time complexity:
- Space complexity:
Where is equal to the given 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]) % modOff-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 += 1Not 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