2301. Match Substring After Replacement
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:
- Building a hash table
d
where each key is a character fromsub
and the value is a set of all characters it can be replaced with - Checking every possible starting position in
s
for a substring of length equal tosub
- 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]
)
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:
- They are already the same character, OR
- The character in
sub
can be replaced to match the character ins
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 replacementa in d[b]
: Characterb
insub
can be replaced with charactera
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 EvaluatorExample 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 C²
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.
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
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
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!