5 Best Ways to Check if an Array Can Be Divided into Pairs Whose Sum Is Divisible by k in Python

πŸ’‘ Problem Formulation: We face a common coding challenge where we need to check if an array can be paired such that the sum of elements in each pair is divisible by a given integer k. Consider an array [1, 2, 3, 4, 5, 6] and k = 5. We want to determine if it’s possible to pair elements (e.g., (1, 4), (2, 3), (5, 6)) where the sum of each pair is divisible by 5. This article discusses five methods to solve this problem.

Method 1: Frequency Count Method with Hashing

The Frequency Count Method relies on creating a hash-map to count the frequency of remainders when dividing array elements by k. Pairing is possible if for every remainder i, there is an equal count of elements with remainder k-i (except when i is 0 or k/2, those must be even).

Here’s an example:

def canArrange(arr, k):
    remainder_counts = {i: 0 for i in range(k)}
    for num in arr:
        remainder_counts[num % k] += 1
    for i in range(1, k//2+1):
        if i != k - i and remainder_counts[i] != remainder_counts[k-i]:
            return False
    return remainder_counts[0] % 2 == 0 and (k % 2 != 0 or remainder_counts[k//2] % 2 == 0)

# Example array and k
arr = [1, 2, 3, 4, 5, 6]
k = 5
print(canArrange(arr, k))

Output:

True

In this code, we create a hash-map to store counts of each remainder. After counting, we verify if each count of a particular remainder matches the count of its complementary remainder to form a divisible pair. Special checks are done for the remainders 0 and k/2. The function returns True if all conditions are met, indicating the array can be paired accordingly.

Method 2: Sorting and Two-Pointer Technique

By sorting the array and using a two-pointer technique, we can check if we can create pairs with divisible sums. Pointers start from the extremes of the sorted array and move towards the center, checking for pair sum divisibility.

Here’s an example:

def canArrange(arr, k):
    arr = sorted([x % k for x in arr])
    left, right = 0, len(arr) - 1
    while left < right:
        if (arr[left] + arr[right]) % k != 0:
            return False
        left += 1
        right -= 1
    return True

arr = [1, 2, 3, 4, 5, 6]
k = 5
print(canArrange(arr, k))

Output:

True

This method first sorts the array of remainders and then runs a two-pointer iteration. If at any point the sum of the elements pointed by the two pointers is not divisible by k, it returns False. If it successfully goes through the array without such an issue, True is returned.

Method 3: Frequency Count Method with Optimized Space

Similar to Method 1, this method uses the frequency count approach but optimizes space by using a single list to hold the remainder counts, directly updating and checking frequencies while iterating through the array.

Here’s an example:

def canArrange(arr, k):
    remainder_counts = [0] * k
    for num in arr:
        remainder_counts[num % k] += 1
    if remainder_counts[0] % 2 != 0:
        return False
    for i in range(1, k//2+1):
        if remainder_counts[i] != remainder_counts[k - i]:
            return False
    return True

arr = [1, 2, 3, 4, 5, 6]
k = 5
print(canArrange(arr, k))

Output:

True

This optimized version requires less space as we’re not creating an extra hash-map but directly modifying the array of counts. The process of validation is the same as in Method 1, ensuring the count of each remainder is appropriate for pairing.

Method 4: Greedy Selection with Preprocessing

The greedy selection approach involves preprocessing the array to get remainders, then trying to greedily form pairs starting from the smallest non-zero remainder, pairing each with its complementary remainder.

Here’s an example:

def canArrange(arr, k):
    remainders = [num % k for num in arr if num % k != 0]
    while remainders:
        current = remainders.pop(0)
        complement = k - current if current else 0
        if complement in remainders:
            remainders.remove(complement)
        else:
            return False
    return True

arr = [1, 2, 3, 4, 5, 6]
k = 5
print(canArrange(arr, k))

Output:

True

This code first eliminates zero remainders and then tries to greedily find and remove complement pairs from the list. This is less efficient due to the list search operation but is conceptually simple. It returns True if all elements are paired up without leftovers.

Bonus One-Liner Method 5: Using Collections Counter

The Collections Counter method is a concise one-liner that leverages Python’s Counter class to count remainders and then verifies pairing using a generator expression within an all() function.

Here’s an example:

from collections import Counter

def canArrange(arr, k):
    remainder_counts = Counter([num % k for num in arr])
    return all((remainder_counts[i] == remainder_counts[-i % k]) for i in range(k))

arr = [1, 2, 3, 4, 5, 6]
k = 5
print(canArrange(arr, k))

Output:

True

This compact method creates a Counter object to count the occurrences of each remainder in the array. It then returns the result of checking that each remainder count matches the count of its complement, accounting also for the special case when the remainder is 0.

Summary/Discussion

  • Method 1: Frequency Count with Hashing. Strengths: Very efficient and uses hashing for fast lookup. Weaknesses: Complexity increases for those new to hashing.
  • Method 2: Sorting and Two-Pointer Technique. Strengths: Conceptually straightforward and does not require additional data structures. Weaknesses: Sorting the array adds extra time complexity.
  • Method 3: Frequency Count with Optimized Space. Strengths: Optimizes space by working in-place. Weaknesses: May be slightly less intuitive due to direct use of indices.
  • Method 4: Greedy Selection with Preprocessing. Strengths: Simple to understand. Weaknesses: Inefficient due to list operations.
  • Method 5: Using Collections Counter. Strengths: Very concise one-liner suitable for Pythonic coders. Weaknesses: May obscure the logic for those who prefer explicitness.