Find the Original Typed String II
HardPrompt
Alice is attempting to type a specific string on her computer. However, she tends to be clumsy and may press a key for too long, resulting in a character being typed multiple times.
You are given a string word, which represents the final output displayed on Alice's screen. You are also given a positive integer k.
Return the total number of possible original strings that Alice might have intended to type, if she was trying to type a string of size at least k.
Since the answer may be very large, return it modulo 109 + 7.
Example 1:
Input: word = "aabbccdd", k = 7
Output: 5
Explanation:
The possible strings are: "aabbccdd", "aabbccd", "aabbcdd", "aabccdd", and "abbccdd".
Example 2:
Input: word = "aabbccdd", k = 8
Output: 1
Explanation:
The only possible string is "aabbccdd".
Example 3:
Input: word = "aaabbb", k = 3
Output: 8
Constraints:
1 <= word.length <= 5 * 105wordconsists only of lowercase English letters.1 <= k <= 2000
Approaches
3 approaches with complexity analysis and trade-offs.
A straightforward but inefficient approach is to use brute-force recursion. The idea is to explore every possible original string that could have produced the given word. We can define a recursive function that builds the original string group by group, keeping track of the current total length. When all groups have been processed, we check if the total length meets the k requirement.
Algorithm
- Parse the input
wordinto a list of groups, where each group consists of a character and its consecutive count. For example,"aaabbc"becomes[('a', 3), ('b', 2), ('c', 1)]. - Implement a recursive function, say
countWays(groupIndex, currentLength). - Base Case: If
groupIndexreaches the end of the list of groups, check ifcurrentLength >= k. If it is, we have found one valid original string, so return 1. Otherwise, return 0. - Recursive Step: For the current group
(character, count)atgroupIndex, iterate through all possible lengthsjfrom 1 tocount. For eachj, make a recursive callcountWays(groupIndex + 1, currentLength + j). Sum up the results from these recursive calls. - The initial call would be
countWays(0, 0).
Walkthrough
This method systematically generates all possible original strings. We first preprocess the word to identify consecutive character groups and their counts. For instance, "aaabbc" has three groups: a with count 3, b with count 2, and c with count 1. An original string is formed by choosing a length j_i (from 1 to n_i) for each group i. A recursive function can explore these choices. The function would take the current group index and the accumulated length as parameters. It would iterate through all possible lengths for the current group and recursively call itself for the next group with an updated length. The recursion terminates when all groups are considered, at which point the total length is checked against k.
class Solution { List<Integer> groups = new ArrayList<>(); int k; long MOD = 1_000_000_007; public int numberOfWays(String word, int k) { this.k = k; if (word.length() == 0) { return 0; } for (int i = 0; i < word.length(); ) { char c = word.charAt(i); int j = i; while (j < word.length() && word.charAt(j) == c) { j++; } groups.add(j - i); i = j; } return (int) solve(0, 0); } private long solve(int index, int currentLength) { if (index == groups.size()) { return currentLength >= k ? 1 : 0; } long count = 0; int groupSize = groups.get(index); for (int i = 1; i <= groupSize; i++) { count = (count + solve(index + 1, currentLength + i)) % MOD; } return count; }}Complexity
Time
O(Π n_i), where `n_i` is the size of the i-th group. In the worst case, like `"aabbcc..."`, this is `O(2^(N/2))`, which is exponential and too slow.
Space
O(m), where `m` is the number of groups. This is for the recursion stack depth. In the worst case, `m` can be equal to `word.length()`.
Trade-offs
Pros
Simple to understand and implement.
Correctly solves the problem for very small inputs.
Cons
Extremely inefficient due to redundant computations.
The time complexity is exponential in the number of groups, making it infeasible for the given constraints.
Will result in a 'Time Limit Exceeded' (TLE) error on most platforms.
The brute-force approach suffers from recomputing the same subproblems. We can use dynamic programming with memoization to store and reuse results. A more standard DP approach is to build a table iteratively. We can calculate the number of ways to form an original string with a specific length. A key insight is to count the complement: find the number of original strings with length less than k and subtract this from the total number of possible original strings.
The total number of original strings is the product of the sizes of all character groups. The number of strings with length less than k can be found using a DP table dp[i][j], representing the number of ways to achieve a total length j using the first i groups. The transition involves summing up results from the previous state, which can be optimized from O(n_i) to O(1) per state using prefix sums.
Algorithm
- The problem asks for the number of ways to get a total length of at least
k. It's easier to calculate the total number of ways and subtract the number of ways to get a length less thank. - Total ways =
product(n_i)for all groupsi. - To find ways for length
< k, we use DP. Letdp[i][j]be the number of ways to get a total length ofjusing the firstigroups. - State:
dp[i][j]= number of ways to get sumjusing firstigroups. - Base Case:
dp[0][0] = 1(0 groups, sum 0, 1 way). - Recurrence:
dp[i][j] = Σ_{l=1 to n_i} dp[i-1][j-l]. - This can be optimized using prefix sums. Let
S[i-1]be the prefix sum array fordp[i-1]. Thendp[i][j] = S[i-1][j-1] - S[i-1][j-n_i-1]. - The final number of invalid ways is
Σ_{j=0 to k-1} dp[m][j]. - The answer is
(Total ways - Invalid ways) % MOD.
Walkthrough
Let's define dp[i][j] as the number of ways to choose lengths for the first i groups such that their sum is exactly j. We are interested in j < k. The DP table will have dimensions (m+1) x k, where m is the number of groups.
The state transition is dp[i][j] = Σ_{l=1 to n_i} dp[i-1][j-l], where n_i is the size of the i-th group. A naive implementation of this would take O(n_i) for each state, leading to a total time complexity of O(m * k * n_avg) = O(N * k). We can optimize the summation. Notice that Σ_{l=1 to n_i} dp[i-1][j-l] is a sum over a contiguous range. By pre-calculating prefix sums for the dp[i-1] row, we can compute this sum in O(1). Let S[i-1][x] = Σ_{p=0 to x} dp[i-1][p]. Then dp[i][j] = S[i-1][j-1] - S[i-1][j-n_i-1]. This reduces the complexity of each state transition to O(1), and the total time to O(m*k). We can also optimize space to O(k) by only keeping track of the previous DP row.
class Solution { public int numberOfWays(String word, int k) { long MOD = 1_000_000_007; List<Integer> groups = new ArrayList<>(); for (int i = 0; i < word.length(); ) { char c = word.charAt(i); int j = i; while (j < word.length() && word.charAt(j) == c) { j++; } groups.add(j - i); i = j; } int m = groups.size(); long[] dp = new long[k]; dp[0] = 1; // Base case: 0 groups, sum 0, 1 way for (int groupSize : groups) { long[] newDp = new long[k]; long[] prefixSum = new long[k]; prefixSum[0] = dp[0]; for (int j = 1; j < k; j++) { prefixSum[j] = (prefixSum[j - 1] + dp[j]) % MOD; } for (int j = 1; j < k; j++) { long upper = prefixSum[j - 1]; long lower = (j - 1 - groupSize >= 0) ? prefixSum[j - 1 - groupSize] : 0; newDp[j] = (upper - lower + MOD) % MOD; } dp = newDp; } long invalidWays = 0; for (long count : dp) { invalidWays = (invalidWays + count) % MOD; } long totalWays = 1; for (int groupSize : groups) { totalWays = (totalWays * groupSize) % MOD; } return (int) ((totalWays - invalidWays + MOD) % MOD); }}Complexity
Time
O(m*k), where `m` is the number of groups and `k` is the target length. This is because we iterate through `m` groups, and for each group, we do `O(k)` work to fill the new DP row.
Space
O(m*k) for the DP table. Can be optimized to O(k) by only storing the previous row's DP values.
Trade-offs
Pros
Much more efficient than brute force.
Systematically builds the solution, avoiding redundant work.
The prefix sum optimization significantly improves performance.
Cons
The time complexity is proportional to the number of groups
m.If
mis large (e.g.,mis close toword.length()), this approach is still too slow and will TLE.
This approach builds upon the previous DP solution by incorporating a critical observation about the problem constraints. The minimum length of any possible original string is equal to the number of character groups, m, since each group must contribute at least one character. This leads to a powerful optimization.
If m >= k, we know for sure that any original string will have a length of at least k. Thus, we don't need to perform any complex counting; all possible combinations are valid. The answer is simply the product of the sizes of all groups.
If m < k, we proceed with the DP approach described previously to count the number of 'invalid' strings (those with length < k). Since m is now bounded by k, the O(m*k) complexity becomes O(k^2), which is efficient enough to pass within the time limits.
Algorithm
- First, parse the
wordinto a list of group lengthsn_1, n_2, ..., n_m. - Crucial Observation: The minimum possible length of an original string is
m, achieved by choosing length 1 from each group. - Case 1:
m >= k- If the number of groups
mis greater than or equal tok, any possible original string will have a length of at leastm >= k. - Therefore, all possible combinations are valid. The answer is the total number of combinations, which is the product of all group sizes:
Π n_i(mod10^9 + 7).
- If the number of groups
- Case 2:
m < k- In this case, some combinations might result in a total length less than
k. - We use the same
O(m*k)DP approach as in the previous method to count these invalid combinations. - Since
m < k, the time complexity of the DP part becomesO(m*k) < O(k^2).
- In this case, some combinations might result in a total length less than
- The overall complexity is dominated by parsing the string and the DP calculation, leading to
O(N + k^2).
Walkthrough
The algorithm first determines the number of groups, m.
If m >= k, the problem simplifies greatly. The shortest possible original string is formed by taking one character from each of the m groups, resulting in a length of m. Since m >= k, this shortest string already satisfies the condition. Any other choice will only result in a longer string, which also satisfies the condition. Hence, every single combination is valid. The total number of ways is n_1 * n_2 * ... * n_m (modulo 10^9 + 7). This part takes O(N) to parse the string and O(m) to compute the product.
If m < k, we must filter out the combinations that result in a total length less than k. We use the space-optimized O(m*k) DP from the previous approach. We maintain a dp array of size k, where dp[j] stores the number of ways to get a total length of j. We iterate through each of the m groups, updating the dp array using prefix sums. Since we are in the case where m < k, the total time for this DP calculation is bounded by O(k * k) = O(k^2). After processing all m groups, we sum up the values in the final dp array to get the total number of invalid ways. The final answer is (Total Ways - Invalid Ways) % MOD.
This case-based analysis ensures that we avoid the expensive O(m*k) computation when m is large, making the solution efficient for all valid inputs.
class Solution { public int numberOfWays(String word, int k) { long MOD = 1_000_000_007; List<Integer> groups = new ArrayList<>(); for (int i = 0; i < word.length(); ) { char c = word.charAt(i); int j = i; while (j < word.length() && word.charAt(j) == c) { j++; } groups.add(j - i); i = j; } int m = groups.size(); long totalWays = 1; for (int groupSize : groups) { totalWays = (totalWays * groupSize) % MOD; } if (m >= k) { return (int) totalWays; } // Case m < k: Use DP to find invalid ways (sum < k) long[] dp = new long[k]; dp[0] = 1; // Base case: 0 groups, sum 0, 1 way for (int groupSize : groups) { long[] newDp = new long[k]; long[] prefixSum = new long[k]; prefixSum[0] = dp[0]; for (int j = 1; j < k; j++) { prefixSum[j] = (prefixSum[j - 1] + dp[j]) % MOD; } for (int j = 1; j < k; j++) { long upper = prefixSum[j - 1]; long lower = (j - 1 - groupSize >= 0) ? prefixSum[j - 1 - groupSize] : 0; newDp[j] = (upper - lower + MOD) % MOD; } dp = newDp; } long invalidWays = 0; for (long count : dp) { invalidWays = (invalidWays + count) % MOD; } return (int) ((totalWays - invalidWays + MOD) % MOD); }}Complexity
Time
O(N + min(m, k) * k). Since `m <= N`, this is effectively `O(N + k^2)`. `O(N)` for parsing, and if `m < k`, `O(m*k)` which is less than `O(k^2)` for the DP part.
Space
O(N + k). `O(N)` to store the group sizes in the worst case, and `O(k)` for the DP arrays.
Trade-offs
Pros
Highly efficient and handles all constraints.
Combines a clever observation with dynamic programming.
The
m >= kcase provides a fast path that avoids complex calculations for a large set of inputs.
Cons
The logic is slightly more complex due to the case analysis.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.