5 Best Ways to Find Max Number of K-Sum Pairs in Python

πŸ’‘ Problem Formulation: This article deals with the problem of identifying the maximum number of pairs in an array that add up to a given sum k. Given an array nums and an integer k, the goal is to find the maximum number of unique pairs (nums[i], nums[j]) where i != j and nums[i] + nums[j] == k. For example, given nums = [1, 2, 3, 4] and k = 5, the desired output is 2, corresponding to pairs (1, 4) and (2, 3).

Method 1: Brute Force Approach

This brute force approach involves checking all possible pairs in the array to count how many of them sum up to k. It is not the most efficient method, as it has a time complexity of O(n2), where n is the number of elements in the array.

Here’s an example:

def max_k_sum_pairs_brute(nums, k):
    count = 0
    pairs = set()
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] + nums[j] == k and (i, j) not in pairs and (j, i) not in pairs:
                pairs.add((i, j))
                count += 1
    return count

# Example usage:
print(max_k_sum_pairs_brute([1, 2, 3, 4], 5))

Output:

2

In this code snippet, the function max_k_sum_pairs_brute is defined to find the maximum number of k-sum pairs using the brute force approach. It iterates over each possible pair in the array to see if they add up to k and ensures pairs are not counted twice by using a set to store already found pairs.

Method 2: Sorting and Two-Pointers Technique

The sorting and two-pointers technique improves over the brute force by first sorting the array and then using two pointers to find pairs. The efficiency of this method is O(nlogn) because of the sorting process, but the actual pair-finding is O(n).

Here’s an example:

def max_k_sum_pairs_two_pointers(nums, k):
    nums.sort()
    count = 0
    left, right = 0, len(nums) - 1
    while left < right:
        current_sum = nums[left] + nums[right]
        if current_sum == k:
            count += 1
            left += 1
            right -= 1
        elif current_sum < k:
            left += 1
        else:
            right -= 1
    return count

# Example usage:
print(max_k_sum_pairs_two_pointers([1, 2, 3, 4], 5))

Output:

2

This function max_k_sum_pairs_two_pointers sorts the array and then uses a left and right pointer to efficiently find and count pairs that sum up to k. When a valid pair is found, both pointers are moved inward to continue searching for more pairs.

Method 3: Using a Hash Map

Using a hash map allows us to track the frequency of each number in the array, reducing the time complexity to O(n). This method is efficient for unsorted arrays and does not require altering the original array.

Here’s an example:

from collections import Counter

def max_k_sum_pairs_hashmap(nums, k):
    count = 0
    num_count = Counter(nums)

    for num in nums:
        complement = k - num
        if num_count[complement] > 0 and num_count[num] > 0:
            if num != complement or num_count[num] > 1:
                count += 1
                num_count[complement] -= 1
                num_count[num] -= 1

    return count // 2 # Pairs are counted twice

# Example usage:
print(max_k_sum_pairs_hashmap([1, 2, 3, 4], 5))

Output:

2

This code snippet uses a Counter from the collections module to create a frequency map for the array elements. The function max_k_sum_pairs_hashmap iterates through the array and checks the hash map for a complement that, when paired with the current element, sums up to k. Each found pair is counted once, hence the final count is divided by two.

Method 4: Greedy with Sorted Counter

This method involves sorting a counter (frequency dictionary) and then iteratively reducing the frequency of encountered pairs. It is similar to using a hash map but incorporates sorting, which can be advantageous in certain cases for reducing the number of required operations.

Here’s an example:

from collections import Counter

def max_k_sum_pairs_greedy(nums, k):
    count = 0
    num_count = Counter(nums)

    for num in sorted(num_count):
        complement = k - num
        pair_count = min(num_count[num], num_count[complement])
        if num == complement:
            pair_count //= 2
        count += pair_count
        num_count[num] -= pair_count
        num_count[complement] -= pair_count

    return count

# Example usage:
print(max_k_sum_pairs_greedy([1, 2, 3, 4], 5))

Output:

2

The function max_k_sum_pairs_greedy sorts the keys of the frequency map and iteratively looks for complements. If it finds a valid complement, it greedily takes as many pairs as possible, taking into account the specific case where the number and its complement are the same.

Bonus One-Liner Method 5: Pythonic Approach with Counter

This one-liner approach exploits Python’s comprehension syntax and Counter to achieve the same result in a more concise manner, though it sacrifices a bit of readability.

Here’s an example:

from collections import Counter

nums = [1, 2, 3, 4]
k = 5
print(sum(min(Counter(nums)[i], Counter(nums)[k - i]) // (2 if i == k-i else 1) for i in set(nums)))

Output:

2

This one-liner uses a generator expression to iterate over unique elements in the array, counts how many times a number and its complement can form a pair, and sums up these counts. It includes conditionals to handle the case where a number can pair with itself.

Summary/Discussion

  • Method 1: Brute Force Approach. Simple but inefficient for large arrays. The strength lies in its simplicity and the fact that it doesn’t require any additional data structures. Its weakness is the O(n2 ) time complexity.
  • Method 2: Sorting and Two-Pointers Technique. More efficient than brute force, especially on sorted arrays. Strengths include better performance for larger data sets and being more intuitive after sorting. Weakness is that the initial sorting incurs O(nlogn) time complexity.
  • Method 3: Using a Hash Map. Efficient and fast for large datasets, with a time complexity of O(n). The strength of this method is its efficiency and direct approach. However, it requires O(n) additional space for the hash map.
  • Method 4: Greedy with Sorted Counter. Similar to Method 3 but may have a better practical performance in specific scenarios. Its strengths mirror those of Method 3 but with potentially fewer operations. Weakness includes additional complexity when dealing with the counter.
  • Method 5: Pythonic Approach with Counter. Extremely concise and leverages Python features. Its strength is the concise and expressive code. The weakness is the lack of readability and potential performance hit due to the repeated creation of Counters.