1639. Number of Ways to Form a Target String Given a Dictionary - Explanation

Problem Link



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

  1. Define a recursive function dfs(i, k) where i is the current target index and k is the current column index in the words.
  2. Base cases:
    • If i == n (target length), we formed the entire target, return 1.
    • If k == m (word length), we ran out of columns, return 0.
  3. Count the ways by skipping column k (call dfs(i, k + 1)).
  4. For each word where word[k] matches target[i], add dfs(i + 1, k + 1) to res.
  5. 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: O(Nm)O(N ^ m)
  • Space complexity: O(m)O(m)

Where NN is the number of words, mm is the length of each word, and nn is the length of the targettarget 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

  1. Precompute a frequency table cnt[k][c] storing how many words have character c at column k.
  2. Define dfs(i, k) with memoization:
    • Base cases same as before.
    • Skip column k by adding dfs(i, k + 1).
    • Use column k by multiplying cnt[k][target[i]] with dfs(i + 1, k + 1).
  3. Return dfs(0, 0) modulo 10^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: O(m(n+N))O(m * (n + N))
  • Space complexity: O(nm)O(n * m)

Where NN is the number of words, mm is the length of each word, and nn is the length of the targettarget 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

  1. Precompute the character frequency table.
  2. Create a DP table of size (n+1) x (m+1) initialized to 0.
  3. Set dp[n][m] = 1 (base case: empty target from the end is valid).
  4. Iterate backward through positions:
    • For each (i, k), set dp[i][k] = dp[i][k+1] (skip column).
    • If i < n, add cnt[k][target[i]] * dp[i+1][k+1] (use column).
  5. Return dp[0][0] 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])

        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: O(m(n+N))O(m * (n + N))
  • Space complexity: O(nm)O(n * m)

Where NN is the number of words, mm is the length of each word, and nn is the length of the targettarget 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

  1. Precompute the character frequency table.
  2. Use a 1D DP array of size m + 1.
  3. Iterate from i = n down to 0, and for each row, iterate through columns in reverse.
  4. Maintain the value from the next row using a temporary variable.
  5. Return dp[0] 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])

        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: O(m(n+N))O(m * (n + N))
  • Space complexity: O(m)O(m)

Where NN is the number of words, mm is the length of each word, and nn is the length of the targettarget 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 O(N)O(N) 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.