230 lines
9.5 KiB
YAML
230 lines
9.5 KiB
YAML
title: Check If Array Pairs Are Divisible by k
|
|
slug: check-if-array-pairs-are-divisible-by-k
|
|
difficulty: medium
|
|
leetcode_id: 1497
|
|
leetcode_url: https://leetcode.com/problems/check-if-array-pairs-are-divisible-by-k/
|
|
categories:
|
|
- arrays
|
|
- hash-tables
|
|
- math
|
|
patterns:
|
|
- prefix-sum
|
|
|
|
description: |
|
|
Given an array of integers `arr` of even length `n` and an integer `k`.
|
|
|
|
We want to divide the array into exactly `n / 2` pairs such that the sum of each pair is divisible by `k`.
|
|
|
|
Return `true` *if you can find a way to do that or* `false` *otherwise*.
|
|
|
|
constraints: |
|
|
- `arr.length == n`
|
|
- `1 <= n <= 10^5`
|
|
- `n` is even
|
|
- `-10^9 <= arr[i] <= 10^9`
|
|
- `1 <= k <= 10^5`
|
|
|
|
examples:
|
|
- input: "arr = [1,2,3,4,5,10,6,7,8,9], k = 5"
|
|
output: "true"
|
|
explanation: "Pairs are (1,9), (2,8), (3,7), (4,6) and (5,10). Each pair sums to a multiple of 5."
|
|
- input: "arr = [1,2,3,4,5,6], k = 7"
|
|
output: "true"
|
|
explanation: "Pairs are (1,6), (2,5) and (3,4). Each pair sums to 7, which is divisible by 7."
|
|
- input: "arr = [1,2,3,4,5,6], k = 10"
|
|
output: "false"
|
|
explanation: "No way to divide arr into 3 pairs where each sum is divisible by 10."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Think about what it means for two numbers to sum to a multiple of `k`. If `a + b` is divisible by `k`, then `(a % k) + (b % k)` must equal either `0` or `k`.
|
|
|
|
Imagine you have buckets numbered `0` to `k-1`, where each bucket holds numbers with that remainder when divided by `k`. For two numbers to pair together, their remainders must "complete" each other to reach `k` (or both be zero).
|
|
|
|
For example, with `k = 5`:
|
|
- A number with remainder `1` must pair with a number with remainder `4` (since `1 + 4 = 5`)
|
|
- A number with remainder `2` must pair with a number with remainder `3`
|
|
- A number with remainder `0` must pair with another number with remainder `0`
|
|
|
|
This insight transforms the problem: instead of trying to find actual pairs, we just need to **count remainders** and check if complementary buckets have equal counts.
|
|
|
|
approach: |
|
|
We solve this using a **Remainder Counting Approach**:
|
|
|
|
**Step 1: Build a remainder frequency map**
|
|
|
|
- Create a hash map (or array of size `k`) to count how many numbers have each remainder
|
|
- For each number in `arr`, compute `remainder = num % k`
|
|
- **Handle negative numbers**: In Python, `(-3) % 5 = 2`, but in some languages it returns `-3`. To ensure a positive remainder, use `((num % k) + k) % k`
|
|
- Increment the count for that remainder
|
|
|
|
|
|
|
|
**Step 2: Check remainder 0 separately**
|
|
|
|
- Numbers with remainder `0` can only pair with other numbers that have remainder `0`
|
|
- Their count must be **even** (so they can all pair up)
|
|
- If `count[0]` is odd, return `false`
|
|
|
|
|
|
|
|
**Step 3: Check complementary pairs**
|
|
|
|
- For each remainder `r` from `1` to `k/2`, check that `count[r] == count[k - r]`
|
|
- These are complementary remainders that must pair together
|
|
- If any pair has unequal counts, return `false`
|
|
|
|
|
|
|
|
**Step 4: Handle the middle remainder (when k is even)**
|
|
|
|
- If `k` is even, remainder `k/2` pairs with itself (since `k/2 + k/2 = k`)
|
|
- Similar to remainder `0`, the count of `k/2` must be even
|
|
- This is automatically handled in step 3 when `r == k - r`
|
|
|
|
|
|
|
|
**Step 5: Return true**
|
|
|
|
- If all checks pass, we can form valid pairs
|
|
|
|
common_pitfalls:
|
|
- title: Forgetting Negative Numbers
|
|
description: |
|
|
The array can contain negative numbers (up to `-10^9`). The modulo operation behaves differently across languages for negative numbers.
|
|
|
|
In Python: `(-3) % 5 = 2` (correct for this problem)
|
|
In Java/C++: `(-3) % 5 = -3` (problematic!)
|
|
|
|
For languages where `%` can return negative values, convert to positive: `((num % k) + k) % k`. This ensures all remainders fall in the range `[0, k-1]`.
|
|
wrong_approach: "Using num % k directly without handling negatives"
|
|
correct_approach: "Use ((num % k) + k) % k for positive remainders"
|
|
|
|
- title: Brute Force Pairing
|
|
description: |
|
|
Trying to actually find and match pairs using nested loops results in **O(n^2)** complexity. With `n = 10^5`, this means up to 10 billion operations, causing TLE.
|
|
|
|
The key insight is that we don't need to find the actual pairs — we only need to verify that valid pairings *exist* by checking remainder counts.
|
|
wrong_approach: "Nested loops trying every possible pairing"
|
|
correct_approach: "Count remainders and check complementary counts"
|
|
|
|
- title: Not Handling Remainder 0 Specially
|
|
description: |
|
|
Numbers with remainder `0` must pair with each other (no other remainder complements them to `k`). If there's an odd count of such numbers, pairing is impossible.
|
|
|
|
Similarly, when `k` is even, numbers with remainder `k/2` must pair with each other.
|
|
wrong_approach: "Only checking complementary pairs without special case for 0"
|
|
correct_approach: "Check that count[0] is even before checking other pairs"
|
|
|
|
key_takeaways:
|
|
- "**Modular arithmetic insight**: Two numbers sum to a multiple of `k` when their remainders sum to `0` or `k`"
|
|
- "**Count instead of pair**: When checking if valid pairings exist, counting elements in each category is often enough"
|
|
- "**Handle edge cases**: Negative modulo and self-pairing remainders (0 and k/2) need special attention"
|
|
- "**Pattern recognition**: This transforms a pairing problem into a counting/frequency problem"
|
|
|
|
time_complexity: "O(n). We iterate through the array once to build the frequency map, then iterate through at most `k` remainders to verify pairing is possible."
|
|
space_complexity: "O(k). We store counts for up to `k` different remainders in our frequency map."
|
|
|
|
solutions:
|
|
- approach_name: Remainder Counting
|
|
is_optimal: true
|
|
code: |
|
|
def can_arrange(arr: list[int], k: int) -> bool:
|
|
# Count frequency of each remainder
|
|
remainder_count = [0] * k
|
|
|
|
for num in arr:
|
|
# Handle negative numbers with proper modulo
|
|
remainder = ((num % k) + k) % k
|
|
remainder_count[remainder] += 1
|
|
|
|
# Check remainder 0: must have even count (pairs with itself)
|
|
if remainder_count[0] % 2 != 0:
|
|
return False
|
|
|
|
# Check complementary remainders
|
|
for r in range(1, k // 2 + 1):
|
|
if r == k - r:
|
|
# Middle remainder (k/2 when k is even): must be even
|
|
if remainder_count[r] % 2 != 0:
|
|
return False
|
|
else:
|
|
# Complementary remainders must have equal counts
|
|
if remainder_count[r] != remainder_count[k - r]:
|
|
return False
|
|
|
|
return True
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single pass through array plus O(k) remainder checks.
|
|
|
|
**Space Complexity:** O(k) — Array to store remainder counts.
|
|
|
|
We count how many numbers fall into each remainder bucket, then verify that complementary remainders have matching counts. This avoids the need to actually find pairs.
|
|
|
|
- approach_name: Hash Map Approach
|
|
is_optimal: true
|
|
code: |
|
|
from collections import Counter
|
|
|
|
def can_arrange(arr: list[int], k: int) -> bool:
|
|
# Count remainders using Counter
|
|
remainder_count = Counter()
|
|
|
|
for num in arr:
|
|
remainder = ((num % k) + k) % k
|
|
remainder_count[remainder] += 1
|
|
|
|
# Verify pairing is possible
|
|
for r in remainder_count:
|
|
complement = (k - r) % k # Handles r=0 case elegantly
|
|
|
|
if remainder_count[r] != remainder_count[complement]:
|
|
return False
|
|
|
|
return True
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single pass to count, plus iteration over unique remainders.
|
|
|
|
**Space Complexity:** O(min(n, k)) — Hash map only stores remainders that actually appear.
|
|
|
|
This variant uses a hash map instead of a fixed-size array. The `(k - r) % k` formula elegantly handles the remainder 0 case (since `(k - 0) % k = 0`). For sparse arrays where most remainders don't appear, this uses less memory than the array approach.
|
|
|
|
- approach_name: Brute Force
|
|
is_optimal: false
|
|
code: |
|
|
def can_arrange(arr: list[int], k: int) -> bool:
|
|
n = len(arr)
|
|
used = [False] * n
|
|
|
|
def backtrack(pairs_formed: int) -> bool:
|
|
# All pairs formed successfully
|
|
if pairs_formed == n // 2:
|
|
return True
|
|
|
|
# Find first unused element
|
|
first = -1
|
|
for i in range(n):
|
|
if not used[i]:
|
|
first = i
|
|
break
|
|
|
|
# Try pairing with every other unused element
|
|
used[first] = True
|
|
for j in range(first + 1, n):
|
|
if not used[j] and (arr[first] + arr[j]) % k == 0:
|
|
used[j] = True
|
|
if backtrack(pairs_formed + 1):
|
|
return True
|
|
used[j] = False
|
|
|
|
used[first] = False
|
|
return False
|
|
|
|
return backtrack(0)
|
|
explanation: |
|
|
**Time Complexity:** O(n!! ) — Double factorial due to pairing permutations.
|
|
|
|
**Space Complexity:** O(n) — Recursion stack and used array.
|
|
|
|
This backtracking approach tries all possible pairings. While correct, it's exponentially slow and will TLE on large inputs. Included to illustrate why the counting approach is necessary.
|