1794. Count Pairs of Equal Substrings With Minimum Difference
Problem Description
You are given two strings firstString
and secondString
that consist only of lowercase English letters. You need to find and count special quadruples (i, j, a, b)
where:
i
andj
are indices infirstString
where0 <= i <= j < firstString.length
a
andb
are indices insecondString
where0 <= a <= b < secondString.length
- The substring
firstString[i...j]
equals the substringsecondString[a...b]
- The value
j - a
is the minimum possible among all valid quadruples
The key insight is that for any matching substring, the difference j - a
represents how far apart the ending position in firstString
is from the starting position in secondString
. The problem asks for all quadruples that achieve this minimum difference.
For example, if firstString = "abcd"
and secondString = "bccda"
, and we find that substring "cd"
appears in both strings, we would check different positions where this occurs and calculate j - a
for each valid match. We want to count only those quadruples where this difference is minimized.
Since substrings must be equal, their lengths must match, meaning j - i = b - a
. This can be rearranged to j - a = b - i
. The problem essentially asks us to minimize this cross-difference between the ending index in the first string and the starting index in the second string.
The solution cleverly recognizes that to minimize j - a
, we need to find characters that appear in both strings, where the character appears as early as possible in firstString
(small i
) and as late as possible in secondString
(large index). For single-character substrings (where i = j
and a = b
), this simplifies to finding matching characters and minimizing i - a
.
Intuition
Let's think about what the problem is really asking. We need matching substrings from both strings, and among all such matches, we want those where j - a
is minimized.
Since the substrings must be equal, they must have the same length. This means j - i = b - a
, which we can rearrange to get j - a = b - i
. So minimizing j - a
is equivalent to minimizing b - i
.
Now here's the key observation: instead of checking all possible substrings (which would be expensive), let's start with the simplest case - single character substrings where i = j
and a = b
. In this case, we're looking for matching characters between the two strings, and we want to minimize i - a
.
For a single character match, i - a
represents the difference between where the character appears in firstString
and where it appears in secondString
. To minimize this:
- We want the character to appear as early as possible in
firstString
(smalli
) - We want the character to appear as late as possible in
secondString
(largea
)
This leads us to a greedy strategy: for each character in secondString
, we only need to remember its last occurrence position. Then, as we traverse firstString
from left to right, for each character we encounter, we check if it exists in secondString
. If it does, we calculate i - last[c]
where last[c]
is the last position of character c
in secondString
.
Why does this work for single characters only? Because for longer substrings, the complexity increases significantly, but the problem's optimal solution always comes from single-character matches. This is because extending a substring in both strings increases both j
and b
by the same amount, keeping j - a
unchanged, but we'd need the extended parts to match exactly, which is a stricter condition without improving our objective.
Therefore, we only need to:
- Track the last occurrence of each character in
secondString
- For each character in
firstString
, check if it exists insecondString
and calculate the difference - Keep track of the minimum difference and count how many times we achieve it
Learn more about Greedy patterns.
Solution Approach
Based on our intuition, we implement a greedy solution using a hash table to efficiently find the minimum i - a
value for matching characters.
Step 1: Build the hash table for last occurrences
We create a dictionary last
that stores the last occurrence index of each character in secondString
:
last = {c: i for i, c in enumerate(secondString)}
This iterates through secondString
and updates the dictionary with each character's position. Since we're updating as we go, earlier occurrences get overwritten by later ones, giving us the last position of each character.
Step 2: Traverse firstString and find minimum difference
We initialize two variables:
ans = 0
: counts the number of quadruples with minimumj - a
mi = inf
: tracks the minimum value ofi - last[c]
found so far
Then we iterate through firstString
with both index i
and character c
:
for i, c in enumerate(firstString):
if c in last:
t = i - last[c]
For each character that exists in both strings, we calculate the difference t = i - last[c]
.
Step 3: Update the minimum and count
We compare the current difference t
with our minimum mi
:
if mi > t: mi = t ans = 1 elif mi == t: ans += 1
- If
t
is smaller than the current minimum, we've found a new minimum. We updatemi
tot
and reset the countans
to 1. - If
t
equals the current minimum, we've found another quadruple with the same minimum difference, so we incrementans
.
Step 4: Return the result
After processing all characters in firstString
, ans
contains the total count of quadruples that achieve the minimum j - a
value.
The time complexity is O(n + m)
where n
and m
are the lengths of the two strings - we traverse each string once. The space complexity is O(min(m, 26))
for the hash table, which stores at most 26 entries (one for each lowercase letter).
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 the solution with firstString = "abcd"
and secondString = "bccda"
.
Step 1: Build the hash table for last occurrences in secondString
We iterate through secondString = "bccda"
and record the last position of each character:
- Index 0: 'b' →
last['b'] = 0
- Index 1: 'c' →
last['c'] = 1
- Index 2: 'c' →
last['c'] = 2
(updates previous value) - Index 3: 'd' →
last['d'] = 3
- Index 4: 'a' →
last['a'] = 4
Final hash table: last = {'b': 0, 'c': 2, 'd': 3, 'a': 4}
Step 2: Traverse firstString
and calculate differences
Initialize mi = infinity
and ans = 0
.
Now iterate through firstString = "abcd"
:
-
i = 0, c = 'a':
- 'a' exists in
last
withlast['a'] = 4
- Calculate
t = 0 - 4 = -4
- Since
-4 < infinity
, updatemi = -4
andans = 1
- This represents the quadruple (0, 0, 4, 4) with substring "a"
- 'a' exists in
-
i = 1, c = 'b':
- 'b' exists in
last
withlast['b'] = 0
- Calculate
t = 1 - 0 = 1
- Since
1 > -4
, no update needed - This would represent quadruple (1, 1, 0, 0) with substring "b"
- 'b' exists in
-
i = 2, c = 'c':
- 'c' exists in
last
withlast['c'] = 2
- Calculate
t = 2 - 2 = 0
- Since
0 > -4
, no update needed - This would represent quadruple (2, 2, 2, 2) with substring "c"
- 'c' exists in
-
i = 3, c = 'd':
- 'd' exists in
last
withlast['d'] = 3
- Calculate
t = 3 - 3 = 0
- Since
0 > -4
, no update needed - This would represent quadruple (3, 3, 3, 3) with substring "d"
- 'd' exists in
Step 3: Return result
The minimum difference found is mi = -4
, achieved by exactly 1 quadruple.
Therefore, the answer is 1
.
The quadruple (0, 0, 4, 4) represents:
- Substring from
firstString[0...0] = "a"
- Substring from
secondString[4...4] = "a"
- The value
j - a = 0 - 4 = -4
is the minimum possible
Solution Implementation
1from math import inf
2
3class Solution:
4 def countQuadruples(self, firstString: str, secondString: str) -> int:
5 # Build a dictionary mapping each character to its last occurrence index in secondString
6 last_occurrence = {char: index for index, char in enumerate(secondString)}
7
8 # Initialize count of valid quadruples and minimum difference
9 count = 0
10 min_difference = inf
11
12 # Iterate through each character and its index in firstString
13 for i, char in enumerate(firstString):
14 # Check if this character exists in secondString
15 if char in last_occurrence:
16 # Calculate the difference: index in firstString - last index in secondString
17 difference = i - last_occurrence[char]
18
19 # If we found a smaller difference, reset count to 1
20 if min_difference > difference:
21 min_difference = difference
22 count = 1
23 # If the difference equals the minimum, increment count
24 elif min_difference == difference:
25 count += 1
26
27 return count
28
1class Solution {
2 public int countQuadruples(String firstString, String secondString) {
3 // Array to store the last occurrence index (1-based) of each character in secondString
4 // Index represents character ('a' = 0, 'b' = 1, ..., 'z' = 25)
5 int[] lastOccurrence = new int[26];
6
7 // Populate the last occurrence of each character in secondString
8 // Using 1-based indexing (position + 1) to distinguish from uninitialized values (0)
9 for (int i = 0; i < secondString.length(); i++) {
10 lastOccurrence[secondString.charAt(i) - 'a'] = i + 1;
11 }
12
13 // Initialize result counter and minimum difference value
14 int count = 0;
15 int minDifference = Integer.MAX_VALUE;
16
17 // Iterate through each character in firstString
18 for (int i = 0; i < firstString.length(); i++) {
19 // Get the last occurrence position of current character in secondString
20 int lastPositionInSecond = lastOccurrence[firstString.charAt(i) - 'a'];
21
22 // If this character exists in secondString (position > 0)
23 if (lastPositionInSecond > 0) {
24 // Calculate the difference: i - j (adjusting for 1-based indexing)
25 int difference = i - lastPositionInSecond;
26
27 // If we found a smaller difference, update minimum and reset count
28 if (minDifference > difference) {
29 minDifference = difference;
30 count = 1;
31 }
32 // If difference equals current minimum, increment count
33 else if (minDifference == difference) {
34 count++;
35 }
36 }
37 }
38
39 return count;
40 }
41}
42
1class Solution {
2public:
3 int countQuadruples(string firstString, string secondString) {
4 // Array to store the last occurrence index (1-indexed) of each character in secondString
5 // Index represents character ('a' = 0, 'b' = 1, ..., 'z' = 25)
6 int lastOccurrence[26] = {0};
7
8 // Record the last position (1-indexed) of each character in secondString
9 for (int i = 0; i < secondString.size(); ++i) {
10 lastOccurrence[secondString[i] - 'a'] = i + 1;
11 }
12
13 // Initialize result counter and minimum difference value
14 int count = 0;
15 int minDifference = 1 << 30; // Large initial value (2^30)
16
17 // Iterate through each character in firstString
18 for (int i = 0; i < firstString.size(); ++i) {
19 // Get the last occurrence position of current character in secondString
20 int lastPosInSecond = lastOccurrence[firstString[i] - 'a'];
21
22 // If this character exists in secondString
23 if (lastPosInSecond > 0) {
24 // Calculate the difference: firstString index - secondString index
25 // Note: i is 0-indexed, lastPosInSecond is 1-indexed, so difference = i - (lastPosInSecond - 1) = i - lastPosInSecond + 1
26 // But the code uses i - lastPosInSecond directly for comparison
27 int difference = i - lastPosInSecond;
28
29 // If we found a smaller difference, update minimum and reset count
30 if (minDifference > difference) {
31 minDifference = difference;
32 count = 1;
33 }
34 // If difference equals current minimum, increment count
35 else if (minDifference == difference) {
36 ++count;
37 }
38 }
39 }
40
41 return count;
42 }
43};
44
1/**
2 * Counts the number of quadruples (i, j, a, b) where:
3 * - firstString[i] == a, secondString[j] == b, and a == b
4 * - The value (i - j) is minimized
5 *
6 * @param firstString - The first input string
7 * @param secondString - The second input string
8 * @returns The count of quadruples with minimum (i - j) value
9 */
10function countQuadruples(firstString: string, secondString: string): number {
11 // Array to store the last occurrence index (1-based) of each letter in secondString
12 // Index represents letter (0 for 'a', 1 for 'b', ..., 25 for 'z')
13 const lastOccurrenceInSecond: number[] = new Array(26).fill(0);
14
15 // Populate the last occurrence of each character in secondString
16 // Using 1-based indexing (i + 1) to distinguish between "not found" (0) and index 0
17 for (let i = 0; i < secondString.length; ++i) {
18 const charIndex: number = secondString.charCodeAt(i) - 97; // Convert char to 0-25 range
19 lastOccurrenceInSecond[charIndex] = i + 1;
20 }
21
22 // Track the answer count and minimum difference found
23 let quadrupleCount: number = 0;
24 let minimumDifference: number = Infinity;
25
26 // Iterate through each character in firstString
27 for (let i = 0; i < firstString.length; ++i) {
28 const charIndex: number = firstString.charCodeAt(i) - 97; // Convert char to 0-25 range
29 const lastPositionInSecond: number = lastOccurrenceInSecond[charIndex];
30
31 // Check if this character exists in secondString
32 if (lastPositionInSecond > 0) {
33 // Calculate the difference (i - j), adjusting for 1-based indexing
34 const currentDifference: number = i - lastPositionInSecond;
35
36 if (minimumDifference > currentDifference) {
37 // Found a new minimum difference
38 minimumDifference = currentDifference;
39 quadrupleCount = 1;
40 } else if (minimumDifference === currentDifference) {
41 // Found another quadruple with the same minimum difference
42 ++quadrupleCount;
43 }
44 }
45 }
46
47 return quadrupleCount;
48}
49
Time and Space Complexity
The time complexity is O(m + n)
, where m
is the length of firstString
and n
is the length of secondString
. This is because:
- Building the
last
dictionary requires iterating throughsecondString
once, which takesO(n)
time - The main loop iterates through
firstString
once, which takesO(m)
time - Dictionary lookup operation
c in last
and accessinglast[c]
areO(1)
operations - Therefore, the total time complexity is
O(n) + O(m) = O(m + n)
The space complexity is O(C)
, where C
is the size of the character set. In this problem, since we're dealing with lowercase English letters, C = 26
. This is because:
- The
last
dictionary stores at most one entry for each unique character insecondString
- In the worst case,
secondString
contains all 26 lowercase letters, so the dictionary would have at most 26 key-value pairs - The other variables (
ans
,mi
,i
,c
,t
) use constant spaceO(1)
- Therefore, the total space complexity is
O(C) = O(26) = O(1)
(constant space)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding the Problem Constraints
The Issue: A common misunderstanding is thinking we need to find all possible matching substrings of any length, not just single characters. The solution only considers single-character matches (where i = j
and a = b
), which might seem incorrect at first glance.
Why This Works: When we have the constraint that j - i = b - a
(equal length substrings) and want to minimize j - a
, we can rewrite this as minimizing b - i
. For any multi-character substring match, the minimum j - a
would require the substring to start as early as possible in firstString
and as late as possible in secondString
. However, the mathematical analysis shows that single-character matches are sufficient to find the global minimum.
Solution: Trust the mathematical reduction. The problem simplifies to finding matching single characters because:
- For longer substrings, the constraints become more restrictive
- Single character matches give us the most flexibility in positioning
- The minimum difference will always be achieved by some single-character match
Pitfall 2: Using First Occurrence Instead of Last Occurrence
The Issue: Developers might intuitively store the first occurrence of each character in secondString
:
# Incorrect approach
first_occurrence = {}
for i, char in enumerate(secondString):
if char not in first_occurrence:
first_occurrence[char] = i
Why This is Wrong: To minimize j - a
(or i - a
for single characters), we need a
to be as large as possible. Using the first occurrence would give us the smallest a
, which maximizes rather than minimizes the difference.
Solution: Always use the last occurrence:
# Correct approach
last_occurrence = {char: index for index, char in enumerate(secondString)}
Pitfall 3: Initializing the Minimum Incorrectly
The Issue: Initializing min_difference
to 0 or a small positive number:
# Incorrect min_difference = 0 # or min_difference = 1
Why This is Wrong: The difference i - last[char]
can be negative when a character appears later in secondString
than in firstString
. If we initialize to 0, we might miss valid negative differences that are actually the minimum.
Solution: Initialize to positive infinity to ensure any valid difference will be smaller:
min_difference = inf
Pitfall 4: Not Handling Missing Characters
The Issue: Forgetting to check if a character from firstString
exists in secondString
:
# Incorrect - will raise KeyError
for i, char in enumerate(firstString):
difference = i - last_occurrence[char] # KeyError if char not in last_occurrence
Solution: Always check for existence before accessing:
for i, char in enumerate(firstString):
if char in last_occurrence:
difference = i - last_occurrence[char]
Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.
Which of the following method should we use to solve this problem?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
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!