Files
codetutor/backend/data/questions/four-sum.yaml

237 lines
10 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
title: 4Sum
slug: four-sum
difficulty: medium
leetcode_id: 18
leetcode_url: https://leetcode.com/problems/4sum/
categories:
- arrays
- two-pointers
- sorting
patterns:
- slug: two-pointers
is_optimal: true
function_signature: "def four_sum(nums: list[int], target: int) -> list[list[int]]:"
test_cases:
visible:
- input: { nums: [1, 0, -1, 0, -2, 2], target: 0 }
expected: [[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]]
- input: { nums: [2, 2, 2, 2, 2], target: 8 }
expected: [[2, 2, 2, 2]]
hidden:
- input: { nums: [0, 0, 0, 0], target: 0 }
expected: [[0, 0, 0, 0]]
- input: { nums: [1, 2, 3, 4], target: 10 }
expected: [[1, 2, 3, 4]]
- input: { nums: [1, 2, 3, 4], target: 100 }
expected: []
description: |
Given an array `nums` of `n` integers, return *an array of all the **unique** quadruplets* `[nums[a], nums[b], nums[c], nums[d]]` such that:
- `0 <= a, b, c, d < n`
- `a`, `b`, `c`, and `d` are **distinct**
- `nums[a] + nums[b] + nums[c] + nums[d] == target`
You may return the answer in **any order**.
constraints: |
- `1 <= nums.length <= 200`
- `-10^9 <= nums[i] <= 10^9`
- `-10^9 <= target <= 10^9`
examples:
- input: "nums = [1,0,-1,0,-2,2], target = 0"
output: "[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]"
explanation: "Three unique quadruplets sum to 0: [-2,-1,1,2], [-2,0,0,2], and [-1,0,0,1]."
- input: "nums = [2,2,2,2,2], target = 8"
output: "[[2,2,2,2]]"
explanation: "The only quadruplet that sums to 8 is [2,2,2,2]."
explanation:
intuition: |
If you've solved **3Sum**, 4Sum follows the same reduction strategy: fix one element and solve a smaller problem.
Think of it like peeling an onion. 3Sum reduces to 2Sum by fixing one element. Similarly, **4Sum reduces to 3Sum** by fixing the first element, which then reduces to 2Sum. Each layer peels away one dimension of complexity.
The key insight is that after sorting, we can use **two nested loops** to fix the first two elements, then apply the familiar two-pointer technique to find the remaining pair. This gives us O(n³) time — the best we can do when there can be O(n³) valid quadruplets.
Sorting remains essential for two reasons:
1. **Two pointers work**: Adjusting sum by moving pointers left or right
2. **Duplicate skipping**: Adjacent duplicates become neighbours we can easily skip
approach: |
We solve this using **Sort + Two Nested Loops + Two Pointers**:
**Step 1: Sort the array**
- Sorting enables two-pointer technique and easy duplicate detection
- Time: O(n log n), dominated by the O(n³) main algorithm
&nbsp;
**Step 2: Fix the first element**
- For each `i` from 0 to n-4:
- Skip if `nums[i] == nums[i-1]` (avoid duplicate quadruplets)
- **Early termination**: If `nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target`, break (smallest possible sum exceeds target)
- **Skip if too small**: If `nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target`, continue (largest possible sum is still less than target)
&nbsp;
**Step 3: Fix the second element**
- For each `j` from i+1 to n-3:
- Skip if `nums[j] == nums[j-1]` and `j > i + 1` (avoid duplicates)
- Apply similar early termination and skip optimisations
&nbsp;
**Step 4: Two-pointer search for remaining pair**
- Set `left = j + 1`, `right = n - 1`
- Calculate `total = nums[i] + nums[j] + nums[left] + nums[right]`
- If `total < target`: move `left` right
- If `total > target`: move `right` left
- If `total == target`: found a quadruplet!
- Add to result, skip duplicates, move both pointers
&nbsp;
**Step 5: Return all unique quadruplets**
Duplicate skipping happens at all four levels: outer loop, second loop, left pointer, and right pointer.
common_pitfalls:
- title: Integer Overflow
description: |
With constraints `-10^9 <= nums[i] <= 10^9` and `-10^9 <= target <= 10^9`, the sum of four numbers can reach `4 × 10^9`, which **overflows 32-bit integers**.
In languages like C++ or Java, you must use `long long` or `long` types for the sum calculation. In Python, integers have arbitrary precision, so this isn't an issue — but be aware when porting to other languages.
wrong_approach: "Using int for sum in C++/Java"
correct_approach: "Use long/long long or cast during addition"
- title: Incorrect Duplicate Skipping at Second Level
description: |
When skipping duplicates for the second element `j`, you must check `j > i + 1` before comparing `nums[j] == nums[j-1]`.
Without this check, you might skip the very first valid `j` after `i`, missing valid quadruplets.
Example: `nums = [0,0,0,0]`, `target = 0` — if you skip when `j == i + 1`, you'd incorrectly skip `j = 1` when comparing to `nums[0]`.
wrong_approach: "if nums[j] == nums[j-1]: continue (always)"
correct_approach: "if j > i + 1 and nums[j] == nums[j-1]: continue"
- title: Missing Early Termination Optimisations
description: |
Unlike 3Sum where you can break when `nums[i] > 0` (since target is 0), 4Sum has a variable target. The optimisations become:
- **Break** if `nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target` — smallest sum exceeds target
- **Continue** if `nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target` — largest sum too small
Without these, you may TLE on edge cases with skewed distributions.
wrong_approach: "No early termination checks"
correct_approach: "Check minimum and maximum possible sums at each level"
key_takeaways:
- "**Generalise N-sum**: Fix k-2 elements with nested loops, then apply two pointers — this pattern works for any kSum"
- "**Time complexity is O(n^(k-1))**: For 4Sum, it's O(n³); for kSum in general, O(n^(k-1)) is optimal when there can be that many solutions"
- "**Early termination matters**: Checking minimum and maximum possible sums can dramatically prune the search space"
- "**Duplicate handling at every level**: Each nested loop needs its own duplicate skip logic with the correct boundary check"
time_complexity: "O(n³). Sorting is O(n log n), then two nested O(n) loops each contain an O(n) two-pointer search."
space_complexity: "O(log n) to O(n). Depends on the sorting algorithm — O(log n) for in-place sorts, O(n) for others. The output is not counted as extra space."
solutions:
- approach_name: Sort + Two Pointers
is_optimal: true
code: |
def four_sum(nums: list[int], target: int) -> list[list[int]]:
nums.sort() # Enable two pointers and duplicate detection
result = []
n = len(nums)
for i in range(n - 3):
# Skip duplicates for first element
if i > 0 and nums[i] == nums[i - 1]:
continue
# Early termination: smallest possible sum exceeds target
if nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target:
break
# Skip: largest possible sum with nums[i] is still too small
if nums[i] + nums[n - 3] + nums[n - 2] + nums[n - 1] < target:
continue
for j in range(i + 1, n - 2):
# Skip duplicates for second element (note: j > i + 1)
if j > i + 1 and nums[j] == nums[j - 1]:
continue
# Early termination for inner loop
if nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target:
break
# Skip if largest sum with nums[i], nums[j] is too small
if nums[i] + nums[j] + nums[n - 2] + nums[n - 1] < target:
continue
# Two pointers for remaining pair
left, right = j + 1, n - 1
while left < right:
total = nums[i] + nums[j] + nums[left] + nums[right]
if total < target:
left += 1
elif total > target:
right -= 1
else:
# Found a quadruplet
result.append([nums[i], nums[j], nums[left], nums[right]])
# Skip duplicates for left pointer
while left < right and nums[left] == nums[left + 1]:
left += 1
# Skip duplicates for right pointer
while left < right and nums[right] == nums[right - 1]:
right -= 1
# Move both pointers
left += 1
right -= 1
return result
explanation: |
**Time Complexity:** O(n³) — O(n log n) sort + two nested O(n) loops with O(n) two-pointer search inside.
**Space Complexity:** O(log n) to O(n) — Sorting space; output not counted.
We sort the array, then use two nested loops to fix the first two elements. For each pair, two pointers find the remaining pair that completes the target sum. Early termination and skip optimisations prune many unnecessary iterations.
- approach_name: Brute Force
is_optimal: false
code: |
def four_sum(nums: list[int], target: int) -> list[list[int]]:
n = len(nums)
result = set() # Use set to avoid duplicates
# Try all possible quadruplets
for i in range(n):
for j in range(i + 1, n):
for k in range(j + 1, n):
for l in range(k + 1, n):
if nums[i] + nums[j] + nums[k] + nums[l] == target:
# Sort tuple to handle duplicates
quad = tuple(sorted([nums[i], nums[j], nums[k], nums[l]]))
result.add(quad)
return [list(q) for q in result]
explanation: |
**Time Complexity:** O(n⁴) — Four nested loops checking all combinations.
**Space Complexity:** O(k) — Where k is the number of unique quadruplets stored in the set.
This naive approach checks every possible combination of four elements. While correct, it's too slow for larger inputs. With n=200, this means up to 64 million iterations. The optimal solution reduces this to O(n³) by using sorting and two pointers.