1638. Count Substrings That Differ by One Character
Problem Description
You are given two strings s
and t
. Your task is to count how many ways you can:
- Choose a non-empty substring from string
s
- Change exactly one character in that substring
- Make the resulting substring match some substring in string
t
The key requirement is that the substring from s
and the substring from t
must differ by exactly one character - no more, no less.
For example, if s = "computer"
and t = "computation"
, one valid way would be:
- Take substring
"compute"
froms
(positions 0-6) - Change the
'e'
to'a'
to get"computa"
- This matches the substring
"computa"
int
(positions 0-6)
The solution approach works by:
- Iterating through each position
i
in strings
and positionj
in stringt
- When characters at these positions differ (
s[i] != t[j]
), treating this as the single differing character - Extending left to count how many matching characters exist before this position
- Extending right to count how many matching characters exist after this position
- The total valid substrings with this differing position is
(l + 1) * (r + 1)
, wherel
is the matching length on the left andr
is the matching length on the right
The algorithm counts all possible substrings that can be formed by choosing different starting and ending positions around each valid differing character position.
Intuition
The key insight is that for two substrings to differ by exactly one character, there must be a single "mismatch point" where the characters are different, with all other characters matching perfectly.
Think of it this way: if we fix a mismatch point at positions (i, j)
where s[i] != t[j]
, then any valid substring pair must:
- Include this mismatch point
- Have all other characters matching
So the natural question becomes: given a fixed mismatch point, how many valid substring pairs can we form around it?
This leads us to consider:
- How far can we extend to the left while characters still match?
- How far can we extend to the right while characters still match?
If we can extend l
positions to the left and r
positions to the right, then we have choices:
- We can start our substring anywhere from
i-l
toi
(that'sl+1
choices) - We can end our substring anywhere from
i
toi+r
(that'sr+1
choices)
Since these choices are independent, the total number of valid substrings centered at this mismatch point is (l+1) * (r+1)
.
By iterating through all possible mismatch points (i, j)
where s[i] != t[j]
and calculating the number of valid substrings for each, we get the total count. This approach ensures we count each valid substring pair exactly once, as each pair has exactly one mismatch point that uniquely identifies it.
Learn more about Dynamic Programming patterns.
Solution Approach
The implementation uses a nested loop approach to examine all possible mismatch points between strings s
and t
.
Main Algorithm Steps:
-
Iterate through all possible mismatch points:
- Use two nested loops with indices
i
for strings
andj
for stringt
- Check each pair of positions
(i, j)
to find wheres[i] != t[j]
- Use two nested loops with indices
-
For each mismatch point found:
- Initialize two counters:
l
for left extension andr
for right extension
- Initialize two counters:
-
Extend left from the mismatch point:
while i > l and j > l and s[i - l - 1] == t[j - l - 1]: l += 1
- Keep moving left while characters match
- Stop when we reach the beginning of either string or find a mismatch
l
represents how many matching positions exist to the left
-
Extend right from the mismatch point:
while i + r + 1 < m and j + r + 1 < n and s[i + r + 1] == t[j + r + 1]: r += 1
- Keep moving right while characters match
- Stop when we reach the end of either string or find a mismatch
r
represents how many matching positions exist to the right
-
Count valid substrings for this mismatch point:
- Calculate
(l + 1) * (r + 1)
- This represents all possible substring combinations that include the mismatch point
- Add this count to the running total
ans
- Calculate
Time Complexity: O(m * n * min(m, n))
where m = len(s)
and n = len(t)
. The nested loops give us O(m * n)
, and for each mismatch point, we might extend up to min(m, n)
positions.
Space Complexity: O(1)
as we only use a constant amount of extra space for variables.
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 small example with s = "abc"
and t = "bbc"
.
We need to find all ways to change exactly one character in a substring of s
to match a substring in t
.
Step 1: Check position (i=0, j=0)
s[0] = 'a'
andt[0] = 'b'
→ They differ! This is a potential mismatch point.- Extend left: Can't extend (already at the beginning), so
l = 0
- Extend right: Check if
s[1] = 'b'
equalst[1] = 'b'
→ Yes!r = 1
- Check if
s[2] = 'c'
equalst[2] = 'c'
→ Yes!r = 2
- Can't extend further (reached end), so
r = 2
- Check if
- Valid substrings:
(0 + 1) * (2 + 1) = 3
- These are: "a"→"b", "ab"→"bb", "abc"→"bbc"
Step 2: Check position (i=0, j=1)
s[0] = 'a'
andt[1] = 'b'
→ They differ!- Extend left: Can't extend (j would go negative), so
l = 0
- Extend right: Check if
s[1] = 'b'
equalst[2] = 'c'
→ No!r = 0
- Valid substrings:
(0 + 1) * (0 + 1) = 1
- This is: "a"→"b"
Step 3: Check position (i=0, j=2)
s[0] = 'a'
andt[2] = 'c'
→ They differ!- Extend left: Can't extend, so
l = 0
- Extend right: Check if
s[1] = 'b'
would equalt[3]
→ Out of bounds!r = 0
- Valid substrings:
(0 + 1) * (0 + 1) = 1
- This is: "a"→"c"
Step 4: Check position (i=1, j=0)
s[1] = 'b'
andt[0] = 'b'
→ They match! Skip this pair.
Step 5: Check position (i=1, j=1)
s[1] = 'b'
andt[1] = 'b'
→ They match! Skip.
Step 6: Check position (i=1, j=2)
s[1] = 'b'
andt[2] = 'c'
→ They differ!- Extend left: Check if
s[0] = 'a'
equalst[1] = 'b'
→ No!l = 0
- Extend right: Check if
s[2] = 'c'
would equalt[3]
→ Out of bounds!r = 0
- Valid substrings:
(0 + 1) * (0 + 1) = 1
- This is: "b"→"c"
Step 7: Check position (i=2, j=0)
s[2] = 'c'
andt[0] = 'b'
→ They differ!- Extend left: Check if
s[1] = 'b'
would equalt[-1]
→ Out of bounds!l = 0
- Extend right: Check if
s[3]
would exist → Out of bounds!r = 0
- Valid substrings:
(0 + 1) * (0 + 1) = 1
- This is: "c"→"b"
Step 8: Check positions (i=2, j=1) and (i=2, j=2)
s[2] = 'c'
andt[1] = 'b'
→ They differ! Count = 1 (substring "c"→"b")s[2] = 'c'
andt[2] = 'c'
→ They match! Skip.
Total count: 3 + 1 + 1 + 1 + 1 + 1 = 8
The algorithm found 8 different ways to change exactly one character in a substring of s
to match a substring in t
.
Solution Implementation
1class Solution:
2 def countSubstrings(self, s: str, t: str) -> int:
3 """
4 Count substrings from s and t that differ by exactly one character.
5
6 Args:
7 s: First string
8 t: Second string
9
10 Returns:
11 Number of substring pairs that differ by exactly one character
12 """
13 total_count = 0
14 len_s, len_t = len(s), len(t)
15
16 # Iterate through all character pairs from both strings
17 for i in range(len_s):
18 for j in range(len_t):
19 # Check if characters at current positions differ
20 if s[i] != t[j]:
21 # Count matching characters to the left of mismatch
22 left_matches = 0
23 while (i - left_matches - 1 >= 0 and
24 j - left_matches - 1 >= 0 and
25 s[i - left_matches - 1] == t[j - left_matches - 1]):
26 left_matches += 1
27
28 # Count matching characters to the right of mismatch
29 right_matches = 0
30 while (i + right_matches + 1 < len_s and
31 j + right_matches + 1 < len_t and
32 s[i + right_matches + 1] == t[j + right_matches + 1]):
33 right_matches += 1
34
35 # Calculate total substrings with this mismatch point
36 # (left_matches + 1) possible starting positions
37 # (right_matches + 1) possible ending positions
38 total_count += (left_matches + 1) * (right_matches + 1)
39
40 return total_count
41
1class Solution {
2 public int countSubstrings(String s, String t) {
3 int totalCount = 0;
4 int sLength = s.length();
5 int tLength = t.length();
6
7 // Iterate through each position in string s
8 for (int sIndex = 0; sIndex < sLength; ++sIndex) {
9 // Iterate through each position in string t
10 for (int tIndex = 0; tIndex < tLength; ++tIndex) {
11 // Check if characters at current positions are different
12 if (s.charAt(sIndex) != t.charAt(tIndex)) {
13 // Count matching characters to the left of the mismatch
14 int leftMatchCount = 0;
15 while (sIndex - leftMatchCount > 0 &&
16 tIndex - leftMatchCount > 0 &&
17 s.charAt(sIndex - leftMatchCount - 1) == t.charAt(tIndex - leftMatchCount - 1)) {
18 ++leftMatchCount;
19 }
20
21 // Count matching characters to the right of the mismatch
22 int rightMatchCount = 0;
23 while (sIndex + rightMatchCount + 1 < sLength &&
24 tIndex + rightMatchCount + 1 < tLength &&
25 s.charAt(sIndex + rightMatchCount + 1) == t.charAt(tIndex + rightMatchCount + 1)) {
26 ++rightMatchCount;
27 }
28
29 // Calculate total substrings with exactly one mismatch at this position
30 // (leftMatchCount + 1) represents possible starting positions
31 // (rightMatchCount + 1) represents possible ending positions
32 totalCount += (leftMatchCount + 1) * (rightMatchCount + 1);
33 }
34 }
35 }
36
37 return totalCount;
38 }
39}
40
1class Solution {
2public:
3 int countSubstrings(string s, string t) {
4 int result = 0;
5 int sLength = s.size();
6 int tLength = t.size();
7
8 // Iterate through each character position in string s
9 for (int i = 0; i < sLength; ++i) {
10 // Iterate through each character position in string t
11 for (int j = 0; j < tLength; ++j) {
12 // Find positions where characters differ (mismatch point)
13 if (s[i] != t[j]) {
14 // Count matching characters to the left of mismatch
15 int leftMatchCount = 0;
16 while (i - leftMatchCount > 0 &&
17 j - leftMatchCount > 0 &&
18 s[i - leftMatchCount - 1] == t[j - leftMatchCount - 1]) {
19 ++leftMatchCount;
20 }
21
22 // Count matching characters to the right of mismatch
23 int rightMatchCount = 0;
24 while (i + rightMatchCount + 1 < sLength &&
25 j + rightMatchCount + 1 < tLength &&
26 s[i + rightMatchCount + 1] == t[j + rightMatchCount + 1]) {
27 ++rightMatchCount;
28 }
29
30 // Calculate total substrings with exactly one difference at position (i, j)
31 // (leftMatchCount + 1) represents possible starting positions
32 // (rightMatchCount + 1) represents possible ending positions
33 result += (leftMatchCount + 1) * (rightMatchCount + 1);
34 }
35 }
36 }
37
38 return result;
39 }
40};
41
1function countSubstrings(s: string, t: string): number {
2 let result = 0;
3 const sLength = s.length;
4 const tLength = t.length;
5
6 // Iterate through each character position in string s
7 for (let i = 0; i < sLength; i++) {
8 // Iterate through each character position in string t
9 for (let j = 0; j < tLength; j++) {
10 // Find positions where characters differ (mismatch point)
11 if (s[i] !== t[j]) {
12 // Count matching characters to the left of mismatch
13 let leftMatchCount = 0;
14 while (i - leftMatchCount > 0 &&
15 j - leftMatchCount > 0 &&
16 s[i - leftMatchCount - 1] === t[j - leftMatchCount - 1]) {
17 leftMatchCount++;
18 }
19
20 // Count matching characters to the right of mismatch
21 let rightMatchCount = 0;
22 while (i + rightMatchCount + 1 < sLength &&
23 j + rightMatchCount + 1 < tLength &&
24 s[i + rightMatchCount + 1] === t[j + rightMatchCount + 1]) {
25 rightMatchCount++;
26 }
27
28 // Calculate total substrings with exactly one difference at position (i, j)
29 // (leftMatchCount + 1) represents possible starting positions
30 // (rightMatchCount + 1) represents possible ending positions
31 result += (leftMatchCount + 1) * (rightMatchCount + 1);
32 }
33 }
34 }
35
36 return result;
37}
38
Time and Space Complexity
Time Complexity: O(m * n * min(m, n))
where m = len(s)
and n = len(t)
The algorithm uses nested loops to iterate through all pairs of indices (i, j)
where i
is from string s
and j
is from string t
. This gives us O(m * n)
iterations. For each pair where s[i] != t[j]
, the algorithm extends left and right to count matching characters. In the worst case, these extensions can go up to min(m, n)
characters (limited by the shorter string length). Therefore, the overall time complexity is O(m * n * min(m, n))
.
Space Complexity: O(1)
The algorithm only uses a constant amount of extra space for variables ans
, m
, n
, i
, j
, a
, b
, l
, and r
. No additional data structures that scale with input size are created, making the space complexity constant.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Attempting to Use Set or Hash-based Approaches
A common mistake is trying to generate all substrings from both strings and compare them using sets or hash tables. This approach fails because:
- Memory explosion: Storing all possible substrings requires O(m²) + O(n²) space
- Incorrect counting: Simply checking if substrings differ by one character doesn't handle the counting correctly
- Performance issues: Generating and comparing all substring pairs is inefficient
Incorrect approach example:
# DON'T DO THIS - generates all substrings first
def countSubstrings_wrong(s, t):
s_substrings = set()
for i in range(len(s)):
for j in range(i+1, len(s)+1):
s_substrings.add(s[i:j])
# Then try to match with t substrings...
2. Double Counting or Missing Valid Substrings
When implementing the extension logic, it's easy to:
- Count the same substring pair multiple times
- Miss valid substrings by incorrect boundary checks
Key insight: The algorithm avoids double counting by treating each (i, j)
mismatch position uniquely. Each substring pair is counted exactly once because it has exactly one mismatch position.
3. Incorrect Boundary Conditions in Extension Logic
The while loops for extending left and right must check multiple conditions simultaneously:
Wrong implementation:
# Missing proper boundary checks while s[i - l - 1] == t[j - l - 1]: # IndexError! l += 1
Correct implementation:
# All boundary conditions must be checked while (i > l and j > l and s[i - l - 1] == t[j - l - 1]): l += 1
4. Misunderstanding the Multiplication Formula
The formula (l + 1) * (r + 1)
might seem arbitrary but represents:
(l + 1)
: Number of possible starting positions (including positioni-l
)(r + 1)
: Number of possible ending positions (including positioni+r
)
Each combination of start and end position that includes the mismatch point at (i, j)
creates a valid substring pair.
5. Handling Edge Cases Incorrectly
Special cases to consider:
- Single character strings
- Strings with no common characters
- Strings where one is much longer than the other
The algorithm naturally handles these because:
- If no mismatches exist between positions, nothing is counted
- Extension stops at string boundaries automatically
- The nested loops ensure all valid position pairs are checked
Solution verification tip: Test with simple examples like s="a", t="b"
(should return 1) and s="ab", t="bb"
(should return 3) to ensure your implementation handles basic cases correctly.
How does merge sort divide the problem into subproblems?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
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!