CF 106169G - Secret Words
We are given a dictionary of secret words and a text string. The text contains lowercase letters and the special character ?. A ? can stand for any single lowercase letter. The task is to count how many different ways the entire text can be split into dictionary words.
Rating: -
Tags: -
Solve time: 45s
Verified: yes
Solution
Problem Understanding
We are given a dictionary of secret words and a text string. The text contains lowercase letters and the special character ?.
A ? can stand for any single lowercase letter. The task is to count how many different ways the entire text can be split into dictionary words.
A dictionary word matches a segment of the text if they have the same length and every position satisfies one of the following conditions:
- the text character equals the word character;
- the text character is
?, meaning it can represent that word character.
Each dictionary word may be used any number of times. Every character of the text must belong to exactly one chosen word. We need the number of valid decompositions modulo 10^9 + 7.
The constraints are small but very suggestive. There are at most 100 words, each word has length at most 10, and the text length is at most 100. A solution around O(|T| * n * maxLen) is completely safe. Even 100 * 100 * 10 = 100000 character comparisons is tiny.
The main subtlety is that a ? does not create just one matching word. It may match several different dictionary words, and every matching choice contributes separately to the answer.
Consider the first sample:
Words: a, b, c, d, ab, bc, cd
Text: ab?d
The third character can behave as a, b, c, or d, producing several distinct decompositions. The answer is 11, not 4.
Another easy mistake is forgetting that different words of the same length can match the same substring.
Example:
Words:
ab
ac
Text:
a?
Both words match. The answer is:
2
A boolean DP that only records whether a position is reachable would incorrectly return 1.
A third edge case is when the whole text consists of question marks.
Example:
Words:
a
b
Text:
??
The first position has 2 choices and the second position has 2 choices, so the answer is:
4
This is one of the official samples.
Approaches
The most direct idea is recursive backtracking. Starting from the beginning of the text, try every dictionary word that matches the current position and recursively process the remainder of the string. Every successful completion contributes one to the answer.
This approach is correct because every valid decomposition corresponds to exactly one path in the recursion tree. The problem is that the same suffix is recomputed many times. In the worst case, such as a text full of ? characters and many matching words, the number of recursive states grows exponentially.
The repeated work suggests dynamic programming.
Let dp[i] denote the number of ways to decompose the prefix ending before position i. Then every matching word gives a transition from an earlier position into a later position.
Suppose a word of length L matches the substring ending at position i. Then every valid decomposition of the prefix before that word can be extended by this word. This gives:
dp[i] += dp[i - L]
The only remaining task is efficiently checking whether a word matches a text segment. Since word lengths are at most 10, a simple character-by-character comparison is already fast enough.
The brute force explores decompositions directly. The dynamic programming solution counts all decompositions of every prefix once and reuses those results.
| Approach | Time Complexity | Space Complexity | Verdict |
|---|---|---|---|
| Brute Force | Exponential | O( | T |
| Optimal DP | O( | T | × n × maxLen) |
Algorithm Walkthrough
- Read all dictionary words.
- Let
mbe the length of the text and create an arraydpof lengthm + 1. - Set
dp[0] = 1.
This represents the empty prefix. There is exactly one way to decompose nothing.
4. For every position i from 0 to m - 1, examine every dictionary word.
5. Let the current word have length L.
If i + L > m, the word cannot start at position i, so skip it.
6. Compare the word with the text segment T[i : i + L].
For every position j, the match succeeds if either T[i + j] == '?' or T[i + j] == word[j].
7. If the entire word matches, add:
dp[i + L] += dp[i]
and take the result modulo 10^9 + 7.
Every decomposition reaching position i can be extended with this matching word.
8. After processing all positions and words, output dp[m].
Why it works
The invariant is that dp[i] always equals the number of valid decompositions of the first i characters of the text.
Initially this is true because the empty prefix has exactly one decomposition.
Whenever a word matches starting at position i, every decomposition counted by dp[i] can be extended by that word, producing a decomposition of length i + L. The transition adds exactly those new decompositions and nothing else.
Every valid decomposition has a unique last word. When that last word is processed, the decomposition is counted exactly once through its preceding prefix. Because every decomposition is generated once and every generated decomposition is valid, dp[m] equals the desired answer.
Python Solution
import sys
input = sys.stdin.readline
MOD = 10 ** 9 + 7
n = int(input())
words = [input().strip() for _ in range(n)]
text = input().strip()
m = len(text)
dp = [0] * (m + 1)
dp[0] = 1
for i in range(m):
if dp[i] == 0:
continue
for word in words:
L = len(word)
if i + L > m:
continue
ok = True
for j in range(L):
if text[i + j] != '?' and text[i + j] != word[j]:
ok = False
break
if ok:
dp[i + L] = (dp[i + L] + dp[i]) % MOD
print(dp[m])
The DP array stores counts for prefixes. Position i represents the first i characters of the text, which makes the transitions clean and avoids off-by-one errors.
The matching test is performed directly. Since every word length is at most 10, there is no need for hashing, tries, or string preprocessing.
The if dp[i] == 0 check is not required for correctness, but it avoids unnecessary matching attempts from unreachable states.
The modulo operation is applied after every addition because the number of decompositions can grow very quickly when many ? characters are present.
Worked Examples
Example 1
Input:
7
a
b
c
d
ab
bc
cd
ab?d
Let dp[i] represent the number of ways for the first i characters.
| Position | Matching words starting here | DP updates |
|---|---|---|
| 0 | a, ab | dp[1]+=1, dp[2]+=1 |
| 1 | b, bc | dp[2]+=1, dp[3]+=1 |
| 2 | a, b, c, d, cd | dp[3]+=2, dp[4]+=2, dp[4]+=2, dp[4]+=2, dp[4]+=2 |
| 3 | d | dp[4]+=5 |
Final DP:
| i | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| dp[i] | 1 | 1 | 2 | 3 | 11 |
Answer:
11
This example shows how a single ? can simultaneously match many different words, each contributing additional decompositions.
Example 2
Input:
2
a
b
??
| Position | Matching words | DP updates |
|---|---|---|
| 0 | a, b | dp[1]=2 |
| 1 | a, b | dp[2]=4 |
Final DP:
| i | 0 | 1 | 2 |
|---|---|---|---|
| dp[i] | 1 | 2 | 4 |
Answer:
4
This demonstrates that every ? independently branches into multiple valid choices.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O( | T |
| Space | O( | T |
With |T| ≤ 100, n ≤ 100, and maxLen ≤ 10, the worst-case work is roughly 100,000 character comparisons, which is easily within the limits.
Test Cases
# helper: run solution on input string, return output string
import sys
import io
def run(inp: str) -> str:
sys.stdin = io.StringIO(inp)
MOD = 10 ** 9 + 7
n = int(input())
words = [input().strip() for _ in range(n)]
text = input().strip()
m = len(text)
dp = [0] * (m + 1)
dp[0] = 1
for i in range(m):
if dp[i] == 0:
continue
for word in words:
L = len(word)
if i + L > m:
continue
ok = True
for j in range(L):
if text[i + j] != '?' and text[i + j] != word[j]:
ok = False
break
if ok:
dp[i + L] = (dp[i + L] + dp[i]) % MOD
return str(dp[m]) + "\n"
# provided samples
assert run(
"""7
a
b
c
d
ab
bc
cd
ab?d
"""
) == "11\n", "sample 1"
assert run(
"""2
a
b
??
"""
) == "4\n", "sample 2"
# minimum size
assert run(
"""1
a
a
"""
) == "1\n", "single exact match"
# impossible decomposition
assert run(
"""1
ab
a
"""
) == "0\n", "cannot cover whole text"
# multiple words match same segment
assert run(
"""2
ab
ac
a?
"""
) == "2\n", "two matching words"
# overlapping decomposition choices
assert run(
"""3
a
aa
aaa
aaa
"""
) == "4\n", "different partition lengths"
| Test input | Expected output | What it validates |
|---|---|---|
One word a, text a |
1 | Smallest valid instance |
One word ab, text a |
0 | Impossible coverage |
Words ab, ac, text a? |
2 | Multiple words matching same segment |
Words a, aa, aaa, text aaa |
4 | Multiple partition structures |
Edge Cases
Consider:
2
ab
ac
a?
At position 0, both words match because ? can represent either b or c.
The algorithm performs both transitions:
dp[2] += dp[0]
dp[2] += dp[0]
giving dp[2] = 2. A solution that only checked whether some word matched would incorrectly produce 1.
Consider:
2
a
b
??
At the first character, both words match. The same happens at the second character.
The DP evolves as:
dp = [1, 2, 4]
and returns 4. This correctly counts every assignment of letters implied by the chosen words.
Consider:
1
ab
a
The word is longer than the remaining text. The transition is skipped because i + L > m. No state reaches the end, so dp[m] remains 0, correctly indicating that the text cannot be decomposed into dictionary words.