The problem asks us to find all unique quadruplets in an array that sum up to a given target.
Pattern: Generalized k-Sum (Sorting + Two Pointers + Recursion/Loops).
Similar to 3Sum, a brute force approach of four nested loops would yield time, which is unacceptable. We can extend the optimal logic of 3Sum:
- Sort the array.
- Fix one number, reducing the problem to 3Sum.
- Fix a second number, reducing the problem to Two Sum II (Two Pointers).
This gives an solution for 4Sum.
For a scalable approach, we can write a generalized kSum function. If , we use the two-pointer approach. If , we iterate through the array, fix the current element, and recursively call kSum with and a reduced target.
Pseudocode:
- Four nested loops
i,j,k,l. - Check if the sum equals
target. - Store in a set to avoid duplicates.
// Unacceptably slow, purely for theoretical understanding
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
set<vector<int>> res;
sort(nums.begin(), nums.end());
int n = nums.size();
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
for (int k = j + 1; k < n; ++k) {
for (int l = k + 1; l < n; ++l) {
if ((long long)nums[i] + nums[j] + nums[k] + nums[l] == target) {
res.insert({nums[i], nums[j], nums[k], nums[l]});
}
}
}
}
}
return vector<vector<int>>(res.begin(), res.end());
}
};Pseudocode:
- Sort
nums. - Define
kSum(start_index, target, k). - Base Cases for
kSum:- If
start_indexis out of bounds, return[]. - If the smallest possible sum (k * smallest element) > target, return
[]. - If the largest possible sum (k * largest element) < target, return
[].
- If
- If : perform standard Two Sum using two pointers.
- If :
- Loop
ifromstart_indexton-1. - Skip duplicates:
if i > start_index and nums[i] == nums[i-1]. - Call
kSum(i + 1, target - nums[i], k - 1). - Append
nums[i]to each result returned and collect them.
- Loop
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
return kSum(nums, target, 0, 4);
}
private:
vector<vector<int>> kSum(vector<int>& nums, long long target, int start, int k) {
vector<vector<int>> res;
// Base cases to early terminate
if (start == nums.size()) {
return res;
}
long long average_value = target / k;
// We cannot obtain a sum of target if the smallest value is greater than the average
// or the largest value is smaller than the average.
if (nums[start] > average_value || average_value > nums.back()) {
return res;
}
if (k == 2) {
return twoSum(nums, target, start);
}
for (int i = start; i < nums.size(); ++i) {
// Skip duplicates
if (i == start || nums[i - 1] != nums[i]) {
for (vector<int>& subset : kSum(nums, target - nums[i], i + 1, k - 1)) {
res.push_back({nums[i]});
res.back().insert(res.back().end(), subset.begin(), subset.end());
}
}
}
return res;
}
vector<vector<int>> twoSum(vector<int>& nums, long long target, int start) {
vector<vector<int>> res;
int left = start;
int right = nums.size() - 1;
while (left < right) {
long long total = (long long)nums[left] + nums[right];
if (total < target) {
left++;
} else if (total > target) {
right--;
} else {
res.push_back({nums[left], nums[right]});
left++;
right--;
while (left < right && nums[left] == nums[left - 1]) {
left++;
}
}
}
return res;
}
};- Time: . For 4Sum, , so the time complexity is . The recursive
kSumfunction nests loops up to times, and the base case (Two Sum) takes . Thus, . - Space: for the recursion stack. For 4Sum, this is auxiliary space. Sorting in C++ generally takes space.
- Large Target/Elements: Overflows can happen if numbers are huge. Using
long longfor targets and sums handles arbitrarily large integers automatically. The early terminationnums[start] > average_value || average_value > nums.back()handles mathematically impossible targets instantly. - Duplicates everywhere:
[2, 2, 2, 2, 2]with target8. Handled cleanly by thei == start || nums[i - 1] != nums[i]deduplication logic.
Thought Process & Recognition:
When scaling from 3Sum to 4Sum, you should immediately recognize that writing 3 nested loops (with a two-pointer base) is brittle. Hardcoding n nested loops is terrible practice.
Instead, recognize the recursive nature: -Sum is just a loop that picks an element and calls -Sum.
The real magic is in the early termination. Calculating the average needed target / k and checking if the smallest or largest elements can even support that average prunes massive branches of the recursion tree, turning a slow into an extremely fast execution in practice.