# [18] 四数之和

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

0 <= a, b, c, d < n

a、b、c 和 d 互不相同

nums[a] + nums[b] + nums[c] + nums[d] == target

你可以按 任意顺序 返回答案 。

示例 1:

输入:nums = [1,0,-1,0,-2,2], target = 0

输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

示例 2:

输入:nums = [2,2,2,2,2], target = 8

输出:[[2,2,2,2]]

提示:

1 <= nums.length <= 200

-10^9 <= nums[i] <= 10^9

-10^9 <= target <= 10^9

作为 [1] 两数之和 与 [15] 三数之和 的再一次升级版,这一次我们可以整理出这一类问题的通用套路了。

实际上 nSum 问题的通用解法为:先通过遍历数组选定 N 元组中最小的那个数,之后再通过遍历选定第二小的数……当然这一过程可以用递归实现。

直到剩余 2 个数未确定,此时调用 2Sum 方法,通过空间换时间,将 O(n^2) 的时间复杂度缩小为 O(n)。

在进入下一级递归之前,我们需要做好一定的剪枝来提升性能。例如,最小的 4 个数都小于 target,直接退出;或者最大的 4 个数都大于 target, 直接跳过。再例如,当发现当前值与下一个值相同时,说明有重复元素,同样跳过。

最终,nSum 的时间复杂度为 O(n^(N-1))。所以随着 N 的增大,这一算法的优越度也变得越来越低了……因为 N=5 以上以后,一个 O(n^4) 的算法已经足以让很多用例直接超时了。

因此,掌握常规算法,并了解其衍生的题型改造就已经足够了,出 5Sum,6Sum 的题并无必要。

function fourSum(nums: number[], target: number): number[][] {
  nums = nums.sort((a, b) => a - b);
  const len = nums.length;

  const res: number[][] = [];
  for (let i = 0; i < len - 3; i++) {
    // 最小的 4 个数都小于 target,直接退出;或者最大的 4 个数都大于 target, 直接跳过
    if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) break;
    if (nums[i] + nums[len - 1] + nums[len - 2] + nums[len - 3] < target) continue;
    // 去掉重复情况
    if (i > 0 && nums[i - 1] === nums[i]) continue;

    // 接下来就是 3Sum 问题了
    threeSumCase(i, nums[i], target, nums, res);
  }

  return res;

  function threeSumCase(start: number, first: number, target: number, nums: number[], res: number[][]) {
    const len = nums.length;
    for (let i = start + 1; i < len - 2; i++) {
      const second = nums[i];

      // 最小的 4 个数都小于 target,直接退出;或者最大的 4 个数都大于 target, 直接跳过
      if (first + second + nums[i + 1] + nums[i + 2] > target) break;
      if (first + second + nums[len - 1] + nums[len - 2] < target) continue;
      // 去掉重复情况
      if (i > start + 1 && nums[i - 1] === nums[i]) continue;

      // 接下来就是 2Sum 问题了
      let left = i + 1;
      let right = nums.length - 1;
      while (left < right) {
        const sum = first + second + nums[left] + nums[right];
        if (sum === target) {
          res.push([first, second, nums[left], nums[right]]);
          // 去除重复情况
          while (left < right && nums[left + 1] === nums[left]) left += 1;
          while (left < right && nums[right - 1] === nums[right]) right -= 1;

          // 指针移动到下一组情况
          left += 1;
          right -= 1;
        } else if (sum > target) {
          right -= 1;
        } else {
          left += 1;
        }
      }
    }
  }
}