LeetCode 1639 - Number of Ways to Form a Target String Given a Dictionary
The problem gives us a list of strings called words, where every string has the same length, and another string called target. We need to count how many different ways we can build target by selecting characters from the columns of words.
Difficulty: 🔴 Hard
Topics: Array, String, Dynamic Programming
Solution
Problem Understanding
The problem gives us a list of strings called words, where every string has the same length, and another string called target. We need to count how many different ways we can build target by selecting characters from the columns of words.
The important restriction is that we build target from left to right, and once we use a column index k, every column <= k becomes permanently unavailable across all strings. This means the chosen column indices must be strictly increasing.
For example, suppose we choose column 2 to build target[0]. Then the next character of the target can only come from columns 3, 4, and so on. We are not choosing entire words, we are choosing columns, and at each column we may use any word that contains the needed character there.
This changes the problem from a simple string matching task into a combinatorial dynamic programming problem.
The input constraints are large:
- Up to
1000words - Each word length up to
1000 - Target length up to
1000
A naive recursive search would explode exponentially because every target character may have many valid column choices. We therefore need a highly optimized solution.
A few observations immediately stand out:
- Since all words have the same length, we can think column by column.
- The order of columns matters.
- Multiple words may contribute the same character in a column.
- We only care about character frequencies per column, not the exact identity of each word.
Important edge cases include:
- The target being longer than the word length, which makes the answer automatically
0. - Columns that do not contain required characters.
- Large counts that require modulo arithmetic.
- Repeated characters in the target, which can create many overlapping subproblems.
Approaches
Brute Force Approach
A brute force solution would recursively try every possible column for every character in the target.
For each target character:
- Iterate through all remaining columns.
- Check every word to see whether the column contains the needed character.
- Recursively continue building the rest of the target.
This approach is correct because it explores every valid sequence of increasing column indices. However, it is far too slow.
If there are m columns and target length t, the number of possible increasing column sequences alone is roughly combinatorial, close to C(m, t). Additionally, each step scans all words.
The runtime becomes exponential and cannot handle the constraint sizes.
Key Insight
The key observation is that the actual identity of the word does not matter. What matters is:
For each column, how many words contain each character.
Suppose column 5 contains the letter 'a' in 7 different words. If we want to place 'a' at this step, then this column contributes 7 possible choices.
This lets us preprocess frequency counts:
count[column][character] = number of words having that character in that column
Then the problem becomes:
- Process columns from left to right.
- Decide whether to use the current column for the next target character.
- Use dynamic programming to count ways efficiently.
This transforms an exponential search into polynomial time.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^m × n) approximately | O(t) recursion | Explores all column combinations |
| Optimal Dynamic Programming | O(n × m + m × t) | O(m × 26 + t) | Uses column character frequencies and DP |
Where:
n = len(words)m = len(words[0])t = len(target)
Algorithm Walkthrough
Step 1: Precompute character frequencies per column
We create a 2D array where:
freq[col][char]
stores how many words contain char at column col.
For example:
words = ["acca","bbbb","caca"]
Column 0 contains:
a, b, c
So:
freq[0]['a'] = 1
freq[0]['b'] = 1
freq[0]['c'] = 1
This preprocessing allows us to instantly know how many choices exist for a target character at a given column.
Step 2: Define the DP state
We define:
dp[i]
as the number of ways to form the first i characters of the target.
Initially:
dp[0] = 1
because there is exactly one way to form an empty string.
Step 3: Process columns from left to right
For each column:
- We may choose to use it for some target character.
- Or we may skip it.
To avoid overwriting states too early, we iterate target indices backward.
Suppose current column contributes character 'a' three times.
If:
target[j] == 'a'
then:
dp[j + 1] += dp[j] * 3
This means:
- Every way to build the first
jcharacters - Can extend into a way to build the first
j + 1 - Using any of the three available
'a'characters in this column.
Step 4: Use modulo arithmetic
The number of combinations can become enormous, so every update is computed modulo:
10^9 + 7
Step 5: Return the final answer
After processing all columns:
dp[len(target)]
contains the total number of ways to form the entire target.
Why it works
The algorithm maintains the invariant that after processing column c, dp[i] stores the number of ways to build the first i characters of the target using only columns 0...c.
Each column is processed exactly once, and backward iteration ensures that a column cannot be reused multiple times for different target positions. Since every valid construction corresponds to a unique sequence of increasing columns, the DP counts every valid solution exactly once.
Python Solution
from typing import List
class Solution:
def numWays(self, words: List[str], target: str) -> int:
MOD = 10**9 + 7
word_length = len(words[0])
target_length = len(target)
if target_length > word_length:
return 0
# freq[col][char_index]
freq = [[0] * 26 for _ in range(word_length)]
for word in words:
for col, ch in enumerate(word):
freq[col][ord(ch) - ord('a')] += 1
# dp[i] = number of ways to form first i characters
dp = [0] * (target_length + 1)
dp[0] = 1
for col in range(word_length):
# Traverse backwards to avoid reusing same column
for i in range(target_length - 1, -1, -1):
target_char_index = ord(target[i]) - ord('a')
char_count = freq[col][target_char_index]
if char_count > 0:
dp[i + 1] = (
dp[i + 1] + dp[i] * char_count
) % MOD
return dp[target_length]
The implementation begins by checking whether the target is longer than the available column count. If it is, forming the target is impossible.
Next, the solution builds the frequency table. Instead of repeatedly scanning all words during DP transitions, each column stores how many times each character appears.
The dynamic programming array is then initialized. dp[0] = 1 represents the empty prefix.
For every column, we iterate backward through the target indices. Backward traversal is essential because it prevents using the same column multiple times in one iteration.
Each update:
dp[i + 1] += dp[i] * char_count
means:
- There are
dp[i]ways to form the current prefix. - The current column offers
char_countways to extend it.
Finally, dp[target_length] contains the total number of valid constructions.
Go Solution
func numWays(words []string, target string) int {
const MOD int = 1_000_000_007
wordLength := len(words[0])
targetLength := len(target)
if targetLength > wordLength {
return 0
}
// freq[col][char]
freq := make([][]int, wordLength)
for i := 0; i < wordLength; i++ {
freq[i] = make([]int, 26)
}
for _, word := range words {
for col, ch := range word {
freq[col][ch-'a']++
}
}
dp := make([]int, targetLength+1)
dp[0] = 1
for col := 0; col < wordLength; col++ {
for i := targetLength - 1; i >= 0; i-- {
charIndex := target[i] - 'a'
charCount := freq[col][charIndex]
if charCount > 0 {
dp[i+1] = (dp[i+1] + dp[i]*charCount) % MOD
}
}
}
return dp[targetLength]
}
The Go implementation follows the same logic as the Python version.
One important difference is integer handling. Since intermediate multiplication values may become large, modulo arithmetic is applied immediately after every update.
Go slices are initialized explicitly, unlike Python lists which can be created with multiplication syntax.
The reverse traversal logic remains exactly the same because it is critical for correctness.
Worked Examples
Example 1
words = ["acca","bbbb","caca"]
target = "aba"
Frequency Table
| Column | a | b | c |
|---|---|---|---|
| 0 | 1 | 1 | 1 |
| 1 | 1 | 1 | 1 |
| 2 | 0 | 1 | 2 |
| 3 | 2 | 1 | 0 |
DP Initialization
dp = [1, 0, 0, 0]
Meaning:
dp[0] = 1, empty string
Process Column 0
Need target characters in reverse:
i = 2, target[2] = 'a'
freq[0]['a'] = 1
dp[3] += dp[2] * 1 = 0
i = 1, target[1] = 'b'
freq[0]['b'] = 1
dp[2] += dp[1] * 1 = 0
i = 0, target[0] = 'a'
freq[0]['a'] = 1
dp[1] += dp[0] * 1
Now:
dp = [1, 1, 0, 0]
Process Column 1
After updates:
dp = [1, 2, 1, 0]
Process Column 2
After updates:
dp = [1, 2, 3, 0]
Process Column 3
After updates:
dp = [1, 4, 5, 6]
Final answer:
6
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n × m + m × t) | Frequency preprocessing plus DP transitions |
| Space | O(m × 26 + t) | Frequency table and DP array |
Where:
nis the number of wordsmis the word lengthtis the target length
The preprocessing scans every character in every word exactly once, producing O(n × m) complexity.
The DP phase processes every column and every target position once, resulting in O(m × t) time.
The space usage comes from the frequency table and the one-dimensional DP array.
Test Cases
from typing import List
class Solution:
def numWays(self, words: List[str], target: str) -> int:
MOD = 10**9 + 7
word_length = len(words[0])
target_length = len(target)
if target_length > word_length:
return 0
freq = [[0] * 26 for _ in range(word_length)]
for word in words:
for col, ch in enumerate(word):
freq[col][ord(ch) - ord('a')] += 1
dp = [0] * (target_length + 1)
dp[0] = 1
for col in range(word_length):
for i in range(target_length - 1, -1, -1):
char_index = ord(target[i]) - ord('a')
count = freq[col][char_index]
dp[i + 1] = (dp[i + 1] + dp[i] * count) % MOD
return dp[target_length]
solution = Solution()
assert solution.numWays(
["acca", "bbbb", "caca"],
"aba"
) == 6 # Provided example 1
assert solution.numWays(
["abba", "baab"],
"bab"
) == 4 # Provided example 2
assert solution.numWays(
["abcd"],
"abcd"
) == 1 # Exact single construction
assert solution.numWays(
["abcd"],
"abcde"
) == 0 # Target longer than word length
assert solution.numWays(
["aaaa", "aaaa"],
"aa"
) == 24 # Many combinations
assert solution.numWays(
["abc", "def"],
"zzz"
) == 0 # Impossible target
assert solution.numWays(
["ab", "ab", "ab"],
"ab"
) == 9 # Multiple identical words
assert solution.numWays(
["xyz"],
"x"
) == 1 # Single character target
assert solution.numWays(
["aaa", "aaa", "aaa"],
"aaa"
) == 27 # Repeated character across all columns
assert solution.numWays(
["bac", "bac", "bac"],
"ac"
) == 9 # Must use increasing columns
| Test | Why |
|---|---|
["acca","bbbb","caca"], "aba" |
Verifies official example |
["abba","baab"], "bab" |
Verifies another official example |
["abcd"], "abcd" |
Exact match case |
["abcd"], "abcde" |
Target longer than columns |
["aaaa","aaaa"], "aa" |
Large combinational count |
["abc","def"], "zzz" |
Impossible target |
["ab","ab","ab"], "ab" |
Duplicate words |
["xyz"], "x" |
Smallest meaningful target |
["aaa","aaa","aaa"], "aaa" |
Repeated character DP transitions |
["bac","bac","bac"], "ac" |
Confirms increasing column restriction |
Edge Cases
Target Longer Than Word Length
If the target length exceeds the word length, forming the target is impossible because columns must be used in strictly increasing order and each column can contribute at most one character.
A naive implementation might still attempt recursion or DP, wasting time. This solution immediately returns 0.
Missing Required Characters
Some targets may contain characters that never appear in the necessary columns.
For example:
words = ["abc", "def"]
target = "zzz"
In this case every frequency lookup becomes zero, so no DP transitions occur. The algorithm naturally leaves the final DP state at zero.
Repeated Characters Causing Double Counting
Targets such as:
target = "aaa"
can easily cause bugs if the DP updates are performed left to right.
If we updated forward, the same column could accidentally contribute multiple target positions in one iteration. The backward traversal prevents this by ensuring each column is used at most once per transition layer.
Extremely Large Answer Values
The number of combinations can become enormous because each column may contribute many valid choices.
Without modulo arithmetic, integer overflow would occur in many languages. Both implementations apply modulo 10^9 + 7 after every update, guaranteeing correctness even for the largest inputs.