Facebook Pixel

291. Word Pattern II 🔒

MediumHash TableStringBacktracking
Leetcode Link

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:

  1. 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)
  2. 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
  • Pattern: "ab", String: "redred"

    • We cannot map both 'a' and 'b' to "red" (violates bijection)
    • No valid mapping exists

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Make a choice: Pick a substring from the current position in s and try mapping it to the current pattern character
  2. Check validity: Ensure the mapping respects our bijection rules (one-to-one correspondence)
  3. Explore further: If valid, recursively try to match the rest
  4. 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:]:

  1. Base Cases:

    • If both i == m and j == n: We've successfully matched everything, return True
    • If only one pointer reaches the end or n - j < m - i: Impossible to match remaining characters, return False
  2. Try Different Substring Lengths: For each possible substring starting at position j, we iterate k from j to n-1, creating substring t = s[j:k+1].

  3. 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) AND t 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]) and vis.remove(t)

Data Structures:

  • d: Dictionary mapping pattern characters to their assigned substrings
  • vis: 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 Evaluator

Example 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)

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)

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)

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)

Next: dfs(1, 3) - Mapping 'b'

  • Try substring "dog" (s[3:6]):
    • Map: d['b'] = "dog", vis = {"cat", "dog"}
    • Call dfs(2, 6)

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 position j in string s
  • 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 into m parts
  • The number of ways to partition a string of length n into at most m parts is bounded by O(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 most m mappings (one per unique pattern character): O(m)
  • The set vis stores at most m unique substrings from s: O(n) in the worst case, as each substring can be up to length n
  • 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More