Facebook Pixel

3267. Count Almost Equal Pairs II

HardArrayHash TableCountingEnumerationSorting
Leetcode Link

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 or y 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 in 001 which equals 1)
  • 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 and 321 are almost equal (swap digits in position 0 and 2 of either number)
  • Numbers 100 and 1 are almost equal (swap any two zeros in 100 to get 001 = 1)
  • Numbers that differ by more than what two swaps can fix are not almost equal

The solution involves:

  1. Sorting the array first to handle cases where smaller numbers (like 1) need to be compared with larger numbers (like 100)
  2. For each number, generating all possible numbers that can be obtained by performing at most 2 digit swaps
  3. Using a hash table to count occurrences and find matching pairs
  4. The sorting ensures that when processing a number, all potentially matching smaller numbers have already been processed and counted
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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) where p and q 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:

  1. Generate all its possible variations
  2. Check how many of these variations we've seen before (sum up their counts)
  3. 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:

  1. 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.

  2. Initialize data structures:

    • ans: Counter for the number of valid pairs
    • cnt: A defaultdict(int) that tracks how many times we've seen each number configuration
  3. 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 that x can become after at most 2 swaps.

    b. Convert to string for digit manipulation: Convert x to a list of characters s = list(str(x)) with length m.

    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).

  4. 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.

  5. Update the counter: Add the current number to cnt for future matching:

    cnt[x] += 1
  6. Return the result: The total count ans represents all valid pairs (i, j) where i < j and nums[i] and nums[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 Evaluator

Example 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) where M is the maximum value (number of digits)
    • The nested loops iterate through all pairs of digit positions: O(m^2) where m = 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 most O(log^4 M) elements (all possible swaps)
    • Summing counts from dictionary: O(|vis|) = O(log^4 M)
  • 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 most n 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 seen 1 yet, so we miss the pair
  • The number 100 can become 001 (which equals 1) 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More