2068. Check Whether Two Strings are Almost Equivalent
Problem Description
You are given two strings word1
and word2
, both of the same length n
. Your task is to determine if these two strings are "almost equivalent."
Two strings are considered almost equivalent if, for every letter from 'a' to 'z', the absolute difference between how many times that letter appears in word1
versus word2
is at most 3.
For example:
- If the letter 'a' appears 5 times in
word1
and 2 times inword2
, the difference is |5 - 2| = 3, which is acceptable. - If the letter 'b' appears 7 times in
word1
and 3 times inword2
, the difference is |7 - 3| = 4, which exceeds 3, so the strings would not be almost equivalent.
The function should return true
if the strings are almost equivalent, and false
otherwise.
The solution uses a Counter to track the frequency differences. It first counts all characters in word1
, then subtracts the counts for characters in word2
. This gives us the difference in frequencies for each character. Finally, it checks if all absolute differences are at most 3.
Intuition
The key insight is that we need to track the frequency difference for each letter between the two strings. Instead of counting frequencies separately for both strings and then comparing them, we can use a single counter to directly compute the differences.
Think of it like a balance sheet: we add counts for characters from word1
(positive contributions) and subtract counts for characters from word2
(negative contributions). After processing both strings, each value in our counter represents the net difference in frequency for that character.
For instance, if 'a' appears 5 times in word1
and 2 times in word2
, our counter would show cnt['a'] = 5 - 2 = 3
. This direct difference calculation is more efficient than maintaining two separate frequency maps.
The beauty of this approach is that after processing both strings, we simply need to check if any character has an absolute frequency difference greater than 3. If all differences are within the range [-3, 3]
, the strings are almost equivalent.
Using Python's Counter
class makes this particularly elegant - we can initialize it with word1
, then decrement counts for each character in word2
using subtraction. The final check all(abs(x) <= 3 for x in cnt.values())
ensures every frequency difference satisfies our constraint.
Solution Approach
The solution uses a counting approach with a single pass through both strings to determine if they are almost equivalent.
Step 1: Initialize the Counter
We create a Counter
object initialized with word1
. This automatically counts the frequency of each character in the first string. For example, if word1 = "aaab"
, the counter would be {'a': 3, 'b': 1}
.
Step 2: Process the Second String
We iterate through each character c
in word2
and decrement its count in our counter:
for c in word2: cnt[c] -= 1
This subtraction effectively computes the frequency difference. If a character appears in word2
but not in word1
, the counter will have a negative value for that character.
Step 3: Check the Differences
After processing both strings, each value in cnt
represents the frequency difference for that character between word1
and word2
. We use:
all(abs(x) <= 3 for x in cnt.values())
This checks if every frequency difference has an absolute value of at most 3.
Example Walkthrough:
word1 = "aaaa"
,word2 = "bbb"
- After initializing:
cnt = {'a': 4}
- After processing
word2
:cnt = {'a': 4, 'b': -3}
- Check:
|4| = 4 > 3
, so returnfalse
The time complexity is O(n)
where n
is the length of the strings, and space complexity is O(1)
since we only store at most 26 letters in the counter.
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 word1 = "abcde"
and word2 = "aabcd"
.
Step 1: Initialize Counter with word1
- Process each character in
word1 = "abcde"
- Counter becomes:
{'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1}
Step 2: Subtract counts for word2
- Process each character in
word2 = "aabcd"
:- 'a':
cnt['a'] = 1 - 1 = 0
- 'a':
cnt['a'] = 0 - 1 = -1
- 'b':
cnt['b'] = 1 - 1 = 0
- 'c':
cnt['c'] = 1 - 1 = 0
- 'd':
cnt['d'] = 1 - 1 = 0
- 'a':
- Counter becomes:
{'a': -1, 'b': 0, 'c': 0, 'd': 0, 'e': 1}
Step 3: Check all differences
- Check each value in the counter:
- |−1| = 1 ≤ 3 ✓
- |0| = 0 ≤ 3 ✓
- |0| = 0 ≤ 3 ✓
- |0| = 0 ≤ 3 ✓
- |1| = 1 ≤ 3 ✓
- All differences are at most 3, so return
true
The key insight: the counter directly tracks the net difference for each character. Positive values mean the character appears more in word1
, negative values mean it appears more in word2
, and the absolute value gives us the actual difference we need to check.
Solution Implementation
1from collections import Counter
2
3class Solution:
4 def checkAlmostEquivalent(self, word1: str, word2: str) -> bool:
5 # Count frequency of each character in word1
6 char_count = Counter(word1)
7
8 # Subtract frequency of each character in word2
9 # This gives us the difference in frequencies between word1 and word2
10 for char in word2:
11 char_count[char] -= 1
12
13 # Check if all frequency differences are at most 3 in absolute value
14 # Two strings are almost equivalent if for every letter,
15 # the absolute difference in frequencies is at most 3
16 return all(abs(freq_diff) <= 3 for freq_diff in char_count.values())
17
1class Solution {
2 /**
3 * Checks if two strings are almost equivalent.
4 * Two strings are almost equivalent if the frequency difference of each character
5 * between the two strings is at most 3.
6 *
7 * @param word1 the first string to compare
8 * @param word2 the second string to compare
9 * @return true if the strings are almost equivalent, false otherwise
10 */
11 public boolean checkAlmostEquivalent(String word1, String word2) {
12 // Array to store the frequency difference for each lowercase letter (a-z)
13 int[] frequencyDifference = new int[26];
14
15 // Increment count for each character in word1
16 for (int i = 0; i < word1.length(); i++) {
17 char currentChar = word1.charAt(i);
18 int charIndex = currentChar - 'a';
19 frequencyDifference[charIndex]++;
20 }
21
22 // Decrement count for each character in word2
23 for (int i = 0; i < word2.length(); i++) {
24 char currentChar = word2.charAt(i);
25 int charIndex = currentChar - 'a';
26 frequencyDifference[charIndex]--;
27 }
28
29 // Check if any character has a frequency difference greater than 3
30 for (int difference : frequencyDifference) {
31 if (Math.abs(difference) > 3) {
32 return false;
33 }
34 }
35
36 // All characters have frequency difference at most 3
37 return true;
38 }
39}
40
1class Solution {
2public:
3 bool checkAlmostEquivalent(string word1, string word2) {
4 // Initialize frequency array for 26 lowercase letters
5 int frequency[26] = {0};
6
7 // Increment count for each character in word1
8 for (char& ch : word1) {
9 frequency[ch - 'a']++;
10 }
11
12 // Decrement count for each character in word2
13 for (char& ch : word2) {
14 frequency[ch - 'a']--;
15 }
16
17 // Check if the absolute difference for any character exceeds 3
18 for (int i = 0; i < 26; i++) {
19 if (abs(frequency[i]) > 3) {
20 return false; // Words are not almost equivalent
21 }
22 }
23
24 return true; // Words are almost equivalent
25 }
26};
27
1/**
2 * Checks if two words are almost equivalent.
3 * Two words are almost equivalent if the frequency difference of each character
4 * between the two words is at most 3.
5 *
6 * @param word1 - The first word to compare
7 * @param word2 - The second word to compare
8 * @returns true if the words are almost equivalent, false otherwise
9 */
10function checkAlmostEquivalent(word1: string, word2: string): boolean {
11 // Initialize frequency counter array for 26 lowercase letters
12 // Positive values indicate excess in word1, negative values indicate excess in word2
13 const frequencyDifference: number[] = new Array(26).fill(0);
14
15 // Count character frequencies from word1 (increment counter)
16 for (const character of word1) {
17 // Convert character to index (0-25) by subtracting ASCII value of 'a' (97)
18 const index: number = character.charCodeAt(0) - 97;
19 frequencyDifference[index]++;
20 }
21
22 // Subtract character frequencies from word2 (decrement counter)
23 for (const character of word2) {
24 // Convert character to index (0-25) by subtracting ASCII value of 'a' (97)
25 const index: number = character.charCodeAt(0) - 97;
26 frequencyDifference[index]--;
27 }
28
29 // Check if all frequency differences are within the allowed range (-3 to 3)
30 return frequencyDifference.every((difference: number) => Math.abs(difference) <= 3);
31}
32
Time and Space Complexity
The time complexity is O(n + m)
where n
is the length of word1
and m
is the length of word2
. The Counter(word1)
operation takes O(n)
time to count all characters in word1
. The loop iterating through word2
takes O(m)
time. Finally, checking all values in the counter takes O(C)
time where C
is the size of the character set (at most 26 for lowercase English letters). Since C
is constant and typically n
and m
are of similar magnitude, this can be simplified to O(n)
when considering both strings have comparable lengths.
The space complexity is O(C)
where C
is the size of the character set. The Counter
object stores at most C = 26
key-value pairs (one for each lowercase English letter that appears in either string). This is constant space with respect to the input size, as the number of distinct characters is bounded by the alphabet size regardless of the string lengths.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Forgetting to Handle Characters Present in Only One String
A common mistake is assuming that both strings contain the same set of characters. The Counter approach handles this elegantly, but if implementing manually with a dictionary, you might forget to account for characters that appear in word2
but not in word1
.
Incorrect approach:
def checkAlmostEquivalent(self, word1: str, word2: str) -> bool:
freq1 = {}
for c in word1:
freq1[c] = freq1.get(c, 0) + 1
# Wrong: This only checks characters that exist in word1
for c in freq1:
count2 = word2.count(c)
if abs(freq1[c] - count2) > 3:
return False
return True
This fails when word2
has characters not in word1
. For example, with word1 = "aaa"
and word2 = "bbbb"
, it would incorrectly return True
because it never checks the frequency of 'b'.
Solution: Always check all 26 lowercase letters or use the Counter subtraction approach which automatically handles missing keys.
Pitfall 2: Using Two Separate Counters Without Proper Comparison
Another mistake is creating two separate counters and not properly comparing all characters from both.
Incorrect approach:
def checkAlmostEquivalent(self, word1: str, word2: str) -> bool:
cnt1 = Counter(word1)
cnt2 = Counter(word2)
# Wrong: Only checks keys in cnt1
for char in cnt1:
if abs(cnt1[char] - cnt2.get(char, 0)) > 3:
return False
return True
Solution: Ensure you check all unique characters from both strings:
def checkAlmostEquivalent(self, word1: str, word2: str) -> bool:
cnt1 = Counter(word1)
cnt2 = Counter(word2)
# Check all characters from both counters
all_chars = set(cnt1.keys()) | set(cnt2.keys())
for char in all_chars:
if abs(cnt1.get(char, 0) - cnt2.get(char, 0)) > 3:
return False
return True
The original solution avoids this pitfall by using Counter subtraction, which automatically handles all characters from both strings.
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
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!