291. Word Pattern II 🔒
Problem Description
This problem asks you to determine if a string s
matches a given pattern
through a bijective mapping relationship.
The pattern consists of characters (like 'a', 'b', 'c'), and you need to map each unique character in the pattern to a non-empty substring of s
. The mapping must satisfy two key conditions:
- One-to-one mapping: Each character in the pattern must map to exactly one unique substring (no two different pattern characters can map to the same substring)
- Consistent mapping: Once a pattern character is mapped to a substring, it must always map to that same substring throughout
When you replace each character in the pattern with its mapped substring, the concatenated result should exactly equal the string s
.
For example:
-
Pattern:
"aba"
, String:"redredblue"
- We could map:
'a' -> "red"
,'b' -> "blue"
- Replacing gives:
"red" + "blue" + "red" = "redredblue"
✓ - This is a valid match
- We could map:
-
Pattern:
"ab"
, String:"redred"
- We cannot map both
'a'
and'b'
to"red"
(violates bijection) - No valid mapping exists
- We cannot map both
The solution uses backtracking with two pointers (i
for pattern position, j
for string position) to try different substring mappings. It maintains a dictionary d
for character-to-substring mappings and a set vis
to track used substrings, ensuring the bijective property is preserved.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem does not involve nodes and edges or graph traversal. We're dealing with pattern matching between a string pattern and a target string.
Need to solve for kth smallest/largest?
- No: We're not looking for any kth smallest or largest element. We need to determine if a valid mapping exists.
Involves Linked Lists?
- No: The problem works with strings and pattern matching, not linked list data structures.
Does the problem have small constraints?
- Yes: Pattern matching problems typically have manageable constraints since we need to explore different possible mappings. Each character in the pattern needs to be mapped to substrings of
s
, and we need to try different combinations.
Brute force / Backtracking?
- Yes: We need to explore all possible ways to map pattern characters to substrings. When a mapping doesn't work, we need to backtrack and try different substring assignments. This is a classic backtracking scenario where we:
- Try assigning a substring to a pattern character
- Recursively check if the rest can be matched
- If it fails, undo the assignment (backtrack) and try a different substring
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This aligns perfectly with the solution, which uses DFS with backtracking to try different substring mappings, maintaining a dictionary for character-to-substring mappings and undoing failed attempts.
Intuition
The key insight is that we need to explore all possible ways to partition the string s
and map these partitions to pattern characters. Since we don't know in advance which substrings should map to which pattern characters, we must try different possibilities systematically.
Think of it like solving a puzzle where you're trying to match pieces (substrings) to slots (pattern characters). You don't know which piece goes where, so you try different combinations until you find one that works or exhaust all possibilities.
The backtracking approach naturally emerges from this need to:
- Make a choice: Pick a substring from the current position in
s
and try mapping it to the current pattern character - Check validity: Ensure the mapping respects our bijection rules (one-to-one correspondence)
- Explore further: If valid, recursively try to match the rest
- Backtrack if needed: If we hit a dead end, undo our choice and try a different substring
The brilliance lies in using two tracking mechanisms:
- A dictionary
d
to remember "pattern character → substring" mappings - A set
vis
to ensure no two pattern characters map to the same substring
At each step, we face two scenarios:
- If the current pattern character already has a mapping, we check if the expected substring matches what we see in
s
- If it's unmapped, we try assigning various substring lengths starting from the current position
The recursive nature with backtracking ensures we systematically explore all valid mapping combinations without missing any possibilities. We prune invalid branches early (like when remaining string length is less than remaining pattern length) to optimize the search.
Solution Approach
The solution implements a depth-first search with backtracking using two pointers to track positions in both the pattern and string.
Core Algorithm Structure:
The dfs(i, j)
function explores whether we can match pattern[i:]
with s[j:]
:
-
Base Cases:
- If both
i == m
andj == n
: We've successfully matched everything, returnTrue
- If only one pointer reaches the end or
n - j < m - i
: Impossible to match remaining characters, returnFalse
- If both
-
Try Different Substring Lengths: For each possible substring starting at position
j
, we iteratek
fromj
ton-1
, creating substringt = s[j:k+1]
. -
Two Mapping Scenarios:
Case 1: Pattern character already mapped
- If
d.get(pattern[i]) == t
: The existing mapping matches our current substring - Continue with
dfs(i + 1, k + 1)
to match the rest
Case 2: Pattern character not yet mapped
- Check if
pattern[i] not in d
(unmapped) ANDt not in vis
(substring not used) - Create new mapping:
d[pattern[i]] = t
and mark substring as used:vis.add(t)
- Recursively try
dfs(i + 1, k + 1)
- If unsuccessful, backtrack:
d.pop(pattern[i])
andvis.remove(t)
- If
Data Structures:
d
: Dictionary mapping pattern characters to their assigned substringsvis
: Set tracking which substrings have been used (ensures bijection)m, n
: Lengths of pattern and string respectively
Key Optimization:
The condition n - j < m - i
prunes branches where the remaining string is shorter than the remaining pattern, making it impossible to assign at least one character to each pattern symbol.
The algorithm systematically explores all valid partitioning possibilities through backtracking, ensuring we find a valid bijective mapping if one exists.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with pattern = "aba"
and s = "catdogcat"
.
Initial State:
d = {}
(empty mappings)vis = set()
(no substrings used)- Start with
dfs(0, 0)
(i=0 points to 'a', j=0 points to 'c')
Step 1: dfs(0, 0) - Mapping first 'a'
- Pattern character 'a' is unmapped
- Try substring "c" (s[0:1]):
- Map:
d['a'] = "c"
,vis = {"c"}
- Call
dfs(1, 1)
- Map:
Step 2: dfs(1, 1) - Mapping 'b'
- Pattern character 'b' is unmapped
- Try substring "a" (s[1:2]):
- Map:
d['b'] = "a"
,vis = {"c", "a"}
- Call
dfs(2, 2)
- Map:
Step 3: dfs(2, 2) - Checking second 'a'
- Pattern character 'a' already mapped to "c"
- Check s[2:3] = "t" ≠"c" (mismatch!)
- Return False, backtrack to Step 2
Step 2 (continued): Try longer substring for 'b'
- Try substring "at" (s[1:3]):
- Map:
d['b'] = "at"
,vis = {"c", "at"}
- Call
dfs(2, 3)
- Map:
Step 4: dfs(2, 3) - Checking second 'a'
- Pattern character 'a' mapped to "c"
- Check s[3:4] = "d" ≠"c" (mismatch!)
- Return False, continue trying longer substrings...
[After more backtracking...]
Eventually: dfs(0, 0) - Try longer substring for first 'a'
- Try substring "cat" (s[0:3]):
- Map:
d['a'] = "cat"
,vis = {"cat"}
- Call
dfs(1, 3)
- Map:
Next: dfs(1, 3) - Mapping 'b'
- Try substring "dog" (s[3:6]):
- Map:
d['b'] = "dog"
,vis = {"cat", "dog"}
- Call
dfs(2, 6)
- Map:
Final: dfs(2, 6) - Checking second 'a'
- Pattern character 'a' mapped to "cat"
- Check s[6:9] = "cat" ✓ (matches!)
- Call
dfs(3, 9)
Base Case: dfs(3, 9)
- Both i=3 (end of pattern) and j=9 (end of string)
- Return True! ✓
Result: Found valid mapping: 'a' → "cat", 'b' → "dog"
The algorithm successfully backtracks through invalid mappings and finds the correct bijective mapping where "aba" becomes "cat" + "dog" + "cat" = "catdogcat".
Solution Implementation
1class Solution:
2 def wordPatternMatch(self, pattern: str, s: str) -> bool:
3 """
4 Check if string s follows the given pattern where each character in pattern
5 can map to a substring in s (bijective mapping).
6
7 Args:
8 pattern: Pattern string containing characters
9 s: Target string to match against the pattern
10
11 Returns:
12 True if s follows the pattern, False otherwise
13 """
14
15 def backtrack(pattern_idx: int, string_idx: int) -> bool:
16 """
17 Recursively try to match pattern with string using backtracking.
18
19 Args:
20 pattern_idx: Current index in the pattern
21 string_idx: Current index in the string s
22
23 Returns:
24 True if valid matching found, False otherwise
25 """
26 # Base case: both pattern and string are fully processed
27 if pattern_idx == pattern_length and string_idx == string_length:
28 return True
29
30 # Invalid case: one is exhausted but not the other, or insufficient characters left
31 if (pattern_idx == pattern_length or
32 string_idx == string_length or
33 string_length - string_idx < pattern_length - pattern_idx):
34 return False
35
36 current_pattern_char = pattern[pattern_idx]
37
38 # Try all possible substrings starting from current position in s
39 for end_idx in range(string_idx, string_length):
40 substring = s[string_idx:end_idx + 1]
41
42 # Case 1: Current pattern character already has a mapping
43 if pattern_to_string.get(current_pattern_char) == substring:
44 if backtrack(pattern_idx + 1, end_idx + 1):
45 return True
46
47 # Case 2: Current pattern character has no mapping yet and substring is not used
48 elif current_pattern_char not in pattern_to_string and substring not in used_substrings:
49 # Create new mapping
50 pattern_to_string[current_pattern_char] = substring
51 used_substrings.add(substring)
52
53 # Recursively check if this mapping works
54 if backtrack(pattern_idx + 1, end_idx + 1):
55 return True
56
57 # Backtrack: remove the mapping if it doesn't lead to solution
58 del pattern_to_string[current_pattern_char]
59 used_substrings.remove(substring)
60
61 return False
62
63 # Initialize variables
64 pattern_length = len(pattern)
65 string_length = len(s)
66 pattern_to_string = {} # Maps pattern characters to substrings
67 used_substrings = set() # Tracks which substrings are already mapped
68
69 # Start backtracking from the beginning
70 return backtrack(0, 0)
71
1class Solution {
2 // Set to track which substrings have already been mapped to pattern characters
3 private Set<String> usedSubstrings;
4 // Map from pattern character to its corresponding substring in s
5 private Map<Character, String> patternToStringMap;
6 // The pattern string
7 private String pattern;
8 // The target string to match against
9 private String targetString;
10 // Length of pattern
11 private int patternLength;
12 // Length of target string
13 private int targetLength;
14
15 /**
16 * Determines if a pattern matches a string where each character in pattern
17 * can map to a substring in s (bijective mapping required)
18 * @param pattern The pattern string containing characters to map
19 * @param s The target string to match against
20 * @return true if pattern matches s with consistent character-to-substring mapping
21 */
22 public boolean wordPatternMatch(String pattern, String s) {
23 // Initialize data structures
24 usedSubstrings = new HashSet<>();
25 patternToStringMap = new HashMap<>();
26 this.pattern = pattern;
27 this.targetString = s;
28 patternLength = pattern.length();
29 targetLength = s.length();
30
31 // Start DFS from beginning of both strings
32 return dfs(0, 0);
33 }
34
35 /**
36 * Recursive DFS to try all possible mappings between pattern and target string
37 * @param patternIndex Current index in pattern string
38 * @param targetIndex Current index in target string
39 * @return true if valid mapping exists from current position onwards
40 */
41 private boolean dfs(int patternIndex, int targetIndex) {
42 // Base case: successfully matched entire pattern and target string
43 if (patternIndex == patternLength && targetIndex == targetLength) {
44 return true;
45 }
46
47 // Base case: one string exhausted but not the other, or not enough characters left
48 // The condition (patternLength - patternIndex > targetLength - targetIndex) ensures
49 // we have enough characters in target string for remaining pattern characters
50 if (patternIndex == patternLength || targetIndex == targetLength ||
51 patternLength - patternIndex > targetLength - targetIndex) {
52 return false;
53 }
54
55 // Get current pattern character to match
56 char currentPatternChar = pattern.charAt(patternIndex);
57
58 // Try all possible substring lengths starting from current target position
59 for (int endIndex = targetIndex + 1; endIndex <= targetLength; ++endIndex) {
60 String currentSubstring = targetString.substring(targetIndex, endIndex);
61
62 // Case 1: Pattern character already has a mapping
63 if (patternToStringMap.getOrDefault(currentPatternChar, "").equals(currentSubstring)) {
64 // If the existing mapping matches current substring, continue DFS
65 if (dfs(patternIndex + 1, endIndex)) {
66 return true;
67 }
68 }
69
70 // Case 2: Pattern character has no mapping yet and substring is not used
71 if (!patternToStringMap.containsKey(currentPatternChar) && !usedSubstrings.contains(currentSubstring)) {
72 // Create new mapping
73 patternToStringMap.put(currentPatternChar, currentSubstring);
74 usedSubstrings.add(currentSubstring);
75
76 // Continue DFS with this mapping
77 if (dfs(patternIndex + 1, endIndex)) {
78 return true;
79 }
80
81 // Backtrack: remove the mapping if it didn't lead to a solution
82 usedSubstrings.remove(currentSubstring);
83 patternToStringMap.remove(currentPatternChar);
84 }
85 }
86
87 // No valid mapping found from current position
88 return false;
89 }
90}
91
1class Solution {
2public:
3 bool wordPatternMatch(string pattern, string s) {
4 // Set to track which substrings have already been mapped to avoid duplicate mappings
5 unordered_set<string> usedStrings;
6 // Map to store pattern character to substring mappings
7 unordered_map<char, string> patternToString;
8
9 return backtrack(0, 0, pattern, s, usedStrings, patternToString);
10 }
11
12private:
13 bool backtrack(int patternIndex, int stringIndex,
14 string& pattern, string& str,
15 unordered_set<string>& usedStrings,
16 unordered_map<char, string>& patternToString) {
17
18 int patternLength = pattern.size();
19 int stringLength = str.size();
20
21 // Base case: both pattern and string are fully matched
22 if (patternIndex == patternLength && stringIndex == stringLength) {
23 return true;
24 }
25
26 // Invalid cases: one is exhausted but not the other, or remaining pattern is longer than remaining string
27 if (patternIndex == patternLength || stringIndex == stringLength ||
28 patternLength - patternIndex > stringLength - stringIndex) {
29 return false;
30 }
31
32 char currentPatternChar = pattern[patternIndex];
33
34 // Try all possible substring lengths starting from current position in string
35 for (int endIndex = stringIndex + 1; endIndex <= stringLength; ++endIndex) {
36 string currentSubstring = str.substr(stringIndex, endIndex - stringIndex);
37
38 // Case 1: Current pattern character already has a mapping
39 if (patternToString.count(currentPatternChar) &&
40 patternToString[currentPatternChar] == currentSubstring) {
41 // Continue matching with the existing mapping
42 if (backtrack(patternIndex + 1, endIndex, pattern, str, usedStrings, patternToString)) {
43 return true;
44 }
45 }
46
47 // Case 2: Current pattern character has no mapping and substring is not used
48 if (!patternToString.count(currentPatternChar) && !usedStrings.count(currentSubstring)) {
49 // Create a new mapping
50 patternToString[currentPatternChar] = currentSubstring;
51 usedStrings.insert(currentSubstring);
52
53 // Recursively check if this mapping leads to a valid solution
54 if (backtrack(patternIndex + 1, endIndex, pattern, str, usedStrings, patternToString)) {
55 return true;
56 }
57
58 // Backtrack: remove the mapping if it doesn't lead to a solution
59 usedStrings.erase(currentSubstring);
60 patternToString.erase(currentPatternChar);
61 }
62 }
63
64 return false;
65 }
66};
67
1function wordPatternMatch(pattern: string, s: string): boolean {
2 // Set to track which substrings have already been mapped to avoid duplicate mappings
3 const usedStrings: Set<string> = new Set();
4 // Map to store pattern character to substring mappings
5 const patternToString: Map<string, string> = new Map();
6
7 return backtrack(0, 0, pattern, s, usedStrings, patternToString);
8}
9
10function backtrack(
11 patternIndex: number,
12 stringIndex: number,
13 pattern: string,
14 str: string,
15 usedStrings: Set<string>,
16 patternToString: Map<string, string>
17): boolean {
18
19 const patternLength = pattern.length;
20 const stringLength = str.length;
21
22 // Base case: both pattern and string are fully matched
23 if (patternIndex === patternLength && stringIndex === stringLength) {
24 return true;
25 }
26
27 // Invalid cases: one is exhausted but not the other, or remaining pattern is longer than remaining string
28 if (patternIndex === patternLength ||
29 stringIndex === stringLength ||
30 patternLength - patternIndex > stringLength - stringIndex) {
31 return false;
32 }
33
34 const currentPatternChar = pattern[patternIndex];
35
36 // Try all possible substring lengths starting from current position in string
37 for (let endIndex = stringIndex + 1; endIndex <= stringLength; endIndex++) {
38 const currentSubstring = str.substring(stringIndex, endIndex);
39
40 // Case 1: Current pattern character already has a mapping
41 if (patternToString.has(currentPatternChar) &&
42 patternToString.get(currentPatternChar) === currentSubstring) {
43 // Continue matching with the existing mapping
44 if (backtrack(patternIndex + 1, endIndex, pattern, str, usedStrings, patternToString)) {
45 return true;
46 }
47 }
48
49 // Case 2: Current pattern character has no mapping and substring is not used
50 if (!patternToString.has(currentPatternChar) && !usedStrings.has(currentSubstring)) {
51 // Create a new mapping
52 patternToString.set(currentPatternChar, currentSubstring);
53 usedStrings.add(currentSubstring);
54
55 // Recursively check if this mapping leads to a valid solution
56 if (backtrack(patternIndex + 1, endIndex, pattern, str, usedStrings, patternToString)) {
57 return true;
58 }
59
60 // Backtrack: remove the mapping if it doesn't lead to a solution
61 usedStrings.delete(currentSubstring);
62 patternToString.delete(currentPatternChar);
63 }
64 }
65
66 return false;
67}
68
Time and Space Complexity
Time Complexity: O(n^m)
where n
is the length of string s
and m
is the length of the pattern.
The analysis breaks down as follows:
- At each position
i
in the pattern, we try all possible substrings starting from positionj
in strings
- For each character in the pattern, we can potentially try up to
n
different substring lengths - The recursion tree has a depth of at most
m
(the pattern length) - At each level, we branch out to try different substring assignments
- In the worst case, we explore all possible ways to partition string
s
intom
parts - The number of ways to partition a string of length
n
into at mostm
parts is bounded byO(n^m)
- The pruning condition
n - j < m - i
helps eliminate some branches early, but doesn't change the worst-case complexity
Space Complexity: O(m + n)
The space usage comes from:
- The recursion call stack depth:
O(m)
in the worst case, as we recurse once for each pattern character - The dictionary
d
stores at mostm
mappings (one per unique pattern character):O(m)
- The set
vis
stores at mostm
unique substrings froms
:O(n)
in the worst case, as each substring can be up to lengthn
- The substring creation
s[j : k + 1]
creates temporary strings:O(n)
for the longest possible substring - Overall space complexity is
O(m + n)
Common Pitfalls
1. Forgetting to Check Bijection (One-to-One Mapping)
A critical mistake is only checking if a pattern character maps consistently to the same substring, while forgetting to ensure different pattern characters map to different substrings.
Incorrect Implementation:
def backtrack(pattern_idx, string_idx):
# ... base cases ...
for end_idx in range(string_idx, string_length):
substring = s[string_idx:end_idx + 1]
# WRONG: Only checking if pattern char is mapped correctly
if current_pattern_char in pattern_to_string:
if pattern_to_string[current_pattern_char] == substring:
if backtrack(pattern_idx + 1, end_idx + 1):
return True
else:
# WRONG: Not checking if substring is already used by another pattern char
pattern_to_string[current_pattern_char] = substring
if backtrack(pattern_idx + 1, end_idx + 1):
return True
del pattern_to_string[current_pattern_char]
This would incorrectly accept pattern "ab"
with string "redred"
by mapping both 'a'
and 'b'
to "red"
.
Correct Solution:
Always maintain a used_substrings
set to track which substrings are already mapped, ensuring no two pattern characters can map to the same substring.
2. Incorrect Backtracking - Not Restoring State Properly
Incorrect Implementation:
# WRONG: Forgetting to remove from used_substrings during backtracking if current_pattern_char not in pattern_to_string and substring not in used_substrings: pattern_to_string[current_pattern_char] = substring used_substrings.add(substring) if backtrack(pattern_idx + 1, end_idx + 1): return True del pattern_to_string[current_pattern_char] # MISSING: used_substrings.remove(substring)
This leaves the substring incorrectly marked as used, preventing valid mappings in subsequent branches of the search tree.
Correct Solution:
Always restore both data structures (pattern_to_string
and used_substrings
) when backtracking to ensure the search space is properly explored.
3. Inefficient Pruning or Missing Early Termination
Incorrect Implementation:
def backtrack(pattern_idx, string_idx):
if pattern_idx == pattern_length and string_idx == string_length:
return True
if pattern_idx == pattern_length or string_idx == string_length:
return False
# MISSING: Check if remaining string is too short for remaining pattern
Without checking string_length - string_idx < pattern_length - pattern_idx
, the algorithm wastes time exploring branches where there aren't enough characters left in the string to assign at least one character to each remaining pattern symbol.
Correct Solution: Add the pruning condition early in the function to avoid unnecessary recursive calls and improve performance significantly for longer inputs.
What does the following code do?
1def f(arr1, arr2):
2 i, j = 0, 0
3 new_arr = []
4 while i < len(arr1) and j < len(arr2):
5 if arr1[i] < arr2[j]:
6 new_arr.append(arr1[i])
7 i += 1
8 else:
9 new_arr.append(arr2[j])
10 j += 1
11 new_arr.extend(arr1[i:])
12 new_arr.extend(arr2[j:])
13 return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2 int i = 0, j = 0;
3 List<Integer> newArr = new ArrayList<>();
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.add(arr1[i]);
8 i++;
9 } else {
10 newArr.add(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.add(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.add(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
1function f(arr1, arr2) {
2 let i = 0, j = 0;
3 let newArr = [];
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.push(arr1[i]);
8 i++;
9 } else {
10 newArr.push(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.push(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.push(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!