A clear explanation of finding words that match a pattern using bijective character mapping.
Problem Restatement
We are given a list of strings words and a string pattern.
We need to return all words that match the pattern.
A word matches the pattern if there is a bijection between letters in pattern and letters in the word. That means:
- Each pattern character maps to exactly one word character.
- No two pattern characters map to the same word character.
- The mapping must be consistent across the whole word.
The answer can be returned in any order. LeetCode defines the mapping as a permutation, meaning every letter maps to another letter and no two letters map to the same letter.
Input and Output
| Item | Meaning |
|---|---|
| Input | words, a list of lowercase strings |
| Input | pattern, a lowercase string |
| Output | Words that match pattern |
| Word length | Every words[i] has the same length as pattern |
| Order | Any order is accepted |
Function shape:
def findAndReplacePattern(self, words: list[str], pattern: str) -> list[str]:
...Examples
Example 1:
Input: words = ["abc","deq","mee","aqq","dkd","ccc"], pattern = "abb"
Output: ["mee","aqq"]"mee" matches "abb":
| Pattern | Word |
|---|---|
a | m |
b | e |
b | e |
The mapping is consistent.
"aqq" also matches "abb":
| Pattern | Word |
|---|---|
a | a |
b | q |
b | q |
"ccc" does not match "abb" because both a and b would need to map to c, which violates the bijection rule.
Example 2:
Input: words = ["a","b","c"], pattern = "a"
Output: ["a","b","c"]Every one-letter word matches a one-letter pattern.
First Thought: One Direction Map
A first idea is to map each pattern character to the corresponding word character.
For example, checking "mee" against "abb":
a -> m
b -> eThis seems enough at first.
But consider:
word = "ccc"
pattern = "abb"A one-direction map gives:
a -> c
b -> cEach pattern character maps consistently, but two different pattern characters map to the same word character.
That is invalid.
So we need a two-way mapping.
Key Insight
The mapping must be bijective.
So while checking a word against the pattern, we maintain:
| Map | Meaning |
|---|---|
p_to_w | Pattern character to word character |
w_to_p | Word character to pattern character |
For each aligned pair (p, w):
- If
pwas already mapped, it must map tow. - If
wwas already mapped, it must map fromp. - Otherwise, create both mappings.
If any check fails, the word does not match.
Algorithm
For each word in words:
- Create two empty hash maps.
- Compare the word and
patterncharacter by character. - For each pair
(p, w):- Check whether
palready maps to a differentw. - Check whether
walready maps to a differentp. - If either check fails, reject the word.
- Otherwise, store both mappings.
- Check whether
- If the whole word passes, add it to the answer.
Return the answer.
Walkthrough
Check:
word = "mee"
pattern = "abb"Start with empty maps:
p_to_w = {}
w_to_p = {}Step 1:
| Pattern | Word | Action |
|---|---|---|
a | m | Add a -> m and m -> a |
Step 2:
| Pattern | Word | Action |
|---|---|---|
b | e | Add b -> e and e -> b |
Step 3:
| Pattern | Word | Action |
|---|---|---|
b | e | Existing mapping matches |
No conflict, so "mee" matches.
Now check:
word = "ccc"
pattern = "abb"Step 1:
| Pattern | Word | Action |
|---|---|---|
a | c | Add a -> c and c -> a |
Step 2:
| Pattern | Word | Problem |
|---|---|---|
b | c | c already maps back to a |
So "ccc" does not match.
Correctness
The algorithm accepts a word only if every aligned character pair satisfies both mapping directions.
If a pattern character appears more than once, the p_to_w map ensures it always maps to the same word character.
If a word character appears more than once, the w_to_p map ensures it always comes from the same pattern character.
Therefore, no pattern character maps to two different word characters, and no two pattern characters map to the same word character.
This is exactly the bijection required by the problem.
If a valid bijection exists, then every aligned pair is consistent in both directions. The algorithm will never detect a conflict, so it will accept the word.
If the algorithm accepts the word, the two maps define a valid bijection between the characters used in the pattern and the characters used in the word. Therefore, replacing each pattern character according to this mapping produces the word.
Thus, the algorithm returns exactly the words that match the pattern.
Complexity
Let:
m = len(words)
L = len(pattern)| Metric | Value | Why |
|---|---|---|
| Time | O(m * L) | We check each character of each word |
| Space | O(L) | The maps store at most one entry per distinct character in a word or pattern |
Implementation
class Solution:
def findAndReplacePattern(self, words: list[str], pattern: str) -> list[str]:
def matches(word: str) -> bool:
p_to_w = {}
w_to_p = {}
for p, w in zip(pattern, word):
if p in p_to_w and p_to_w[p] != w:
return False
if w in w_to_p and w_to_p[w] != p:
return False
p_to_w[p] = w
w_to_p[w] = p
return True
return [word for word in words if matches(word)]Code Explanation
The helper function checks one word:
def matches(word: str) -> bool:We create two maps:
p_to_w = {}
w_to_p = {}Then we scan the pattern and word together:
for p, w in zip(pattern, word):If p already maps to another character, the word is invalid:
if p in p_to_w and p_to_w[p] != w:
return FalseIf w already maps back to another pattern character, the word is invalid:
if w in w_to_p and w_to_p[w] != p:
return FalseOtherwise, we record the mapping:
p_to_w[p] = w
w_to_p[w] = pIf all character pairs pass, the word matches.
Finally, we filter all words:
return [word for word in words if matches(word)]Testing
def run_tests():
s = Solution()
assert sorted(s.findAndReplacePattern(
["abc", "deq", "mee", "aqq", "dkd", "ccc"],
"abb"
)) == ["aqq", "mee"]
assert sorted(s.findAndReplacePattern(
["a", "b", "c"],
"a"
)) == ["a", "b", "c"]
assert s.findAndReplacePattern(
["ccc"],
"abb"
) == []
assert s.findAndReplacePattern(
["abc"],
"aaa"
) == []
assert s.findAndReplacePattern(
["xyy", "yxx", "abc"],
"abb"
) == ["xyy", "yxx"]
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
| Standard example | Finds only words with pattern shape abb |
| One-letter pattern | Every one-letter word matches |
| Many pattern chars mapping to one word char | Rejects non-bijection |
| One pattern char mapping to many word chars | Rejects inconsistent mapping |
| Multiple valid words | Accepts any valid bijection |