Number of Ways to Form a Target String Given a Dictionary
HardPrompt
You are given a list of strings of the same length words and a string target.
Your task is to form target using the given words under the following rules:
targetshould be formed from left to right.- To form the
ithcharacter (0-indexed) oftarget, you can choose thekthcharacter of thejthstring inwordsiftarget[i] = words[j][k]. - Once you use the
kthcharacter of thejthstring ofwords, you can no longer use thexthcharacter of any string inwordswherex <= k. In other words, all characters to the left of or at indexkbecome unusuable for every string. - Repeat the process until you form the string
target.
Notice that you can use multiple characters from the same string in words provided the conditions above are met.
Return the number of ways to form target from words. Since the answer may be too large, return it modulo 109 + 7.
Example 1:
Input: words = ["acca","bbbb","caca"], target = "aba"
Output: 6
Explanation: There are 6 ways to form target.
"aba" -> index 0 ("acca"), index 1 ("bbbb"), index 3 ("caca")
"aba" -> index 0 ("acca"), index 2 ("bbbb"), index 3 ("caca")
"aba" -> index 0 ("acca"), index 1 ("bbbb"), index 3 ("acca")
"aba" -> index 0 ("acca"), index 2 ("bbbb"), index 3 ("acca")
"aba" -> index 1 ("caca"), index 2 ("bbbb"), index 3 ("acca")
"aba" -> index 1 ("caca"), index 2 ("bbbb"), index 3 ("caca")Example 2:
Input: words = ["abba","baab"], target = "bab"
Output: 4
Explanation: There are 4 ways to form target.
"bab" -> index 0 ("baab"), index 1 ("baab"), index 2 ("abba")
"bab" -> index 0 ("baab"), index 1 ("baab"), index 3 ("baab")
"bab" -> index 0 ("baab"), index 2 ("baab"), index 3 ("baab")
"bab" -> index 1 ("abba"), index 2 ("baab"), index 3 ("baab")
Constraints:
1 <= words.length <= 10001 <= words[i].length <= 1000- All strings in
wordshave the same length. 1 <= target.length <= 1000words[i]andtargetcontain only lowercase English letters.
Approaches
4 approaches with complexity analysis and trade-offs.
A straightforward recursive approach that explores all possible ways to form the target string. It directly translates the problem's rules into a recursive function without any optimization for overlapping subproblems.
Algorithm
- Define a recursive function
solve(i, k)that returns the number of ways to formtarget.substring(i)using characters fromwordsat columnsj >= k. - Base Case 1: If
i == target.length(), we have successfully formed the target. Return 1. - Base Case 2: If
k == words[0].length(), we have run out of columns but not finished the target. Return 0. - Recursive Step: The total ways are the sum of two choices:
- Skip column
k: Don't use any character from columnk. The number of ways issolve(i, k + 1). - Use column
k: Count the number of wordswords[j]wherewords[j][k] == target.charAt(i). Let this becount_matches. The number of ways iscount_matches * solve(i + 1, k + 1).
- Skip column
- The final answer is
solve(0, 0).
Walkthrough
This approach tackles the problem by defining a recursive function, say solve(i, k), which calculates the number of ways to form the suffix of the target string starting at index i (target[i:]) using characters from the words dictionary at or after column k.
The base cases for the recursion are:
- If
ireaches the end of thetargetstring (i == target.length), it means we have successfully formed the target. We return 1, signifying one valid way. - If
kreaches the end of the columns inwords(k == words[0].length) butihas not reached the end oftarget, it's impossible to continue. We return 0.
In the recursive step for solve(i, k), we consider two choices for forming target[i]:
- Skip column
k: We don't use any character from columnk. We move to the next column to find a match fortarget[i]. This contributessolve(i, k + 1)ways. - Use column
k: We find all characters in columnkofwordsthat matchtarget[i]. Let this count becount_matches. For each match, we've formedtarget[i], so we need to form the rest of the target (target[i+1:]) using columns strictly greater thank. This contributescount_matches * solve(i + 1, k + 1)ways.
The total number of ways for solve(i, k) is the sum of the ways from these two choices, taken modulo 10^9 + 7. The final answer is the result of the initial call solve(0, 0).
class Solution { private String[] words; private String target; private int wordLen; private int targetLen; private int MOD = 1_000_000_007; public int numWays(String[] words, String target) { this.words = words; this.target = target; this.wordLen = words[0].length(); this.targetLen = target.length(); return (int) solve(0, 0); } private long solve(int i, int k) { // Base case: successfully formed the target if (i == targetLen) { return 1; } // Base case: ran out of columns in words if (k == wordLen) { return 0; } // Option 1: Skip column k long ways = solve(i, k + 1); // Option 2: Use column k to match target[i] long countMatches = 0; for (String word : words) { if (word.charAt(k) == target.charAt(i)) { countMatches++; } } if (countMatches > 0) { ways = (ways + countMatches * solve(i + 1, k + 1)) % MOD; } return ways; }}Complexity
Time
O(W * 2^(m+n)), where `W` is `words.length`, `m` is `target.length`, and `n` is `words[0].length`. This is because the recursion tree can have a depth of `m+n` with a branching factor of 2, and at each node, we iterate through `W` words.
Space
O(m + n), for the recursion stack depth, where `m` is the length of `target` and `n` is the length of the words.
Trade-offs
Pros
Simple to conceptualize and implement as it directly follows the problem statement.
Cons
Extremely inefficient due to massive re-computation of the same subproblems.
Will result in a 'Time Limit Exceeded' (TLE) error for all but the smallest inputs.
This approach improves upon the brute-force recursion by storing the results of subproblems in a memoization table (a 2D array). This avoids redundant calculations for the same state (i, k), drastically reducing the time complexity from exponential to polynomial.
Algorithm
- Pre-compute a frequency table
counts[n][26]wherecounts[k][c]is the number of occurrences of charactercin columnkof all words. - Use a memoization table
memo[m][n]to store the results of subproblems, initialized to a sentinel value like -1. - Define a recursive function
solve(i, k)as in the brute-force approach. - Inside
solve(i, k), first check ifmemo[i][k]has a stored result. If so, return it. - Otherwise, compute the result using the same recurrence:
res = solve(i, k + 1) + counts[k][target[i]] * solve(i + 1, k + 1). - Store the computed result in
memo[i][k]before returning.
Walkthrough
The core recursive structure remains the same as the brute-force approach, where solve(i, k) calculates the number of ways to form target[i:] using columns from k onwards. However, we introduce a 2D array, memo[m][n], to store the results of solve(i, k) once they are computed.
Before any computation inside solve(i, k), we first check if memo[i][k] already contains a valid result (i.e., not the initial sentinel value). If it does, we immediately return the stored value, avoiding a costly recursive call.
If the result is not in the memoization table, we compute it using the recurrence relation. To make this faster, we first pre-process the words list to create a frequency map, counts[n][26], where counts[k][char - 'a'] stores how many times char appears in column k across all words. This avoids iterating through all words at every step. The recurrence then becomes:
ways = solve(i, k + 1) + (counts[k][target.charAt(i) - 'a'] * solve(i + 1, k + 1))
Once the result is computed, we store it in memo[i][k] before returning. This ensures that any future call to solve(i, k) will be an O(1) lookup.
class Solution { private long[][] memo; private long[][] counts; private String target; private int wordLen; private int targetLen; private int MOD = 1_000_000_007; public int numWays(String[] words, String target) { this.target = target; this.wordLen = words[0].length(); this.targetLen = target.length(); this.memo = new long[targetLen][wordLen]; for (long[] row : memo) { Arrays.fill(row, -1); } this.counts = new long[wordLen][26]; for (String word : words) { for (int k = 0; k < wordLen; k++) { counts[k][word.charAt(k) - 'a']++; } } return (int) solve(0, 0); } private long solve(int i, int k) { if (i == targetLen) { return 1; } if (k == wordLen) { return 0; } if (memo[i][k] != -1) { return memo[i][k]; } // Option 1: Skip column k long ways = solve(i, k + 1); // Option 2: Use column k long numMatches = counts[k][target.charAt(i) - 'a']; if (numMatches > 0) { ways = (ways + numMatches * solve(i + 1, k + 1)) % MOD; } return memo[i][k] = ways; }}Complexity
Time
O(W * n + m * n), where `W` is `words.length`, `m` is `target.length`, and `n` is `words[0].length`. `O(W * n)` is for pre-computing frequencies, and `O(m * n)` for the DP part, as there are `m * n` states, each taking `O(1)` to compute.
Space
O(m * n) for the memoization table. An additional `O(n)` for the frequency counts and `O(m+n)` for the recursion stack. The dominant factor is `O(m * n)`.
Trade-offs
Pros
Significantly more efficient than brute-force, with polynomial time complexity.
Guaranteed to compute each subproblem only once.
Often more intuitive to write than the bottom-up (iterative) DP approach.
Cons
May cause a stack overflow for very deep recursion, though the given constraints should be fine.
Has the overhead of recursive function calls compared to an iterative solution.
This is an iterative version of the memoized recursion. It builds the solution from the base cases up to the final answer, dp[0][0], using a 2D DP table. This avoids recursion and its associated overhead, which can lead to slightly better performance.
Algorithm
- Define a 2D DP table
dp[m+1][n+1], wheredp[i][k]stores the number of ways to formtarget.substring(i)using columns fromkonwards. - Pre-compute the frequency table
countsas in the memoization approach. - Initialize Base Cases:
dp[m][k] = 1for allkfrom0ton(one way to form an empty target suffix).dp[i][n] = 0for alli < m(no way to form a non-empty target with no columns).
- Fill the Table: Iterate
ifromm-1down to0andkfromn-1down to0. - For each
(i, k), calculatedp[i][k]using the recurrence:dp[i][k] = (dp[i][k+1] + counts[k][target[i]] * dp[i+1][k+1]) % MOD. - The final answer is
dp[0][0].
Walkthrough
This approach converts the top-down recursive solution into an iterative one. We use a 2D array dp[m+1][n+1], where dp[i][k] stores the same value as our recursive function: the number of ways to form the suffix target[i:] using characters from columns k onwards.
The table is filled starting from the base cases. The base cases are:
dp[m][k] = 1for allkfrom0ton. This signifies that an empty target suffix (target[m:]) can be formed in exactly one way (by picking no more characters), regardless of the available columns.dp[i][n] = 0fori < m. This signifies that if we have run out of columns (k=n) but still need to form characters, there are zero ways to succeed.
We then fill the rest of the table by iterating backwards from these base cases. The outer loop runs for i from m-1 down to 0, and the inner loop for k from n-1 down to 0. This order ensures that when we calculate dp[i][k], the values it depends on (dp[i][k+1] and dp[i+1][k+1]) have already been computed. The recurrence relation remains the same:
dp[i][k] = (dp[i][k+1] + counts[k][target.charAt(i) - 'a'] * dp[i+1][k+1]) % MOD
The final answer is stored in dp[0][0], which represents the number of ways to form the entire target (target[0:]) using all available columns (k=0 onwards).
class Solution { public int numWays(String[] words, String target) { int n = words[0].length(); int m = target.length(); int MOD = 1_000_000_007; long[][] counts = new long[n][26]; for (String word : words) { for (int k = 0; k < n; k++) { counts[k][word.charAt(k) - 'a']++; } } long[][] dp = new long[m + 1][n + 1]; // Base case: One way to form an empty target for (int k = 0; k <= n; k++) { dp[m][k] = 1; } for (int i = m - 1; i >= 0; i--) { for (int k = n - 1; k >= 0; k--) { // Option 1: Skip column k dp[i][k] = dp[i][k + 1]; // Option 2: Use column k long numMatches = counts[k][target.charAt(i) - 'a']; if (numMatches > 0) { dp[i][k] = (dp[i][k] + numMatches * dp[i + 1][k + 1]) % MOD; } } } return (int) dp[0][0]; }}Complexity
Time
O(W * n + m * n). `O(W * n)` for pre-computation and `O(m * n)` for filling the DP table.
Space
O(m * n) for the DP table, plus `O(n)` for the frequency counts.
Trade-offs
Pros
Avoids recursion overhead and the risk of stack overflow.
Can be slightly faster in practice than the top-down approach due to better cache locality and no function call overhead.
Cons
The order of loops and state transitions might be less intuitive to derive compared to the recursive solution.
Uses a large amount of memory (
O(m*n)) which can be optimized.
This approach optimizes the space complexity of the bottom-up DP. By observing the state transition dependencies, we see that computing the values for the current row i only requires information from the immediately previous row i+1. This allows us to reduce the space from a 2D table O(m*n) to a 1D array O(n), making it the most memory-efficient solution.
Algorithm
- Observe that computing
dp[i][k]only requires values from rowi(dp[i][k+1]) and rowi+1(dp[i+1][k+1]). - This allows us to reduce space by only keeping track of the current row (
i) and the previous row (i+1). - We can use a single 1D array
dpof sizen+1. - Initialize
dpwith all 1s, representing the base case fori=m. - Iterate
ifromm-1down to0. - For each
i, iteratekfromn-1down to0to update thedparray in place for rowi. - To compute the new
dp[k], we need the olddp[k+1](from rowi+1). We save this value in a temporary 'diagonal' variable beforedp[k+1]is overwritten. - The update rule is:
new_dp[k] = new_dp[k+1] + count * old_dp[k+1]. - After the loops complete,
dp[0]holds the final answer.
Walkthrough
The space optimization builds on the bottom-up DP approach. The recurrence dp[i][k] = dp[i][k+1] + counts[k][target[i]] * dp[i+1][k+1] shows that to compute any cell dp[i][k], we only need values from its own row (dp[i][...]) and the next row (dp[i+1][...]). This means we don't need to store the entire m x n table.
We can use a single 1D array, dp of size n+1, to represent one row of the DP table. We iterate i from m-1 down to 0. In each iteration, the dp array starts by holding the values for row i+1, and we update it in-place to hold the values for row i.
To do this with a single array, we must iterate k from n-1 down to 0. When we compute the new value for dp[k], the formula needs dp[i][k+1] and dp[i+1][k+1]. Since we iterate k downwards, dp[k+1] in our array has already been updated to its value for row i, which is exactly dp[i][k+1]. However, we also need dp[i+1][k+1], which was the value at dp[k+1] before it was updated. We can manage this by saving the required value from the previous row in a temporary variable (let's call it diagonal) before it gets overwritten.
class Solution { public int numWays(String[] words, String target) { int n = words[0].length(); int m = target.length(); int MOD = 1_000_000_007; long[][] counts = new long[n][26]; for (String word : words) { for (int k = 0; k < n; k++) { counts[k][word.charAt(k) - 'a']++; } } long[] dp = new long[n + 1]; // Base case for i = m: dp[k] = 1 for all k Arrays.fill(dp, 1); for (int i = m - 1; i >= 0; i--) { // This variable will hold the value of dp[i+1][k+1] as we iterate k downwards. // It starts as dp[i+1][n], which is 1. long diagonal = 1; for (int k = n - 1; k >= 0; k--) { // Save dp[i+1][k] before it's overwritten. long temp = dp[k]; // dp[k+1] now holds dp[i][k+1]. // 'diagonal' holds dp[i+1][k+1]. long ways = dp[k + 1]; // Option 1: Skip column k long numMatches = counts[k][target.charAt(i) - 'a']; if (numMatches > 0) { ways = (ways + numMatches * diagonal) % MOD; // Option 2: Use column k } dp[k] = ways; // Update diagonal for the next iteration of k (k-1). // The new diagonal will be dp[i+1][k], which we saved in temp. diagonal = temp; } } return (int) dp[0]; }}Complexity
Time
O(W * n + m * n). The asymptotic time complexity remains the same as the unoptimized DP solutions.
Space
O(n), for the DP array and the frequency counts. This is a significant improvement over `O(m * n)` when `m` is large.
Trade-offs
Pros
Most efficient in terms of space complexity.
Retains the optimal time efficiency of the other DP approaches.
Cons
The logic for in-place updates using a single array can be tricky to get right and is less straightforward to read and debug.
Solutions
Solution
class Solution { private int m ; private int n ; private String target ; private Integer [][] f ; private int [][] cnt ; private final int mod = ( int ) 1 e9 + 7 ; public int numWays ( String [] words , String target ) { m = target . length (); n = words [ 0 ]. length (); f = new Integer [ m ][ n ]; this . target = target ; cnt = new int [ n ][ 26 ]; for ( var w : words ) { for ( int j = 0 ; j < n ; ++ j ) { cnt [ j ][ w . charAt ( j ) - 'a' ]++; } } return dfs ( 0 , 0 ); } private int dfs ( int i , int j ) { if ( i >= m ) { return 1 ; } if ( j >= n ) { return 0 ; } if ( f [ i ][ j ] != null ) { return f [ i ][ j ]; } long ans = dfs ( i , j + 1 ); ans += 1L * dfs ( i + 1 , j + 1 ) * cnt [ j ][ target . charAt ( i ) - 'a' ]; ans %= mod ; return f [ i ][ j ] = ( int ) ans ; } }class Solution { public: int numWays ( vector < string >& words , string target ) { const int mod = 1e9 + 7 ; int m = target . size (), n = words [ 0 ]. size (); vector < vector < int >> cnt ( n , vector < int > ( 26 )); for ( auto & w : words ) { for ( int j = 0 ; j < n ; ++ j ) { ++ cnt [ j ][ w [ j ] - 'a' ]; } } int f [ m ][ n ]; memset ( f , - 1 , sizeof ( f )); function < int ( int , int ) > dfs = [ & ]( int i , int j ) -> int { if ( i >= m ) { return 1 ; } if ( j >= n ) { return 0 ; } if ( f [ i ][ j ] != - 1 ) { return f [ i ][ j ]; } int ans = dfs ( i , j + 1 ); ans = ( ans + 1LL * dfs ( i + 1 , j + 1 ) * cnt [ j ][ target [ i ] - 'a' ]) % mod ; return f [ i ][ j ] = ans ; }; return dfs ( 0 , 0 ); } };class Solution : def numWays ( self , words : List [ str ], target : str ) -> int : @ cache def dfs ( i : int , j : int ) -> int : if i >= m : return 1 if j >= n : return 0 ans = dfs ( i + 1 , j + 1 ) * cnt [ j ][ ord ( target [ i ]) - ord ( 'a' )] ans = ( ans + dfs ( i , j + 1 )) % mod return ans m , n = len ( target ), len ( words [ 0 ]) cnt = [[ 0 ] * 26 for _ in range ( n )] for w in words : for j , c in enumerate ( w ): cnt [ j ][ ord ( c ) - ord ( 'a' )] += 1 mod = 10 ** 9 + 7 return dfs ( 0 , 0 )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.