Count Ways To Build Good Strings

Count Ways To Build Good Strings: C++ DP Solution

Understanding the Problem

Given the integers zero, one, low, and high, we can construct a string by starting with an empty string, and then at each step perform either of the following:

  • Append the character '0' exactly zero times.
  • Append the character '1' exactly one times.

This operation can be performed any number of times. A good string is a string constructed by the above process having a length between low and high (inclusive).

We are tasked with returning the number of different good strings that can be constructed satisfying these properties. Because the final answer can be exceptionally large, we must return it modulo 109 + 7.

Solution Intuition and Dynamic Programming Approach

When faced with a combinatorial problem that asks for the total number of ways to construct an object under specific rules, Dynamic Programming (DP) is often the most efficient paradigm. The problem essentially asks us to find the number of paths to reach any length between low and high by taking steps of size zero and one.

We can define our state dp[i] as the number of ways to construct a valid string of exact length i. The base case is dp[0] = 1, which means there is exactly one way to construct an empty string (by doing nothing).

For any target length i from 1 up to high, we can arrive at this length in two possible ways:

  • If we appended '0's, our previous length must have been i - zero. Therefore, we add dp[i - zero] to dp[i].
  • If we appended '1's, our previous length must have been i - one. Therefore, we add dp[i - one] to dp[i].

By iteratively calculating the number of ways for each length, we build a complete tabulation array up to the maximum required length, high. This bottom-up approach eliminates the overhead associated with recursive function calls and avoids redundant calculations.

Optimal C++ Implementation: Tabulation

The fastest execution is achieved by using the tabulation dynamic programming technique. This iterative method ensures that we compute each subproblem exactly once in a continuous block of memory, which is highly cache-friendly.


    int countGoodStrings(int low, int high, int zero, int one) {
        int sum[100001];
        sum[0] = 1;
        for (int i = 1; i <= high; i++)
        {
            long long sumCur = 0;
            if (i >= zero)
                sumCur += sum[i-zero];
            if (i >= one)
                sumCur += sum[i-one];
            
            if (sumCur > 0x3000000000000000ll)
                sumCur %= 1000000007;
                
            sum[i] = sumCur % 1000000007;
        }

        long long sumTotal = 0;
        for (int i = low; i <= high; i++)
            sumTotal += sum[i];
            
        return sumTotal % 1000000007;
    }
    

In this C++ implementation, we maintain a sum array of size 100001, which is sufficient since the maximum value for high typically does not exceed 100000 in standard constraint environments. We carefully apply the modulo operation to prevent overflow issues, a common pitfall in combinatorial mathematics algorithms.

Alternative Approach: Recursive DFS with Memoization

A slower, yet conceptually simpler approach is to use a map for memoization paired with a recursive Depth First Search (DFS) method. This top-down technique naturally translates the recursive relationship into code.


class Solution {
public:
    int countGoodStrings(int low, int high, int zero, int one) {
        map<int, int> dp;
        long mod = pow(10,9) + 7;    
        function<int(int)> dfs = [&](int length) -> int{
            
            if( length > high ) return 0;

            if(dp.find(length) != dp.end() )
                return dp[length];

            int count = ( length >= low) ? 1 : 0;
            count = (count + dfs(length + zero)) % mod;
            count = (count + dfs(length + one)) % mod;
            
            return dp[length] = count;
        };

        return dfs(0);    
    }
};
    

While correct, the use of std::map introduces a logarithmic overhead for every state lookup and insertion. For a state space of size 100,000, this overhead combined with the recursion stack depth can lead to Time Limit Exceeded (TLE) or Memory Limit Exceeded (MLE) verdicts in highly competitive programming environments.

Complexity Analysis

Understanding the computational complexity of the dynamic programming tabulation approach is vital for ensuring your algorithm will execute within strict time limits.

  • Time Complexity: O(high). We iterate through a loop from 1 to high. Inside the loop, we perform constant time checks and additions. Finally, we iterate from low to high to calculate the final sum. The overall time is directly proportional to high.
  • Space Complexity: O(high). We allocate an array of size proportional to high to store the dynamic programming states. This is necessary to recall previously computed values for the i - zero and i - one transitions.

Frequently Asked Questions

Why do we use the modulo 10^9 + 7?

The modulo 10^9 + 7 is a large prime number frequently used in competitive programming. It prevents integer overflow during intermediate calculations while satisfying mathematical properties required for combinatorial addition.

Can the space complexity be optimized further?

Yes, if the values of zero and one are small compared to high, we only need to keep track of the previous max(zero, one) elements. However, since zero and one can theoretically be as large as high, the O(high) space complexity is the tightest worst-case bound.

What happens if zero and one are equal?

If zero and one are identical, every length step can be achieved in two distinct ways. The DP transitions naturally account for this, branching twice for the exact same step size, thus doubling the combinatorial pathways correctly.

Conclusion and Next Steps

Mastering dynamic programming concepts like state definition and optimal transition tracking is a cornerstone of advanced software engineering. The "Count Ways To Build Good Strings" algorithm perfectly illustrates how combinatorial explosions can be tamed into linear time complexities using simple tabulation. Continuing to practice similar sequence-building algorithms will dramatically enhance your algorithmic intuition and problem-solving speed.