Files
codetutor/backend/data/questions/check-if-array-pairs-are-divisible-by-k.yaml
2025-05-25 10:16:13 +01:00

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
&nbsp;
**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`
&nbsp;
**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`
&nbsp;
**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`
&nbsp;
**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.