#108 🔢 1639. Number of Ways to Form a Target String Given a Dictionary 🚀🧠
Let’s dive into an exciting problem that challenges our understanding of dynamic programming! 🚀 We’re tasked with figuring out the number of ways to form a target string using characters from a list of strings, under some specific constraints. Ready to explore this fascinating challenge? Let’s go! 🎉
📜 Problem Statement
You are given:
- A list of strings
words
, where each string has the same length. - A string
target
.
Your task is to determine how many ways you can form the target
string by selecting characters from words
. The rules are as follows:
- Target formation: Form the
target
string from left to right. - Character selection: To form the
i-th
character oftarget
, you can pick any character from the same column (k-th
position) of any string inwords
if it matches the character intarget[i]
. - Index restriction: Once you use a character at index
k
from any string inwords
, all characters at indices ≤k
in every string become unavailable. - Repetition allowed: You can use multiple characters from the same string.
Return the number of ways to form the target
string, modulo \(10^9 + 7\).
🖼️ Examples
Example 1
Input:
words = ["acca", "bbbb", "caca"]
target = "aba"
Output:
6
Explanation:
There are six ways to form "aba"
:
"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 four ways to form "bab"
:
"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”)
🧠 Thought Process
Intuition and Thought Process 🧠
Before diving into the solution, let’s take a step back and understand the intuition behind solving the problem. The problem asks us to find the number of ways to form a target string from a list of words. This might seem challenging at first due to the constraints, but breaking it into smaller subproblems reveals an efficient approach using dynamic programming.
Step 1: Understand the Problem 🔍
The primary challenge is:
- Forming the target string: Each character in the target can be chosen from any string in the
words
list, but the rules for selection are strict:- Characters in each word are accessed left-to-right.
- Once a character is used, all earlier indices in that word are “locked” and cannot be reused.
This means:
- To form a character in the target, we need to count how many times that character appears at a given index across all words.
- We need to combine these counts recursively to determine the total number of ways to form the entire target.
Step 2: Break Down the Problem 🧩
To form the target[i]
using the characters in words
, we must decide:
- Skip the current index
k
inwords
and move to the next index. This decision ensures we leave the current column untouched for forming later parts of the target. - Use the current column to form
target[i]
. Here, the number of options depends on how many timestarget[i]
appears in columnk
ofwords
. We then recursively attempt to form the rest of the target (target[i+1:]
) from the remaining columns.
Step 3: Dynamic Programming (DP) Insight 💡
Dynamic programming helps us efficiently compute overlapping subproblems:
- Define
dp[i][k]
as the number of ways to form the substringtarget[i:]
using columnsk
to the end ofwords
. - Base cases:
- If
i == len(target)
, we’ve formed the entire target, so return 1. - If
k == len(words[0])
, we’ve exhausted the columns and cannot form the target, so return 0.
- If
- Recurrence relation:
- Skip the current column:
dp[i][k] += dp[i][k+1]
. - Use the current column if
target[i]
exists in columnk
:dp[i][k] += frequency[target[i]][k] * dp[i+1][k+1]
.
- Skip the current column:
The step
dp[(i, k)] += cnt[(k, c)] * dfs(i + 1, k + 1)
is the heart of the dynamic programming solution. It combines two essential concepts:
- Counting Possibilities:
- The term
cnt[(k, c)]
represents the number of times the characterc
(which istarget[i]
) appears in columnk
of thewords
matrix. This gives us the number of ways we can choose the current character from this column.
- The term
- Recursing to the Next State:
- The function
dfs(i + 1, k + 1)
computes the number of ways to form the remainder of the target string (target[i+1:]
) using the columns starting fromk + 1
.
- The function
Why Multiplication? 🤔
The multiplication combines these two quantities because they are independent events. Here’s the reasoning:
- If
cnt[(k, c)]
is the number of ways to pick the current charactertarget[i]
from columnk
, and dfs(i + 1, k + 1)
is the number of ways to form the rest of the target string (target[i+1:]
) from the subsequent columns (k + 1
onwards),
then the total number of ways to form the target string from this step is the product of these two numbers. This is because:
- Each choice of
target[i]
from columnk
can independently lead to forming the rest of the target indfs(i + 1, k + 1)
. - For every valid way to pick
target[i]
, there aredfs(i + 1, k + 1)
ways to complete the rest of the target string.
Example to Illustrate Multiplication
Input:
words = ["acca", "bbbb", "caca"]
target = "aba"
Precomputed Frequency Table:
Column (Index) | ‘a’ | ‘b’ | ‘c’ |
---|---|---|---|
0 | 2 | 0 | 1 |
1 | 0 | 3 | 0 |
2 | 1 | 0 | 2 |
3 | 1 | 0 | 1 |
Case: i = 0, k = 0
(Forming “aba”, starting at column 0)
- We are forming the first character
'a'
of the target:cnt[(0, 'a')] = 2
(two ‘a’s in column 0).- To compute the number of ways to form the rest of the target (
"ba"
) starting from column 1, we recursively calldfs(1, 1)
.
Calculation:
The number of ways to form "aba"
starting from column 0 is:
Here, 2
accounts for the two ways to choose 'a'
in column 0, and for each of those choices, we multiply by the number of ways to form "ba"
starting from column 1.
Why Not Addition?
If we were to use addition instead of multiplication, it would imply that the possibilities are mutually exclusive instead of independent. However, in this problem, picking each occurrence of the current character does not exclude other choices for forming the rest of the target.
General Pattern in DP Problems
This step of combining counts of current possibilities (cnt[(k, c)]
) with results of future possibilities (dfs(i + 1, k + 1)
) through multiplication is a common technique in problems where:
- Choices at one step are independent of the choices in subsequent steps.
- Each choice contributes to all possible outcomes of the remaining problem.
This is a hallmark of problems involving combinatorics and recursive enumeration.
Key Observations 🔍
- Precompute Frequencies: Instead of repeatedly counting occurrences of
target[i]
in each column, precompute a frequency tablefreq
wherefreq[c][k]
is the count of characterc
in columnk
. - Recursive Transition: Each state depends on two smaller subproblems: one where the current column is skipped, and one where it contributes to forming the target.
- Modulo Constraint: Since the answer can be very large, every addition is computed modulo \(10^9 + 7\).
💡 Basic Solution (Brute Force)
Approach:
- For each character of
target
, iterate through all columns ofwords
. - Count how many characters in the current column match the current
target
character. - Recursively find all possible ways to form the remaining part of
target
.
Code:
def numWays(words, target):
MOD = 10**9 + 7
def countWays(i, k):
if i == len(target): # Successfully formed the target
return 1
if k == len(words[0]): # Ran out of columns
return 0
# Count the number of matches for target[i] in column k
matches = sum(1 for word in words if word[k] == target[i])
# Skip the current column or use it and move to the next character
return (countWays(i, k + 1) + matches * countWays(i + 1, k + 1)) % MOD
return countWays(0, 0)
Time Complexity:
The brute force approach is highly inefficient due to repeated work. Each recursive call has two branches, leading to exponential complexity:
- \(O(2^{\text{len(words[0])}} \cdot \text{len(target)})\).
⚡ Optimized Solution (Dynamic Programming)
We can improve the brute force approach by using memoization to store the results of subproblems. This avoids redundant calculations.
Key Ideas:
- Frequency Table: Precompute the frequency of each character in every column of
words
. - Memoization: Store the results of
(i, k)
in a dictionarydp
to avoid recalculating. - Recursion: Use a recursive function to calculate the number of ways to form the target using columns starting from
k
.
Code:
from collections import defaultdict
class Solution:
def numWays(self, words, target):
MOD = 10**9 + 7
cnt = defaultdict(int)
# Build frequency table for each column
for word in words:
for i, c in enumerate(word):
cnt[(i, c)] += 1
dp = {}
def dfs(i, k):
if i == len(target): # Successfully formed the target
return 1
if k == len(words[0]): # Ran out of columns
return 0
if (i, k) in dp: # Return memoized result
return dp[(i, k)]
# Option 1: Skip the current column
dp[(i, k)] = dfs(i, k + 1)
# Option 2: Use the current column if it matches target[i]
c = target[i]
if (k, c) in cnt:
dp[(i, k)] += cnt[(k, c)] * dfs(i + 1, k + 1)
dp[(i, k)] %= MOD
return dp[(i, k)]
return dfs(0, 0)
Time Complexity:
- Building Frequency Table: \(O(n \cdot m)\), where \(n\) is the number of strings in
words
and \(m\) is their length. - Dynamic Programming: \(O(t \cdot m)\), where \(t\) is the length of
target
.
Total time complexity: \(O(n \cdot m + t \cdot m)\).
Space Complexity:
- Frequency table: \(O(26 \cdot m)\) for storing character counts.
- DP table: \(O(t \cdot m)\).
🔍 Example Walkthrough
Input:
words = ["abba", "baab"]
target = "bab"
- Frequency Table:
Column | ‘a’ | ‘b’ |
---|---|---|
0 | 1 | 1 |
1 | 1 | 1 |
2 | 1 | 1 |
3 | 1 | 1 |
- Recursive DP Steps:
- Starting at \(i=0, k=0\), match ‘b’ using column 0, 1, or 2.
- Move to \(i=1, k=1\), match ‘a’.
- Move to \(i=2, k=2\), match ‘b’.
- Final Output: \(4\) ways.
🛠️ Edge Cases
- No Matching Characters: If
target
contains characters not inwords
, return \(0\). - Empty Target: If
target
is an empty string, there’s exactly one way to form it: do nothing. - Empty Words: If
words
is empty, return \(0\).
🌟 Conclusion
This problem highlights the power of dynamic programming in solving complex combinatorial problems efficiently. By using memoization and a precomputed frequency table, we reduced what would have been an exponential time complexity to a manageable polynomial one. 🎉
Ready to tackle more problems like this? 🚀