1639. Number of Ways to Form a Target String Given a Dictionary
Problem Description
You have a list of strings words
where all strings have the same length, and you need to form a target string target
by selecting characters from these strings following specific rules.
Rules for forming the target:
-
Build from left to right: You must construct
target
character by character from index 0 to the end. -
Character selection: To form the
i
-th character oftarget
(0-indexed), you can pick thek
-th character from any stringj
inwords
, as long aswords[j][k]
equalstarget[i]
. -
Position restriction: Once you use a character at position
k
from any string inwords
, all characters at positions0
throughk
become permanently unavailable across all strings inwords
. This means you can only move forward in the character positions. -
Multiple uses allowed: You can use multiple characters from the same string in
words
, as long as you follow the position restriction rule.
Example visualization:
- If
words = ["acca", "bbbb", "caca"]
and you want to formtarget = "aba"
- You could pick 'a' from position 0 of
words[0]
fortarget[0]
- Then pick 'b' from position 2 of
words[1]
fortarget[1]
(positions 0-2 are now unusable) - Finally pick 'a' from position 3 of
words[2]
fortarget[2]
The task is to count how many different ways you can form the complete target
string following these rules. Since the answer can be very large, return it modulo 10^9 + 7
.
Intuition
When we look at this problem, we need to make a series of choices: for each character in target
, we need to decide which position across all strings in words
to pick it from. The key constraint is that once we use position k
, we can't use any position ≤ k
again.
This naturally leads us to think about dynamic programming or recursion with memoization. At each step, we have two indices to track:
i
: which character oftarget
we're trying to matchj
: the current position in the strings ofwords
we're considering
For any state (i, j)
, we have two choices:
- Skip position
j
: We don't use any character from positionj
of any string, and move to positionj+1
while still trying to matchtarget[i]
- Use position
j
: If there are characters at positionj
that matchtarget[i]
, we can use them and move to matchtarget[i+1]
from positionj+1
onwards
The second choice brings up an important observation: since all strings in words
have the same length, at any position j
, multiple strings might have the character we need. If 3 strings have 'a' at position j
and we need 'a' for target[i]
, we have 3 different ways to pick it.
This suggests we should preprocess the data: for each position j
and each character c
, count how many strings have character c
at position j
. This gives us cnt[j][c]
- the frequency of character c
at position j
across all strings.
With this preprocessing, our recursive formula becomes clearer:
dfs(i, j)
= number of ways to formtarget[i:]
starting from positionj
inwords
dfs(i, j) = dfs(i, j+1) + cnt[j][target[i]] * dfs(i+1, j+1)
The first term represents skipping position j
, and the second term represents using position j
(with cnt[j][target[i]]
different ways to do so, then continuing from position j+1
for the next character).
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a top-down dynamic programming approach with memoization using the insights from our intuition.
Step 1: Preprocessing
First, we build a frequency table cnt
where cnt[j][c]
stores how many strings in words
have character c
at position j
:
cnt = [[0] * 26 for _ in range(n)] # n is the length of each string
for w in words:
for j, c in enumerate(w):
cnt[j][ord(c) - ord('a')] += 1
This preprocessing takes O(len(words) × n)
time and allows us to quickly look up how many ways we can pick a specific character from a specific position.
Step 2: Recursive Function with Memoization
We define dfs(i, j)
which returns the number of ways to form target[i:]
when we can only use characters from positions j
onwards in words
:
@cache
def dfs(i: int, j: int) -> int:
if i >= m: # All characters of target are matched
return 1
if j >= n: # No more positions available in words
return 0
# Option 1: Use position j to match target[i]
ans = dfs(i + 1, j + 1) * cnt[j][ord(target[i]) - ord('a')]
# Option 2: Skip position j
ans = (ans + dfs(i, j + 1)) % mod
return ans
Base Cases:
i >= m
: We've successfully matched all characters intarget
, so return1
j >= n
: We've exhausted all positions inwords
but haven't matched all oftarget
, so return0
Recursive Cases:
- Use position
j
: If we use positionj
to matchtarget[i]
, we havecnt[j][target[i]]
ways to do so. After using positionj
, we move totarget[i+1]
and can only use positions fromj+1
onwards. This gives uscnt[j][target[i]] × dfs(i+1, j+1)
ways. - Skip position
j
: We don't use positionj
at all and try to matchtarget[i]
from positionj+1
onwards. This gives usdfs(i, j+1)
ways.
The total number of ways is the sum of both options, taken modulo 10^9 + 7
.
Time Complexity: O(m × n)
where m = len(target)
and n = len(words[0])
, since we have at most m × n
unique states and each state is computed once due to memoization.
Space Complexity: O(m × n)
for the memoization cache plus O(26 × n)
for the preprocessing array.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Given:
words = ["acca", "bbbb", "caca"]
target = "aba"
Step 1: Preprocessing
First, we build the frequency table cnt[j][c]
that counts how many strings have character c
at position j
:
Position 0: a appears 1 time (in "acca") b appears 1 time (in "bbbb") c appears 1 time (in "caca") Position 1: a appears 1 time (in "caca") b appears 1 time (in "bbbb") c appears 1 time (in "acca") Position 2: a appears 0 times b appears 1 time (in "bbbb") c appears 2 times (in "acca", "caca") Position 3: a appears 2 times (in "acca", "caca") b appears 1 time (in "bbbb") c appears 0 times
Step 2: Recursive Calculation
Now we calculate dfs(0, 0)
- ways to form target[0:]
= "aba" starting from position 0.
dfs(0, 0)
: Form "aba" from position 0
- Option 1: Use position 0 for 'a' →
cnt[0]['a'] × dfs(1, 1)
=1 × dfs(1, 1)
- Option 2: Skip position 0 →
dfs(0, 1)
- Result:
dfs(1, 1) + dfs(0, 1)
dfs(1, 1)
: Form "ba" from position 1
- Option 1: Use position 1 for 'b' →
cnt[1]['b'] × dfs(2, 2)
=1 × dfs(2, 2)
- Option 2: Skip position 1 →
dfs(1, 2)
- Result:
dfs(2, 2) + dfs(1, 2)
dfs(2, 2)
: Form "a" from position 2
- Option 1: Use position 2 for 'a' →
cnt[2]['a'] × dfs(3, 3)
=0 × 1
=0
- Option 2: Skip position 2 →
dfs(2, 3)
- Result:
0 + dfs(2, 3)
dfs(2, 3)
: Form "a" from position 3
- Option 1: Use position 3 for 'a' →
cnt[3]['a'] × dfs(3, 4)
=2 × 1
=2
- Option 2: Skip position 3 →
dfs(2, 4)
=0
(no more positions) - Result:
2 + 0 = 2
Working backwards:
dfs(2, 2) = 0 + 2 = 2
dfs(1, 2)
: Form "ba" from position 2- Use position 2 for 'b':
1 × dfs(2, 3)
=1 × 2
=2
- Skip position 2:
dfs(1, 3)
=0
(can't form 'b' from position 3) - Result:
2 + 0 = 2
- Use position 2 for 'b':
dfs(1, 1) = 2 + 2 = 4
For dfs(0, 1)
: Form "aba" from position 1
- Eventually evaluates to
0
(we can't form all of "aba" starting from position 1)
Final Answer: dfs(0, 0) = 4 + 0 = 4
The 4 ways are:
words[0][0]='a', words[1][1]='b', words[0][3]='a'
words[0][0]='a', words[1][1]='b', words[2][3]='a'
words[0][0]='a', words[1][2]='b', words[0][3]='a'
words[0][0]='a', words[1][2]='b', words[2][3]='a'
Solution Implementation
1class Solution:
2 def numWays(self, words: List[str], target: str) -> int:
3 """
4 Count the number of ways to form the target string by selecting characters
5 from columns of the words array.
6
7 Args:
8 words: List of strings with equal length
9 target: Target string to form
10
11 Returns:
12 Number of ways to form target modulo 10^9 + 7
13 """
14 from functools import cache
15
16 # Get dimensions
17 target_length = len(target)
18 word_length = len(words[0])
19
20 # Count frequency of each character at each column position
21 # char_count[column][character] = frequency of character at column
22 char_count = [[0] * 26 for _ in range(word_length)]
23
24 for word in words:
25 for column_idx, char in enumerate(word):
26 char_index = ord(char) - ord('a')
27 char_count[column_idx][char_index] += 1
28
29 # Modulo value for large numbers
30 MOD = 10**9 + 7
31
32 @cache
33 def dp(target_idx: int, column_idx: int) -> int:
34 """
35 Dynamic programming function to count ways to form target[target_idx:]
36 using columns starting from column_idx.
37
38 Args:
39 target_idx: Current index in target string
40 column_idx: Current column index in words
41
42 Returns:
43 Number of ways to form the remaining target string
44 """
45 # Base case: Successfully formed the entire target string
46 if target_idx >= target_length:
47 return 1
48
49 # Base case: No more columns available but target not complete
50 if column_idx >= word_length:
51 return 0
52
53 # Option 1: Use current column to match target[target_idx]
54 # Get the character we need from target
55 needed_char_index = ord(target[target_idx]) - ord('a')
56
57 # Count ways if we use this column (multiply by frequency of needed character)
58 use_column = dp(target_idx + 1, column_idx + 1) * char_count[column_idx][needed_char_index]
59
60 # Option 2: Skip current column
61 skip_column = dp(target_idx, column_idx + 1)
62
63 # Total ways is sum of both options
64 total_ways = (use_column + skip_column) % MOD
65
66 return total_ways
67
68 # Start the dynamic programming from beginning of both target and columns
69 return dp(0, 0)
70
1class Solution {
2 // Dimensions and target string
3 private int targetLength;
4 private int wordLength;
5 private String target;
6
7 // Memoization table for dynamic programming
8 private Integer[][] memo;
9
10 // Frequency count of each character at each position across all words
11 private int[][] charFrequency;
12
13 // Modulo value for preventing integer overflow
14 private final int MOD = (int) 1e9 + 7;
15
16 /**
17 * Calculates the number of ways to form the target string using characters
18 * from the given words array at specific positions.
19 *
20 * @param words Array of words with equal length
21 * @param target Target string to form
22 * @return Number of ways to form target modulo 10^9 + 7
23 */
24 public int numWays(String[] words, String target) {
25 // Initialize dimensions
26 this.targetLength = target.length();
27 this.wordLength = words[0].length();
28 this.target = target;
29
30 // Initialize memoization table
31 memo = new Integer[targetLength][wordLength];
32
33 // Build frequency table: charFrequency[position][character]
34 // Counts how many times each character appears at each position
35 charFrequency = new int[wordLength][26];
36 for (String word : words) {
37 for (int position = 0; position < wordLength; position++) {
38 charFrequency[position][word.charAt(position) - 'a']++;
39 }
40 }
41
42 // Start DFS from position 0 in both target and words
43 return dfs(0, 0);
44 }
45
46 /**
47 * Recursive DFS with memoization to count ways to form target.
48 *
49 * @param targetIndex Current index in the target string
50 * @param wordPosition Current position in the words being considered
51 * @return Number of ways to form the remaining target string
52 */
53 private int dfs(int targetIndex, int wordPosition) {
54 // Base case: Successfully formed the entire target string
55 if (targetIndex >= targetLength) {
56 return 1;
57 }
58
59 // Base case: No more positions left in words to form target
60 if (wordPosition >= wordLength) {
61 return 0;
62 }
63
64 // Return memoized result if already computed
65 if (memo[targetIndex][wordPosition] != null) {
66 return memo[targetIndex][wordPosition];
67 }
68
69 // Option 1: Skip current position in words
70 long ways = dfs(targetIndex, wordPosition + 1);
71
72 // Option 2: Use current position if it has the required character
73 // Multiply by frequency since we can choose from any word with that character
74 char requiredChar = target.charAt(targetIndex);
75 int frequency = charFrequency[wordPosition][requiredChar - 'a'];
76 ways += (long) dfs(targetIndex + 1, wordPosition + 1) * frequency;
77
78 // Apply modulo to prevent overflow
79 ways %= MOD;
80
81 // Store result in memoization table and return
82 memo[targetIndex][wordPosition] = (int) ways;
83 return memo[targetIndex][wordPosition];
84 }
85}
86
1class Solution {
2public:
3 int numWays(vector<string>& words, string target) {
4 const int MOD = 1e9 + 7;
5 int targetLength = target.size();
6 int wordLength = words[0].size();
7
8 // Count frequency of each character at each position across all words
9 // charFrequency[position][character] = count of character at position
10 vector<vector<int>> charFrequency(wordLength, vector<int>(26, 0));
11 for (const string& word : words) {
12 for (int position = 0; position < wordLength; ++position) {
13 ++charFrequency[position][word[position] - 'a'];
14 }
15 }
16
17 // Memoization table for dynamic programming
18 // dp[targetIndex][wordPosition] = number of ways to form target[targetIndex:]
19 // starting from wordPosition in words
20 int dp[targetLength][wordLength];
21 memset(dp, -1, sizeof(dp));
22
23 // Recursive function with memoization
24 function<int(int, int)> calculateWays = [&](int targetIndex, int wordPosition) -> int {
25 // Base case: successfully formed the entire target string
26 if (targetIndex >= targetLength) {
27 return 1;
28 }
29
30 // Base case: no more positions available in words to pick from
31 if (wordPosition >= wordLength) {
32 return 0;
33 }
34
35 // Return memoized result if already computed
36 if (dp[targetIndex][wordPosition] != -1) {
37 return dp[targetIndex][wordPosition];
38 }
39
40 // Option 1: Skip current position in words
41 int totalWays = calculateWays(targetIndex, wordPosition + 1);
42
43 // Option 2: Use current position if it matches target character
44 // Multiply by frequency of matching character at current position
45 long long waysUsingCurrentPosition = 1LL * calculateWays(targetIndex + 1, wordPosition + 1)
46 * charFrequency[wordPosition][target[targetIndex] - 'a'];
47 totalWays = (totalWays + waysUsingCurrentPosition) % MOD;
48
49 // Store and return the result
50 return dp[targetIndex][wordPosition] = totalWays;
51 };
52
53 return calculateWays(0, 0);
54 }
55};
56
1/**
2 * Counts the number of ways to form a target string by selecting characters
3 * from the same index across multiple words
4 * @param words - Array of strings with equal length
5 * @param target - The target string to form
6 * @returns Number of ways to form the target string modulo 10^9 + 7
7 */
8function numWays(words: string[], target: string): number {
9 const targetLength: number = target.length;
10 const wordLength: number = words[0].length;
11 const MOD: number = 1e9 + 7;
12
13 // dp[i][j] represents number of ways to form first i characters of target
14 // using first j columns of words
15 const dp: number[][] = Array(targetLength + 1)
16 .fill(0)
17 .map(() => Array(wordLength + 1).fill(0));
18
19 // Base case: empty target can be formed in 1 way (by selecting nothing)
20 for (let columnIndex = 0; columnIndex <= wordLength; columnIndex++) {
21 dp[0][columnIndex] = 1;
22 }
23
24 // characterCount[j][c] stores count of character c at column j across all words
25 const characterCount: number[][] = Array(wordLength)
26 .fill(0)
27 .map(() => Array(26).fill(0));
28
29 // Precompute character frequencies at each column position
30 for (const word of words) {
31 for (let columnIndex = 0; columnIndex < wordLength; columnIndex++) {
32 const charIndex: number = word.charCodeAt(columnIndex) - 97; // Convert 'a'-'z' to 0-25
33 characterCount[columnIndex][charIndex]++;
34 }
35 }
36
37 // Fill the DP table
38 for (let targetIndex = 1; targetIndex <= targetLength; targetIndex++) {
39 for (let columnIndex = 1; columnIndex <= wordLength; columnIndex++) {
40 // Option 1: Skip current column (don't use column j-1)
41 dp[targetIndex][columnIndex] = dp[targetIndex][columnIndex - 1];
42
43 // Option 2: Use current column if it contains the required character
44 const requiredCharIndex: number = target.charCodeAt(targetIndex - 1) - 97;
45 const charOccurrences: number = characterCount[columnIndex - 1][requiredCharIndex];
46
47 dp[targetIndex][columnIndex] += dp[targetIndex - 1][columnIndex - 1] * charOccurrences;
48 dp[targetIndex][columnIndex] %= MOD;
49 }
50 }
51
52 return dp[targetLength][wordLength];
53}
54
Time and Space Complexity
Time Complexity: O(m × n + k × n)
The algorithm consists of two main parts:
-
Preprocessing phase: Building the
cnt
array takesO(k × n)
time, wherek
is the number of words in thewords
array andn
is the length of each word. We iterate through all characters in all words. -
Dynamic Programming phase: The
dfs
function with memoization explores at mostm × n
unique states, wherem
is the length of the target string andn
is the length of each word. Each state(i, j)
is computed only once due to the@cache
decorator. For each state, the computation takesO(1)
time.
Therefore, the overall time complexity is O(k × n + m × n)
. If we assume k
is relatively small or consider it as part of the input preprocessing, the dominant term becomes O(m × n)
.
Space Complexity: O(m × n + n)
The space usage includes:
-
Memoization cache: The
@cache
decorator stores up tom × n
states, each takingO(1)
space, resulting inO(m × n)
space. -
Count array: The
cnt
array has dimensionsn × 26
, which isO(n)
space since 26 is a constant. -
Recursion stack: In the worst case, the recursion depth can be
O(m + n)
.
The dominant term is O(m × n)
from the memoization cache, making the overall space complexity O(m × n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow When Computing Products
The Pitfall:
When calculating dp(target_idx + 1, column_idx + 1) * char_count[column_idx][needed_char_index]
, the multiplication can produce very large numbers that exceed Python's integer limits in other languages or cause issues even before applying the modulo operation. While Python handles arbitrary precision integers, in languages like Java or C++, this would cause overflow. Even in Python, performing modulo operations too late can impact performance.
Example of the Issue:
# Problematic approach - modulo applied after multiplication use_column = dp(target_idx + 1, column_idx + 1) * char_count[column_idx][needed_char_index] total_ways = (use_column + skip_column) % MOD # Modulo applied too late
The Solution: Apply modulo operations immediately after each arithmetic operation to keep numbers manageable:
# Better approach - apply modulo after each operation use_column = (dp(target_idx + 1, column_idx + 1) * char_count[column_idx][needed_char_index]) % MOD skip_column = dp(target_idx, column_idx + 1) % MOD total_ways = (use_column + skip_column) % MOD
2. Incorrect Understanding of the Position Restriction
The Pitfall:
A common misunderstanding is thinking that once you use a character from a specific string at position k
, you can't use that particular string anymore. The actual rule is that positions 0 through k
become unavailable across ALL strings, not just the one you picked from.
Incorrect Mental Model:
# Wrong: Tracking which strings have been used
def dp(target_idx, column_idx, used_strings):
# This would incorrectly track individual string usage
pass
Correct Understanding: The restriction is column-based (position-based) across all strings:
# Correct: Only tracking column position
def dp(target_idx, column_idx):
# When we use column_idx, all columns 0 to column_idx become unavailable
# We can only proceed with column_idx + 1 onwards
use_column = dp(target_idx + 1, column_idx + 1) * char_count[column_idx][needed_char_index]
3. Off-by-One Errors in Index Handling
The Pitfall: Mixing up whether indices are inclusive or exclusive, especially in base cases:
Common Mistake:
# Wrong base case checks if target_idx > target_length: # Should be >= return 1 if column_idx > word_length: # Should be >= return 0
Correct Implementation:
# Correct base case checks if target_idx >= target_length: # We've processed all characters (0 to target_length-1) return 1 if column_idx >= word_length: # No more columns available (0 to word_length-1) return 0
4. Forgetting to Handle the Case Where Character Doesn't Exist
The Pitfall:
Not considering that char_count[column_idx][needed_char_index]
might be 0, meaning the needed character doesn't exist at that column position.
Why It's Not Obviously Wrong: The code still works because multiplying by 0 gives 0 ways, which is correct. However, this can lead to unnecessary recursive calls and reduced performance.
Optimization:
# More efficient - skip recursion if character doesn't exist
needed_char_index = ord(target[target_idx]) - ord('a')
if char_count[column_idx][needed_char_index] > 0:
use_column = (dp(target_idx + 1, column_idx + 1) *
char_count[column_idx][needed_char_index]) % MOD
else:
use_column = 0 # Skip unnecessary recursion
skip_column = dp(target_idx, column_idx + 1)
return (use_column + skip_column) % MOD
A heap is a ...?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!