1639. Number of Ways to Form a Target String Given a Dictionary - Explanation
Prerequisites
Before attempting this problem, you should be comfortable with:
- Dynamic Programming - Used to count ways to form the target by choosing characters from word columns
- Memoization - Caching results for (target index, column index) pairs to avoid recomputation
- Character Frequency Preprocessing - Precomputing character counts at each column position optimizes the solution
- Modular Arithmetic - Required for handling large results modulo 10^9+7
1. Recursion
Intuition
We build the target string character by character. For each target character, we can pick it from any word at the current column position, then move to the next column. We can also skip columns without picking anything. The constraint is that once we use a column, we cannot go back to previous columns.
Algorithm
- Define a recursive function
dfs(i, k)whereiis the current target index andkis the current column index in the words. - Base cases:
- If
i == n(target length), we formed the entire target, return1. - If
k == m(word length), we ran out of columns, return0.
- If
- Count the ways by skipping column
k(calldfs(i, k + 1)). - For each word where
word[k]matchestarget[i], adddfs(i + 1, k + 1)tores. - Return the total count modulo
10^9 + 7.
class Solution:
def numWays(self, words: List[str], target: str) -> int:
mod = 10**9 + 7
n, m = len(target), len(words[0])
def dfs(i, k):
if i == n:
return 1
if k == m:
return 0
res = dfs(i, k + 1)
for w in words:
if w[k] != target[i]:
continue
res = (res + dfs(i + 1, k + 1)) % mod
return res
return dfs(0, 0)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of words, is the length of each word, and is the length of the string.
2. Dynamic Programming (Top-Down)
Intuition
The naive recursion is slow because we check every word at each step. We can precompute how many times each character appears at each column position. Then instead of iterating through all words, we simply multiply by the count of matching characters.
Algorithm
- Precompute a frequency table
cnt[k][c]storing how many words have charactercat columnk. - Define
dfs(i, k)with memoization:- Base cases same as before.
- Skip column
kby addingdfs(i, k + 1). - Use column
kby multiplyingcnt[k][target[i]]withdfs(i + 1, k + 1).
- Return
dfs(0, 0)modulo10^9 + 7.
class Solution:
def numWays(self, words: List[str], target: str) -> int:
mod = 10**9 + 7
cnt = defaultdict(int) # (index, char) --> count among all words
for w in words:
for i, c in enumerate(w):
cnt[(i, c)] += 1
dp = {}
def dfs(i, k):
if i == len(target):
return 1
if k == len(words[0]):
return 0
if (i, k) in dp:
return dp[(i, k)]
c = target[i]
dp[(i, k)] = dfs(i, k + 1) # skip k position
dp[(i, k)] += cnt[(k, c)] * dfs(i + 1, k + 1)
dp[(i, k)] %= mod
return dp[(i, k)]
return dfs(0, 0)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of words, is the length of each word, and is the length of the string.
3. Dynamic Programming (Bottom-Up)
Intuition
We can convert the memoized solution to a bottom-up DP. We fill a 2D table where dp[i][k] represents the number of ways to form target[i:] using columns k to m-1.
Algorithm
- Precompute the character frequency table.
- Create a DP table of size
(n+1) x (m+1)initialized to0. - Set
dp[n][m] = 1(base case: empty target from the end is valid). - Iterate backward through positions:
- For each
(i, k), setdp[i][k] = dp[i][k+1](skip column). - If
i < n, addcnt[k][target[i]] * dp[i+1][k+1](use column).
- For each
- Return
dp[0][0]modulo10^9 + 7.
class Solution:
def numWays(self, words: List[str], target: str) -> int:
MOD = 10**9 + 7
n, m = len(target), len(words[0])
cnt = [[0] * 26 for _ in range(m)]
for word in words:
for i, c in enumerate(word):
cnt[i][ord(c) - ord('a')] += 1
dp = [[0] * (m + 1) for _ in range(n + 1)]
dp[n][m] = 1
for i in range(n, -1, -1):
for k in range(m - 1, -1, -1):
dp[i][k] = dp[i][k + 1]
if i < n:
c = ord(target[i]) - ord('a')
dp[i][k] = (dp[i][k] + cnt[k][c] * dp[i + 1][k + 1]) % MOD
return dp[0][0]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of words, is the length of each word, and is the length of the string.
4. Dynamic Programming (Space Optimized)
Intuition
Since we only need the previous row of the DP table to compute the current row, we can reduce space by using a single 1D array.
Algorithm
- Precompute the character frequency table.
- Use a 1D DP array of size
m + 1. - Iterate from
i = ndown to0, and for each row, iterate through columns in reverse. - Maintain the value from the next row using a temporary variable.
- Return
dp[0]modulo10^9 + 7.
class Solution:
def numWays(self, words: List[str], target: str) -> int:
MOD = 10**9 + 7
n, m = len(target), len(words[0])
cnt = [[0] * 26 for _ in range(m)]
for word in words:
for i, c in enumerate(word):
cnt[i][ord(c) - ord('a')] += 1
dp = [0] * (m + 1)
dp[m] = 1
for i in range(n, -1, -1):
nxt = 1 if i == n - 1 else 0
for k in range(m - 1, -1, -1):
cur = dp[k]
dp[k] = dp[k + 1]
if i < n:
c = ord(target[i]) - ord('a')
dp[k] = (dp[k] + cnt[k][c] * nxt) % MOD
nxt = cur
dp[m] = 0
return dp[0]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of words, is the length of each word, and is the length of the string.
Common Pitfalls
Forgetting to Precompute Character Frequencies
A common mistake is iterating through all words at each step to count matching characters. This leads to TLE because the time complexity becomes per state transition. Instead, precompute a frequency table cnt[k][c] storing how many words have character c at column k before starting the DP.
Integer Overflow When Multiplying Count by DP Value
When computing cnt[k][c] * dp[i+1][k+1], both values can be large. In languages like Java and C++, multiplying two int values can overflow before the modulo operation is applied. Always cast to long or long long before multiplication: (long) cnt[k][c] * dp[i+1][k+1] % MOD.
Using Wrong Base Case for Bottom-Up DP
In bottom-up DP, the base case placement matters. Setting dp[n][k] = 1 for all k is incorrect because only dp[n][m] = 1 represents a valid completion (target fully matched at the end). The recurrence dp[i][k] = dp[i][k+1] propagates this value correctly, but initializing all dp[n][k] = 1 would overcount.
Confusing Column Index with Target Index
The DP state involves two indices: i for the target position and k for the column in words. A common error is swapping their roles or forgetting that k can skip ahead (we can skip columns) while i must advance exactly by 1 each time we pick a character. The column index k must always be at least as large as the target index i.
Not Handling Target Longer Than Word Length
If the target string is longer than the word length (n > m), it is impossible to form the target since we need at least n columns. The algorithm should return 0 in this case. While the recursion handles this naturally (running out of columns before finishing target), explicitly checking can prevent unnecessary computation.