questions F-L
This commit is contained in:
219
backend/data/questions/four-sum.yaml
Normal file
219
backend/data/questions/four-sum.yaml
Normal file
@@ -0,0 +1,219 @@
|
||||
title: 4Sum
|
||||
slug: four-sum
|
||||
difficulty: medium
|
||||
leetcode_id: 18
|
||||
leetcode_url: https://leetcode.com/problems/4sum/
|
||||
categories:
|
||||
- arrays
|
||||
- two-pointers
|
||||
- sorting
|
||||
patterns:
|
||||
- two-pointers
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
**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)
|
||||
|
||||
|
||||
|
||||
**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
|
||||
|
||||
|
||||
|
||||
**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
|
||||
|
||||
|
||||
|
||||
**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.
|
||||
Reference in New Issue
Block a user