3267. Count Almost Equal Pairs II
Problem Description
You are given an array nums
consisting of positive integers.
Two integers x
and y
are called almost equal if they can become equal after performing the following operation at most twice:
- Choose either
x
ory
and swap any two digits within the chosen number.
Your task is to return the number of index pairs (i, j)
where i < j
such that nums[i]
and nums[j]
are almost equal.
Important Notes:
- Leading zeros are allowed after performing a swap operation (e.g., swapping digits in
100
could result in001
which equals1
) - The operation can be performed 0, 1, or 2 times total across both numbers
- Each operation involves swapping exactly two digits within one chosen number
Example scenarios:
- Numbers
123
and321
are almost equal (swap digits in position 0 and 2 of either number) - Numbers
100
and1
are almost equal (swap any two zeros in100
to get001
=1
) - Numbers that differ by more than what two swaps can fix are not almost equal
The solution involves:
- Sorting the array first to handle cases where smaller numbers (like
1
) need to be compared with larger numbers (like100
) - For each number, generating all possible numbers that can be obtained by performing at most 2 digit swaps
- Using a hash table to count occurrences and find matching pairs
- The sorting ensures that when processing a number, all potentially matching smaller numbers have already been processed and counted
Intuition
The key insight is that we need to find all pairs of numbers that can be made equal through at most 2 digit swaps. The naive approach would be to compare every pair of numbers and check if they can be made equal, but this would be inefficient.
Instead, we can think of it differently: for each number, what are all the possible numbers it could become after 0, 1, or 2 swaps? If we can generate this set of possibilities, then we can check if any previously seen numbers match these possibilities.
Why do we need sorting? Consider the pair (100, 1)
. When we swap digits in 100
, we can get 001
which equals 1
. However, if we process 1
first, we won't find 100
in our generated possibilities from 1
(since 1
has only one digit). But if we process 100
first, we can generate 1
from it. Sorting ensures larger numbers are processed after smaller ones, allowing us to catch all such pairs.
The generation strategy works as follows:
- First, include the original number (0 swaps)
- Then, try all possible single swaps: for each pair of positions
(i, j)
, swap them and add the result - For two swaps, we need to be careful: after making the first swap at positions
(i, j)
, we make a second swap at different positions(p, q)
wherep
andq
are both different from the currently swapped positions
By maintaining a counter cnt
that tracks how many times we've seen each number configuration, we can efficiently count pairs. When processing a number x
, we:
- Generate all its possible variations
- Check how many of these variations we've seen before (sum up their counts)
- Add
x
itself to our counter for future numbers to match against
This approach ensures we count each valid pair exactly once since we process numbers in sorted order and only look backward (at previously processed numbers).
Learn more about Sorting patterns.
Solution Approach
The implementation follows these steps:
-
Sort the array: First, we sort
nums
to ensure we process smaller numbers before larger ones. This handles edge cases like(100, 1)
where the larger number can transform into the smaller one. -
Initialize data structures:
ans
: Counter for the number of valid pairscnt
: Adefaultdict(int)
that tracks how many times we've seen each number configuration
-
Process each number: For each number
x
in the sorted array:a. Create a set of all possible variations: Initialize
vis = {x}
to store all numbers thatx
can become after at most 2 swaps.b. Convert to string for digit manipulation: Convert
x
to a list of characterss = list(str(x))
with lengthm
.c. Generate all single-swap variations:
for j in range(m): for i in range(j): s[i], s[j] = s[j], s[i] # Swap digits at positions i and j vis.add(int("".join(s))) # Add the new number to vis # ... (two-swap logic here) s[i], s[j] = s[j], s[i] # Swap back to restore original
d. Generate all two-swap variations: After making the first swap
(i, j)
, we make a second swap:for q in range(i + 1, m): for p in range(i + 1, q): s[p], s[q] = s[q], s[p] # Second swap vis.add(int("".join(s))) s[p], s[q] = s[q], s[p] # Undo second swap
The condition
p, q > i
ensures we don't interfere with the first swap at positions(i, j)
. -
Count matching pairs: After generating all variations in
vis
, we count how many times these variations have been seen before:ans += sum(cnt[x] for x in vis)
This adds the count of all previously processed numbers that match any variation of the current number.
-
Update the counter: Add the current number to
cnt
for future matching:cnt[x] += 1
-
Return the result: The total count
ans
represents all valid pairs(i, j)
wherei < j
andnums[i]
andnums[j]
are almost equal.
The time complexity is O(n * d^4)
where n
is the array length and d
is the maximum number of digits, since we have four nested loops for generating variations. The space complexity is O(n * d^4)
for storing all possible variations 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 algorithm with nums = [100, 1, 100, 12, 21]
.
Step 1: Sort the array
After sorting: nums = [1, 12, 21, 100, 100]
Step 2: Process each number
Processing nums[0] = 1:
- Generate variations:
vis = {1}
(single digit, no swaps possible) - Check counter:
sum(cnt[x] for x in {1}) = 0
(no matches yet) - Update:
ans = 0
,cnt[1] = 1
Processing nums[1] = 12:
- Original:
vis = {12}
- Single swap (positions 0,1): swap '1' and '2' β get 21
- Variations:
vis = {12, 21}
- Check counter:
cnt[12] = 0, cnt[21] = 0
, sum = 0 - Update:
ans = 0
,cnt[12] = 1
Processing nums[2] = 21:
- Original:
vis = {21}
- Single swap (positions 0,1): swap '2' and '1' β get 12
- Variations:
vis = {21, 12}
- Check counter:
cnt[21] = 0, cnt[12] = 1
, sum = 1 (found match with previous 12!) - Update:
ans = 1
,cnt[21] = 1
Processing nums[3] = 100:
- Original:
vis = {100}
- Single swaps:
- Swap positions (0,1): '1' and '0' β 010 = 10
- Swap positions (0,2): '1' and '0' β 001 = 1
- Swap positions (1,2): '0' and '0' β 100 (no change)
- Variations:
vis = {100, 10, 1}
- Check counter:
cnt[100] = 0, cnt[10] = 0, cnt[1] = 1
, sum = 1 (found match with 1!) - Update:
ans = 2
,cnt[100] = 1
Processing nums[4] = 100:
- Original:
vis = {100}
- Single swaps: (same as above)
- Variations:
vis = {100, 10, 1}
- Check counter:
cnt[100] = 1, cnt[10] = 0, cnt[1] = 1
, sum = 2 (matches with previous 100 and 1!) - Update:
ans = 4
,cnt[100] = 2
Final Result: ans = 4
The pairs are:
- (1, 2): nums[1]=12 and nums[2]=21
- (0, 3): nums[0]=1 and nums[3]=100
- (0, 4): nums[0]=1 and nums[4]=100
- (3, 4): nums[3]=100 and nums[4]=100
Solution Implementation
1from typing import List
2from collections import defaultdict
3
4class Solution:
5 def countPairs(self, nums: List[int]) -> int:
6 # Sort the array to process numbers in order
7 nums.sort()
8
9 # Initialize the answer counter
10 pair_count = 0
11
12 # Dictionary to store frequency of each number processed so far
13 frequency_map = defaultdict(int)
14
15 # Process each number in the sorted array
16 for current_num in nums:
17 # Set to store all possible numbers reachable from current_num
18 # through at most 2 swaps
19 reachable_numbers = {current_num}
20
21 # Convert number to list of digit characters for swapping
22 digits = list(str(current_num))
23 num_digits = len(digits)
24
25 # Generate all possible numbers with at most 2 swaps
26 # First loop: try all single swaps
27 for second_pos in range(num_digits):
28 for first_pos in range(second_pos):
29 # Perform first swap
30 digits[first_pos], digits[second_pos] = digits[second_pos], digits[first_pos]
31 reachable_numbers.add(int("".join(digits)))
32
33 # Try second swap on top of the first swap
34 # Consider positions after first_pos to avoid duplicates
35 for fourth_pos in range(first_pos + 1, num_digits):
36 for third_pos in range(first_pos + 1, fourth_pos):
37 # Perform second swap
38 digits[third_pos], digits[fourth_pos] = digits[fourth_pos], digits[third_pos]
39 reachable_numbers.add(int("".join(digits)))
40 # Undo second swap
41 digits[third_pos], digits[fourth_pos] = digits[fourth_pos], digits[third_pos]
42
43 # Undo first swap
44 digits[first_pos], digits[second_pos] = digits[second_pos], digits[first_pos]
45
46 # Count pairs: for each reachable number, add how many times
47 # we've seen it before in the array
48 pair_count += sum(frequency_map[reachable_num] for reachable_num in reachable_numbers)
49
50 # Update frequency map with current number
51 frequency_map[current_num] += 1
52
53 return pair_count
54
1class Solution {
2 /**
3 * Counts the number of pairs (i, j) where i < j and nums[i] and nums[j] are almost equal.
4 * Two numbers are almost equal if they can be made equal by at most one swap operation
5 * on the digits of one number.
6 *
7 * @param nums the input array of integers
8 * @return the count of almost equal pairs
9 */
10 public int countPairs(int[] nums) {
11 // Sort the array to ensure we process elements in order
12 Arrays.sort(nums);
13
14 int pairCount = 0;
15 // Map to store frequency of each number processed so far
16 Map<Integer, Integer> frequencyMap = new HashMap<>();
17
18 // Process each number in the sorted array
19 for (int currentNum : nums) {
20 // Set to store all possible numbers that can be formed from currentNum
21 // by at most one swap operation
22 Set<Integer> possibleNumbers = new HashSet<>();
23 possibleNumbers.add(currentNum); // Original number (no swap)
24
25 // Convert number to character array for easy digit manipulation
26 char[] digits = String.valueOf(currentNum).toCharArray();
27
28 // Generate all possible single swap variations
29 for (int j = 0; j < digits.length; ++j) {
30 for (int i = 0; i < j; ++i) {
31 // Perform first swap between positions i and j
32 swap(digits, i, j);
33 possibleNumbers.add(Integer.parseInt(String.valueOf(digits)));
34
35 // Try all possible second swaps starting from position i
36 // This generates numbers with exactly one swap
37 for (int q = i; q < digits.length; ++q) {
38 for (int p = i; p < q; ++p) {
39 // Perform second swap between positions p and q
40 swap(digits, p, q);
41 possibleNumbers.add(Integer.parseInt(String.valueOf(digits)));
42 // Restore second swap
43 swap(digits, p, q);
44 }
45 }
46
47 // Restore first swap to original state
48 swap(digits, i, j);
49 }
50 }
51
52 // Count pairs by checking how many times each possible number
53 // has appeared in previously processed elements
54 for (int possibleNum : possibleNumbers) {
55 pairCount += frequencyMap.getOrDefault(possibleNum, 0);
56 }
57
58 // Add current number to frequency map for future comparisons
59 frequencyMap.merge(currentNum, 1, Integer::sum);
60 }
61
62 return pairCount;
63 }
64
65 /**
66 * Helper method to swap two characters in a character array
67 *
68 * @param charArray the character array
69 * @param i first index to swap
70 * @param j second index to swap
71 */
72 private void swap(char[] charArray, int i, int j) {
73 char temp = charArray[i];
74 charArray[i] = charArray[j];
75 charArray[j] = temp;
76 }
77}
78
1class Solution {
2public:
3 int countPairs(vector<int>& nums) {
4 // Sort the array for processing
5 sort(nums.begin(), nums.end());
6
7 int pairCount = 0;
8 // Map to store frequency of each number processed so far
9 unordered_map<int, int> frequencyMap;
10
11 for (int currentNum : nums) {
12 // Set to store all possible values after 0, 1, or 2 swaps
13 unordered_set<int> possibleValues = {currentNum};
14 string numStr = to_string(currentNum);
15 int strLength = numStr.length();
16
17 // Generate all possible values with at most 2 swaps
18 // First swap: positions i and j
19 for (int j = 0; j < strLength; ++j) {
20 for (int i = 0; i < j; ++i) {
21 // Perform first swap
22 swap(numStr[i], numStr[j]);
23 possibleValues.insert(stoi(numStr));
24
25 // Second swap: positions p and q (after first swap)
26 // Note: p and q must be after position i to avoid duplicate swaps
27 for (int q = i + 1; q < strLength; ++q) {
28 for (int p = i + 1; p < q; ++p) {
29 // Perform second swap
30 swap(numStr[p], numStr[q]);
31 possibleValues.insert(stoi(numStr));
32 // Undo second swap
33 swap(numStr[p], numStr[q]);
34 }
35 }
36
37 // Undo first swap
38 swap(numStr[i], numStr[j]);
39 }
40 }
41
42 // Count pairs: for each possible value, add its frequency
43 // from previously processed numbers
44 for (int possibleValue : possibleValues) {
45 pairCount += frequencyMap[possibleValue];
46 }
47
48 // Add current number to frequency map for future pairs
49 frequencyMap[currentNum]++;
50 }
51
52 return pairCount;
53 }
54};
55
1/**
2 * Counts pairs of numbers where one can be transformed into another by swapping at most 2 pairs of digits
3 * @param nums - Array of numbers to process
4 * @returns Number of valid pairs
5 */
6function countPairs(nums: number[]): number {
7 // Sort the array in ascending order
8 nums.sort((a, b) => a - b);
9
10 let totalPairs = 0;
11 // Map to store frequency of each number seen so far
12 const frequencyMap = new Map<number, number>();
13
14 for (const currentNumber of nums) {
15 // Set to store all possible variations of current number after swaps
16 const possibleVariations = new Set<number>();
17 possibleVariations.add(currentNumber);
18
19 // Convert number to array of digit characters
20 const digits = currentNumber.toString().split('');
21
22 // Generate all variations with one swap (swap positions i and j)
23 for (let j = 0; j < digits.length; j++) {
24 for (let i = 0; i < j; i++) {
25 // Perform first swap between positions i and j
26 [digits[i], digits[j]] = [digits[j], digits[i]];
27 possibleVariations.add(Number(digits.join('')));
28
29 // Generate all variations with two swaps (additional swap between p and q)
30 for (let q = i + 1; q < digits.length; q++) {
31 for (let p = i + 1; p < q; p++) {
32 // Perform second swap between positions p and q
33 [digits[p], digits[q]] = [digits[q], digits[p]];
34 possibleVariations.add(Number(digits.join('')));
35 // Revert second swap
36 [digits[p], digits[q]] = [digits[q], digits[p]];
37 }
38 }
39
40 // Revert first swap
41 [digits[i], digits[j]] = [digits[j], digits[i]];
42 }
43 }
44
45 // Count pairs with all previously seen numbers that match any variation
46 for (const variation of possibleVariations) {
47 totalPairs += frequencyMap.get(variation) || 0;
48 }
49
50 // Update frequency of current number
51 frequencyMap.set(currentNumber, (frequencyMap.get(currentNumber) || 0) + 1);
52 }
53
54 return totalPairs;
55}
56
Time and Space Complexity
Time Complexity: O(n Γ (log n + log^5 M))
Breaking down the analysis:
- Sorting takes
O(n log n)
- For each of the
n
elements:- Converting number to string takes
O(log M)
whereM
is the maximum value (number of digits) - The nested loops iterate through all pairs of digit positions:
O(m^2)
wherem = O(log M)
- Inner nested loops for second swap:
O(m^2)
- Total swapping operations:
O(m^4) = O(log^4 M)
- Each swap operation involves string manipulation and integer conversion:
O(log M)
- Therefore, generating all variations:
O(log^4 M Γ log M) = O(log^5 M)
- The
vis
set can contain at mostO(log^4 M)
elements (all possible swaps) - Summing counts from dictionary:
O(|vis|) = O(log^4 M)
- Converting number to string takes
- Overall:
O(n log n) + O(n Γ log^5 M) = O(n Γ (log n + log^5 M))
Space Complexity: O(n + log^4 M)
Breaking down the analysis:
- The
cnt
dictionary stores at mostn
unique values:O(n)
- The
vis
set for each number stores all possible variations from swapping operations - Maximum size of
vis
:O(log^4 M)
(all possible combinations of at most 2 swaps) - String
s
for digit manipulation:O(log M)
- Overall:
O(n + log^4 M)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Loop Boundaries for Second Swap
The Pitfall: A common mistake is using incorrect loop boundaries when generating the second swap combinations. Many developers might write:
# INCORRECT - allows interference with first swap
for fourth_pos in range(num_digits):
for third_pos in range(fourth_pos):
This allows the second swap to potentially involve positions already used in the first swap (positions first_pos
and second_pos
), which would either:
- Undo the first swap if we swap the same positions again
- Create an invalid state where we're effectively doing 3 position changes instead of 4 distinct positions
The Solution: Ensure the second swap only considers positions that come after the first position involved in the initial swap:
# CORRECT - ensures no interference
for fourth_pos in range(first_pos + 1, num_digits):
for third_pos in range(first_pos + 1, fourth_pos):
2. Forgetting to Process Numbers in Sorted Order
The Pitfall: Processing the array without sorting can miss valid pairs where a larger number can transform into a smaller one through swaps. For example:
- Array:
[100, 1]
- Without sorting: When processing
100
first, we haven't seen1
yet, so we miss the pair - The number
100
can become001
(which equals1
) by swapping zeros
The Solution: Always sort the array first to ensure smaller numbers are processed before larger ones:
nums.sort() # Critical for correctness
3. Not Handling Leading Zeros Correctly
The Pitfall: Developers might think that numbers with leading zeros are invalid or try to handle them specially. For instance, they might avoid creating numbers like 001
from 100
.
The Solution: Python's int()
function automatically handles leading zeros correctly:
int("001") # Returns 1, not an error
int("00123") # Returns 123
No special handling is needed - just convert the string directly to an integer.
4. Double Counting or Missing the Original Number
The Pitfall: Forgetting to include the original number itself in the set of reachable numbers (when 0 swaps are performed):
# INCORRECT - missing the original number
reachable_numbers = set() # Should include current_num
The Solution: Initialize the reachable set with the original number:
reachable_numbers = {current_num} # Includes 0-swap case
5. Inefficient String Conversions
The Pitfall: Converting between string and integer repeatedly inside the innermost loops:
# INEFFICIENT - converts to string multiple times
for j in range(m):
for i in range(j):
s = list(str(x)) # Converting inside the loop
s[i], s[j] = s[j], s[i]
vis.add(int("".join(s)))
The Solution: Convert once outside the loops and maintain the character list:
digits = list(str(current_num)) # Convert once
for second_pos in range(num_digits):
for first_pos in range(second_pos):
# Work with the same list, swap and swap back
digits[first_pos], digits[second_pos] = digits[second_pos], digits[first_pos]
# ... operations ...
digits[first_pos], digits[second_pos] = digits[second_pos], digits[first_pos] # Restore
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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!