LeetCode 2301 - Match Substring After Replacement
The problem gives us a string s, a target string sub, and a list of character replacement rules called mappings. Each mapping [old, new] means that a character old inside sub may be replaced with new.
Difficulty: 🔴 Hard
Topics: Array, Hash Table, String, String Matching
Solution
Problem Understanding
The problem gives us a string s, a target string sub, and a list of character replacement rules called mappings.
Each mapping [old, new] means that a character old inside sub may be replaced with new. The replacement is optional, and every character in sub can be replaced at most once. Importantly, replacements only apply from old to new, not the other way around.
Our task is to determine whether, after applying zero or more valid replacements to characters in sub, the resulting string can appear as a contiguous substring inside s.
The important detail is that replacements are conceptual, we do not actually need to generate all possible transformed versions of sub. Instead, we only need to check whether there exists some alignment between sub and a substring of s where every character matches either:
- Directly, meaning
sub[i] == s[j] - Through a valid replacement mapping, meaning
sub[i]can becomes[j]
The constraints are important:
s.lengthandsub.lengthare at most5000mappings.lengthis at most1000
A brute force solution that generates every possible transformed version of sub would explode combinatorially. Even a moderate number of replaceable characters could produce an enormous number of candidate strings.
However, the string sizes are small enough that checking every possible starting position in s is feasible, as long as each comparison is efficient.
Some important edge cases include:
- No mappings are provided
subalready appears inswithout replacements- Multiple mappings exist for the same character
- A character in
subcannot be replaced into the required character - Uppercase and lowercase characters are different
- Digits and letters may both appear
- Replacement direction matters strictly
For example, if we have mapping ["o", "0"], then o -> 0 is allowed, but 0 -> o is not.
Approaches
Brute Force Approach
A naive approach would attempt to generate every possible transformed version of sub using the replacement mappings.
Suppose a character in sub has multiple possible replacement options. Then every occurrence of that character creates branching possibilities. We could recursively try:
- Keep the original character
- Replace it with one of its mapped characters
After generating each possible transformed string, we would check whether it exists as a substring of s.
This approach is correct because it explicitly explores all valid transformed versions of sub. If any transformed version appears in s, the answer is true.
The problem is that this becomes exponentially expensive. If several positions in sub each have multiple replacement choices, the number of generated strings grows explosively.
For example, if 20 characters each have 2 possibilities, we already have:
$$2^{20}$$
possible strings.
That is completely infeasible for the given constraints.
Optimal Approach
The key observation is that we do not need to generate transformed strings at all.
Instead, we can directly compare sub against every substring window of s with the same length.
For every aligned pair of characters:
- If the characters are equal, the position matches
- Otherwise, we check whether a replacement mapping allows the character from
subto become the character froms
If every character position matches under these rules, then we found a valid transformed substring.
This works because each character replacement is independent. We never need to track complicated state or sequences of transformations. A position either matches directly, matches via mapping, or fails.
We preprocess the mappings into a hash set for constant time lookup.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | Exponential | Exponential | Generates all transformed versions of sub |
| Optimal | O((n - m + 1) × m) | O(k) | Checks each substring window using hash lookups |
Where:
n = len(s)m = len(sub)k = len(mappings)
Algorithm Walkthrough
- Create a hash set to store all valid replacement mappings.
Each mapping is stored as a pair (old_char, new_char). Using a hash set allows constant time lookup during substring comparisons.
2. Iterate through every possible starting position in s.
Since sub has length m, the last valid starting index is:
$$n - m$$
For every starting position, we attempt to match the entire sub.
3. Compare characters one by one.
For every index j inside sub:
- Let
sub_char = sub[j] - Let
s_char = s[start + j]
The position is valid if either:
sub_char == s_char(sub_char, s_char)exists in the mapping set
- Stop early on mismatch.
If neither condition holds, the current substring window cannot work, so we immediately move to the next starting position.
5. Return true if an entire window matches.
If all characters in a window satisfy the matching rules, then sub can be transformed into that substring.
6. Return false if no window works.
After checking all possible positions, if none match successfully, then the transformation is impossible.
Why it works
The algorithm checks every possible substring of s that could match sub. For each aligned character pair, it validates exactly the conditions allowed by the problem:
- Exact equality
- One valid replacement
Because replacements are independent per character and applied at most once, checking positions independently is sufficient. Therefore, if the algorithm finds a fully matching window, a valid transformed substring exists. If all windows fail, no valid transformation can produce a substring of s.
Python Solution
from typing import List, Set, Tuple
class Solution:
def matchReplacement(
self,
s: str,
sub: str,
mappings: List[List[str]]
) -> bool:
allowed: Set[Tuple[str, str]] = set()
for old_char, new_char in mappings:
allowed.add((old_char, new_char))
n = len(s)
m = len(sub)
for start in range(n - m + 1):
match = True
for j in range(m):
sub_char = sub[j]
s_char = s[start + j]
if sub_char == s_char:
continue
if (sub_char, s_char) not in allowed:
match = False
break
if match:
return True
return False
The implementation begins by preprocessing all mappings into a hash set called allowed. This lets us check whether a replacement is valid in constant time.
Next, we compute the lengths of s and sub. We then iterate over every possible substring window in s that has the same length as sub.
For each window, we compare characters position by position. If the characters already match, we continue immediately. Otherwise, we check whether the pair exists in the mapping set.
If a mismatch occurs that cannot be repaired through replacement, we stop checking the current window early using break.
If we finish checking an entire window without failure, we immediately return True.
If all windows fail, the function returns False.
Go Solution
func matchReplacement(s string, sub string, mappings [][]byte) bool {
allowed := make(map[byte]map[byte]bool)
for _, mapping := range mappings {
oldChar := mapping[0]
newChar := mapping[1]
if allowed[oldChar] == nil {
allowed[oldChar] = make(map[byte]bool)
}
allowed[oldChar][newChar] = true
}
n := len(s)
m := len(sub)
for start := 0; start <= n-m; start++ {
match := true
for j := 0; j < m; j++ {
subChar := sub[j]
sChar := s[start+j]
if subChar == sChar {
continue
}
if allowed[subChar] == nil || !allowed[subChar][sChar] {
match = false
break
}
}
if match {
return true
}
}
return false
}
The Go implementation uses a nested map structure instead of a tuple hash set because Go does not have Python-style tuple hashing built in.
The structure:
map[byte]map[byte]bool
stores all valid replacement relationships.
The rest of the logic mirrors the Python version closely. String indexing in Go operates directly on bytes, which works correctly because the problem guarantees ASCII characters only.
Worked Examples
Example 1
s = "fool3e7bar"
sub = "leet"
mappings = [
["e", "3"],
["t", "7"],
["t", "8"]
]
The mapping set becomes:
| Original | Replacement |
|---|---|
| e | 3 |
| t | 7 |
| t | 8 |
We examine each possible window of length 4.
| Start | Window | Comparison Result |
|---|---|---|
| 0 | fool | l != f, fail |
| 1 | ool3 | l != o, fail |
| 2 | ol3e | l != o, fail |
| 3 | l3e7 | valid |
Detailed comparison for "l3e7":
| sub char | window char | Valid? |
|---|---|---|
| l | l | direct match |
| e | 3 | mapped |
| e | e | direct match |
| t | 7 | mapped |
All positions match, so the answer is true.
Example 2
s = "fooleetbar"
sub = "f00l"
mappings = [["o", "0"]]
The only allowed replacement is:
o -> 0
Not:
0 -> o
Checking substring "fool":
| sub char | window char | Valid? |
|---|---|---|
| f | f | direct match |
| 0 | o | invalid |
The replacement direction is wrong.
No valid window exists, so the answer is false.
Example 3
s = "Fool33tbaR"
sub = "leetd"
Mappings:
| Original | Replacement |
|---|---|
| e | 3 |
| t | 7 |
| t | 8 |
| d | b |
| p | b |
Candidate window:
l33tb
Character comparisons:
| sub char | window char | Valid? |
|---|---|---|
| l | l | direct |
| e | 3 | mapped |
| e | 3 | mapped |
| t | t | direct |
| d | b | mapped |
Every position matches, so the answer is true.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O((n - m + 1) × m) | We check every substring window and compare up to m characters |
| Space | O(k) | The mapping hash set stores all replacement pairs |
Where:
n = len(s)m = len(sub)k = len(mappings)
The worst case occurs when every substring window must be checked completely before failing. Since there are n - m + 1 windows and each may require m comparisons, the total running time is quadratic in the worst case.
The space complexity is proportional only to the number of mapping pairs stored in the hash structure.
Test Cases
sol = Solution()
# Provided examples
assert sol.matchReplacement(
"fool3e7bar",
"leet",
[["e", "3"], ["t", "7"], ["t", "8"]]
) is True # basic mapping success
assert sol.matchReplacement(
"fooleetbar",
"f00l",
[["o", "0"]]
) is False # replacement direction matters
assert sol.matchReplacement(
"Fool33tbaR",
"leetd",
[["e", "3"], ["t", "7"], ["t", "8"], ["d", "b"], ["p", "b"]]
) is True # multiple replacements
# Exact substring without mappings
assert sol.matchReplacement(
"abcdef",
"bcd",
[]
) is True # direct substring match
# No possible match
assert sol.matchReplacement(
"abcdef",
"xyz",
[]
) is False # completely different strings
# Single character replacement
assert sol.matchReplacement(
"a1c",
"abc",
[["b", "1"]]
) is True # one replacement needed
# Multiple mappings for same character
assert sol.matchReplacement(
"a2c",
"abc",
[["b", "1"], ["b", "2"], ["b", "3"]]
) is True # multiple valid replacements
# Case sensitivity
assert sol.matchReplacement(
"abc",
"Abc",
[["A", "a"]]
) is True # uppercase replacement
# Entire string match
assert sol.matchReplacement(
"hello",
"hello",
[]
) is True # full string equality
# Mapping exists but wrong direction
assert sol.matchReplacement(
"abc",
"a1c",
[["1", "b"]]
) is False # cannot reverse mapping
# Minimum size strings
assert sol.matchReplacement(
"a",
"a",
[]
) is True # smallest valid input
# Single character replacement success
assert sol.matchReplacement(
"z",
"a",
[["a", "z"]]
) is True # single replacement
# Repeated characters
assert sol.matchReplacement(
"1111",
"aaaa",
[["a", "1"]]
) is True # repeated mapping usage
# Window fails late
assert sol.matchReplacement(
"abcde",
"abcf",
[["f", "e"]]
) is True # last character replacement
| Test | Why |
|---|---|
| Example 1 | Standard successful replacement |
| Example 2 | Verifies replacement direction |
| Example 3 | Multiple simultaneous replacements |
| Exact substring | Ensures zero replacements work |
| No match | Confirms proper failure behavior |
| Single replacement | Small simple transformation |
| Multiple mappings | Same source character has many targets |
| Case sensitivity | Uppercase and lowercase treated differently |
| Entire string | Full-string matching |
| Wrong direction | Prevents reverse replacement |
| Minimum size | Smallest valid constraints |
| Single char replacement | One-character transformation |
| Repeated characters | Same mapping used multiple times |
| Late failure | Ensures full window comparison correctness |
Edge Cases
One important edge case occurs when no mappings are provided. In this situation, the problem reduces to a normal substring search. A buggy implementation might incorrectly assume replacements are always available. Our solution handles this naturally because the mapping set is simply empty, so only direct character equality succeeds.
Another important edge case is replacement direction. The mapping ["o", "0"] allows o -> 0, but not 0 -> o. A common bug is accidentally treating mappings as bidirectional. Our implementation stores mappings exactly as ordered pairs and only checks whether (sub_char, s_char) exists, preserving the correct directionality.
Repeated characters also create subtle issues. Suppose sub = "aaaa" and every a must transform into 1. A flawed implementation might incorrectly think a mapping can only be used once globally. However, the problem only states that each individual character can be replaced at most once. Different positions may independently apply the same mapping. Our algorithm checks each position independently, so repeated usage works correctly.
Case sensitivity is another potential source of mistakes. Since uppercase and lowercase letters are distinct ASCII characters, "A" and "a" are not interchangeable unless explicitly mapped. Our implementation compares characters exactly as given, so case-sensitive behavior is preserved automatically.