Facebook Pixel

2301. Match Substring After Replacement

HardArrayHash TableStringString Matching
Leetcode Link

Problem Description

You are given two strings s and sub, along with a 2D character array mappings. Each element in mappings is a pair [old_i, new_i] that defines a replacement rule: you can replace character old_i in sub with character new_i.

The task is to determine if you can make sub appear as a substring somewhere in s by applying these replacement rules to characters in sub.

Key constraints to note:

  • You can apply any mapping any number of times (including zero times)
  • Each character position in sub can only be replaced once (you cannot chain replacements)
  • A character in sub can either stay the same or be replaced according to the mappings

For example, if sub = "abc" and you have mappings [['a','x'], ['b','y']], you could transform sub to any of: "abc", "xbc", "ayc", or "xyc".

The function should return true if there exists at least one position in s where a substring matches sub (either directly or after applying valid replacements). Otherwise, return false.

The solution works by:

  1. Building a hash table d where each key is a character from sub and the value is a set of all characters it can be replaced with
  2. Checking every possible starting position in s for a substring of length equal to sub
  3. For each position, verifying if the substring at that position can match sub by checking if each character pair either matches directly (a == b) or can be matched through a valid replacement (a in d[b])
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 check if sub can "match" any substring of s after applying allowed character replacements. Since we're looking for a substring match, we need to check all possible positions in s where sub could potentially fit.

For each potential starting position in s, we need to verify character-by-character if the substring at that position can match sub. A character at position i in the substring matches the corresponding character in sub if:

  1. They are already the same character, OR
  2. The character in sub can be replaced to match the character in s according to our mappings

To efficiently check if a replacement is valid, we preprocess the mappings into a hash table. For each character that can be replaced (the old character in mappings), we store all possible characters it can become (the new characters). This gives us O(1) lookup time when checking if a replacement is valid.

The sliding window approach naturally emerges: we slide a window of size len(sub) across string s, and at each position, we check if all characters can match. We use the relationship a == b or a in d[b] where a is from s and b is from sub. This checks: "Is a already equal to b, or can b be replaced to become a?"

The beauty of this approach is its simplicity - rather than trying to generate all possible variations of sub (which could be exponential), we simply check each position in s and verify if a match is possible using our replacement rules.

Solution Approach

The solution uses a hash table combined with enumeration to efficiently solve the problem.

Step 1: Building the Mapping Dictionary

d = defaultdict(set)
for a, b in mappings:
    d[a].add(b)

We create a dictionary d where each key represents a character that can be replaced, and the value is a set of all characters it can be replaced with. Using defaultdict(set) ensures that we can safely add replacements without checking if the key exists first. For example, if mappings = [['a','x'], ['a','y'], ['b','z']], then d would be {'a': {'x', 'y'}, 'b': {'z'}}.

Step 2: Enumerate All Possible Starting Positions

for i in range(len(s) - len(sub) + 1):

We iterate through all valid starting positions in string s where a substring of length len(sub) could exist. The range is len(s) - len(sub) + 1 because the last valid starting position is when there are exactly len(sub) characters remaining.

Step 3: Check Character-by-Character Match

if all(a == b or a in d[b] for a, b in zip(s[i : i + len(sub)], sub)):
    return True

For each starting position i, we extract the substring s[i : i + len(sub)] and pair it with sub using zip. For each character pair (a, b) where a comes from s and b comes from sub, we check:

  • a == b: The characters already match without any replacement
  • a in d[b]: Character b in sub can be replaced with character a according to our mappings

The all() function ensures that every character position satisfies the matching condition. If all characters match, we've found a valid substring and return True.

Step 4: Return Result

If we complete the loop without finding any matching substring, we return False.

Time Complexity: O(n × m) where n is the length of s and m is the length of sub, as we check at most n positions and each check takes O(m) time.

Space Complexity: O(k) where k is the number of mappings, used to store the hash table.

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 walk through a concrete example to illustrate the solution approach.

Given:

  • s = "axxxbc"
  • sub = "abc"
  • mappings = [['a','x'], ['b','y']]

Step 1: Build the mapping dictionary

We process each mapping to create our dictionary d:

  • ['a','x']d['a'] = {'x'}
  • ['b','y']d['b'] = {'y'}

Final dictionary: d = {'a': {'x'}, 'b': {'y'}}

This tells us:

  • Character 'a' in sub can be replaced with 'x'
  • Character 'b' in sub can be replaced with 'y'
  • Character 'c' has no replacements (stays as 'c')

Step 2: Check each possible starting position in s

We need to check positions 0 through 3 (since len(s) - len(sub) + 1 = 6 - 3 + 1 = 4).

Position i = 0: Check substring "axx" against "abc"

  • Compare 'a' with 'a': 'a' == 'a' ✓ (direct match)
  • Compare 'x' with 'b': 'x' == 'b' ✗ and 'x' in d['b']? → 'x' in {'y'}
  • Since not all characters match, continue to next position

Position i = 1: Check substring "xxx" against "abc"

  • Compare 'x' with 'a': 'x' == 'a' ✗ and 'x' in d['a']? → 'x' in {'x'} ✓ (can replace 'a' with 'x')
  • Compare 'x' with 'b': 'x' == 'b' ✗ and 'x' in d['b']? → 'x' in {'y'}
  • Since not all characters match, continue to next position

Position i = 2: Check substring "xxb" against "abc"

  • Compare 'x' with 'a': 'x' == 'a' ✗ and 'x' in d['a']? → 'x' in {'x'} ✓ (can replace 'a' with 'x')
  • Compare 'x' with 'b': 'x' == 'b' ✗ and 'x' in d['b']? → 'x' in {'y'}
  • Since not all characters match, continue to next position

Position i = 3: Check substring "xbc" against "abc"

  • Compare 'x' with 'a': 'x' == 'a' ✗ and 'x' in d['a']? → 'x' in {'x'} ✓ (can replace 'a' with 'x')
  • Compare 'b' with 'b': 'b' == 'b' ✓ (direct match)
  • Compare 'c' with 'c': 'c' == 'c' ✓ (direct match)
  • All characters match! Return True

Result: The function returns True because we found that substring "xbc" at position 3 matches "abc" after replacing 'a' with 'x'.

Solution Implementation

1from collections import defaultdict
2from typing import List
3
4class Solution:
5    def matchReplacement(self, s: str, sub: str, mappings: List[List[str]]) -> bool:
6        """
7        Check if substring 'sub' can match any substring in 's' after applying character mappings.
8      
9        Args:
10            s: The main string to search in
11            sub: The substring pattern to match
12            mappings: List of [old_char, new_char] pairs representing allowed character replacements
13      
14        Returns:
15            True if a match is found, False otherwise
16        """
17        # Build a dictionary where each character maps to a set of characters it can be replaced with
18        replacement_map = defaultdict(set)
19        for old_char, new_char in mappings:
20            replacement_map[old_char].add(new_char)
21      
22        # Try to match the substring at each possible starting position in s
23        for start_idx in range(len(s) - len(sub) + 1):
24            # Extract the candidate substring from s
25            candidate = s[start_idx : start_idx + len(sub)]
26          
27            # Check if every character pair matches (either directly or through mapping)
28            is_match = True
29            for char_in_s, char_in_sub in zip(candidate, sub):
30                # Characters match if they're identical OR if char_in_s can replace char_in_sub
31                if not (char_in_s == char_in_sub or char_in_s in replacement_map[char_in_sub]):
32                    is_match = False
33                    break
34          
35            if is_match:
36                return True
37      
38        return False
39
1class Solution {
2    public boolean matchReplacement(String s, String sub, char[][] mappings) {
3        // Build a map where each character maps to a set of characters it can be replaced with
4        // Key: original character from sub, Value: set of possible replacement characters
5        Map<Character, Set<Character>> replacementMap = new HashMap<>();
6      
7        // Process each mapping rule [original, replacement]
8        for (char[] mapping : mappings) {
9            char originalChar = mapping[0];
10            char replacementChar = mapping[1];
11          
12            // If the original character doesn't exist in map, create a new HashSet for it
13            // Then add the replacement character to the set
14            replacementMap.computeIfAbsent(originalChar, key -> new HashSet<>())
15                         .add(replacementChar);
16        }
17      
18        int mainStringLength = s.length();
19        int subStringLength = sub.length();
20      
21        // Try to match substring starting from each possible position in the main string
22        for (int startIndex = 0; startIndex <= mainStringLength - subStringLength; startIndex++) {
23            boolean isMatch = true;
24          
25            // Check if substring matches at current starting position
26            for (int subIndex = 0; subIndex < subStringLength && isMatch; subIndex++) {
27                char mainChar = s.charAt(startIndex + subIndex);
28                char subChar = sub.charAt(subIndex);
29              
30                // Characters must either be equal, or mainChar must be a valid replacement for subChar
31                boolean charactersMatch = (mainChar == subChar);
32                boolean isValidReplacement = replacementMap.getOrDefault(subChar, Collections.emptySet())
33                                                           .contains(mainChar);
34              
35                if (!charactersMatch && !isValidReplacement) {
36                    isMatch = false;
37                }
38            }
39          
40            // If we found a complete match at this position, return true
41            if (isMatch) {
42                return true;
43            }
44        }
45      
46        // No match found at any starting position
47        return false;
48    }
49}
50
1class Solution {
2public:
3    bool matchReplacement(string s, string sub, vector<vector<char>>& mappings) {
4        // Build a mapping dictionary where key is the original character
5        // and value is a set of characters it can be replaced with
6        unordered_map<char, unordered_set<char>> replacementMap;
7        for (auto& mapping : mappings) {
8            replacementMap[mapping[0]].insert(mapping[1]);
9        }
10      
11        int mainStringLength = s.size();
12        int subStringLength = sub.size();
13      
14        // Try to match substring starting from each possible position in main string
15        for (int startPos = 0; startPos <= mainStringLength - subStringLength; ++startPos) {
16            bool isMatch = true;
17          
18            // Check if substring matches at current position
19            for (int offset = 0; offset < subStringLength && isMatch; ++offset) {
20                char mainChar = s[startPos + offset];
21                char subChar = sub[offset];
22              
23                // Characters must either be equal, or mainChar must be a valid replacement for subChar
24                if (mainChar != subChar && !replacementMap[subChar].count(mainChar)) {
25                    isMatch = false;
26                }
27            }
28          
29            // If we found a valid match, return true
30            if (isMatch) {
31                return true;
32            }
33        }
34      
35        // No valid match found
36        return false;
37    }
38};
39
1function matchReplacement(s: string, sub: string, mappings: string[][]): boolean {
2    // Build a mapping dictionary where key is the original character
3    // and value is a set of characters it can be replaced with
4    const replacementMap: Map<string, Set<string>> = new Map();
5  
6    for (const mapping of mappings) {
7        if (!replacementMap.has(mapping[0])) {
8            replacementMap.set(mapping[0], new Set());
9        }
10        replacementMap.get(mapping[0])!.add(mapping[1]);
11    }
12  
13    const mainStringLength: number = s.length;
14    const subStringLength: number = sub.length;
15  
16    // Try to match substring starting from each possible position in main string
17    for (let startPos = 0; startPos <= mainStringLength - subStringLength; startPos++) {
18        let isMatch: boolean = true;
19      
20        // Check if substring matches at current position
21        for (let offset = 0; offset < subStringLength && isMatch; offset++) {
22            const mainChar: string = s[startPos + offset];
23            const subChar: string = sub[offset];
24          
25            // Characters must either be equal, or mainChar must be a valid replacement for subChar
26            if (mainChar !== subChar && 
27                (!replacementMap.has(subChar) || !replacementMap.get(subChar)!.has(mainChar))) {
28                isMatch = false;
29            }
30        }
31      
32        // If we found a valid match, return true
33        if (isMatch) {
34            return true;
35        }
36    }
37  
38    // No valid match found
39    return false;
40}
41

Time and Space Complexity

Time Complexity: O(m × n)

The algorithm iterates through all possible starting positions in string s, which gives us m - n + 1 iterations where m = len(s) and n = len(sub). For each starting position, we check if the substring matches by comparing each character pair, which takes O(n) time. The character comparison itself (checking a == b or a in d[b]) is O(1) since we're using a set for lookups. Therefore, the overall time complexity is O((m - n + 1) × n), which simplifies to O(m × n).

Space Complexity: O(C²)

The space is primarily consumed by the dictionary d which stores the mapping relationships. In the worst case, every character in the character set C could map to every other character, resulting in possible mappings. Each mapping is stored as a character in a set within the dictionary. Since we're storing at most C sets (one for each possible key character) and each set can contain at most C characters (all possible replacement characters), the space complexity is O(C²).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Misunderstanding the Direction of Replacement

The Problem: A common mistake is confusing which character can replace which. The mappings [old_i, new_i] mean that old_i in sub can be replaced with new_i. Many people incorrectly interpret this as "new_i can replace old_i" and build their mapping dictionary backwards.

Incorrect Implementation:

# WRONG: This reverses the mapping direction
for old_char, new_char in mappings:
    replacement_map[new_char].add(old_char)  # Backwards!

Why This Fails: If you have mapping ['a', 'x'], this means 'a' in sub can become 'x'. So when checking if substring "xyz" in s matches sub = "abc", we need to check if 'x' (from s) can match 'a' (from sub). The correct check is 'x' in replacement_map['a'], not the other way around.

Solution: Always remember that mappings define what characters in sub can be replaced with. Build the dictionary with the character from sub as the key:

# CORRECT: old_char is from sub, new_char is what it can become
for old_char, new_char in mappings:
    replacement_map[old_char].add(new_char)

Pitfall 2: Attempting to Chain Replacements

The Problem: Some might try to apply multiple replacements to the same character position, thinking if 'a' can become 'b' and 'b' can become 'c', then 'a' can become 'c'.

Example Scenario:

mappings = [['a', 'b'], ['b', 'c']]
sub = "a"
s = "c"

Why This is Wrong: The problem explicitly states each character position can only be replaced once. You cannot chain replacements. In the above example, 'a' can only become 'b', not 'c'.

Solution: Only consider direct mappings. Don't compute transitive closures or attempt to chain replacements. The current solution correctly handles this by only checking direct mappings.

Pitfall 3: Modifying sub Instead of Checking Against s

The Problem: Some might try to generate all possible variations of sub and then check if any of them exist in s. This approach becomes exponentially complex.

Inefficient Approach:

# WRONG: Trying to generate all possible variations
def generate_all_variations(sub, mappings):
    # This could create 2^n variations for n characters!
    variations = set()
    # ... complex recursive generation ...

Why This Fails: If sub has length n and each character has k possible replacements, you could have up to (k+1)^n variations to check, making this approach impractical for large inputs.

Solution: The correct approach (as shown in the solution) is to iterate through s and check if each substring can match sub with the allowed replacements. This is much more efficient with O(n × m) complexity.

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

A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".

What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?


Recommended Readings

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

Load More