nSum 问题通用模板详解

如果你刷过 LeetCode 的 Two Sum3Sum4Sum,很容易发现它们其实在问同一类问题:

从数组里选出 n 个数,让它们的和等于 target,并且结果不能重复。

这类题可以统一叫做 nSumKSum 问题。一个非常经典的通用模板就是:先排序,然后把 nSum 递归降维成 (n-1)Sum,一直降到 2Sum,最后用双指针解决。

一句话概括这个模板:

nSum 的核心不是一次性找出 n 个数,而是先固定一个数,再去剩下的数组里找 (n-1)Sum


一、问题从 3Sum 开始

3Sum 为例,题目通常是:

给定一个整数数组 nums,找出所有不重复的三元组 [a, b, c],使得:

a + b + c = 0

代码为:

from typing import List

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        return self.nSumTarget(nums, 3, 0, 0)

它只做两件事:

  1. 先排序。
  2. 调用通用的 nSumTarget(nums, 3, 0, 0)

这里的参数含义是:

nSumTarget(nums, n, start, target)

也就是:从 nums[start:] 这个范围里,找出所有由 n 个数组成、和为 target 的组合。

所以 threeSum(nums) 本质上就是从整个数组里,找 3 个数,使它们的和为 0。


二、为什么一定要先排序?

排序是这个模板成立的前提。它主要有两个作用。

第一个作用是让 2Sum 可以使用双指针。

数组有序后,左指针 left 指向较小的数,右指针 right 指向较大的数:

nums[left] + nums[right] < target  ->  left 右移,让和变大
nums[left] + nums[right] > target  ->  right 左移,让和变小
nums[left] + nums[right] == target ->  找到一组答案

第二个作用是方便去重。

比如排序后数组长这样:

[-4, -1, -1, 0, 1, 2]

相同元素会挨在一起。这样在遍历或移动指针时,只要发现当前元素和上一个元素相同,就可以跳过,避免产生重复组合。


三、递归思想:把 nSum 变成 2Sum

模板最关键的地方在这里:

sub = self.nSumTarget(nums, n - 1, i + 1, target - nums[i])

当我们要求 n 个数的和为 target 时,可以先固定一个数 nums[i]

那么剩下的问题就变成:

从 i + 1 之后的数组里,找 n - 1 个数,使它们的和等于 target - nums[i]

也就是:

nSum(nums, n, start, target) = 固定 nums[i] + (n-1)Sum(nums, n-1, i+1, target-nums[i])

3Sum 举例:

找 3 个数,和为 0

可以先固定一个数,比如 -1,那么问题就变成:

找 2 个数,和为 1

这就是递归降维。

如果是 4Sum,过程也是一样:

4Sum -> 固定一个数 -> 3Sum
3Sum -> 固定一个数 -> 2Sum
2Sum -> 双指针解决

所以这套方法可以自然扩展到任意固定的 n


四、为什么最终停在 2Sum?

递归一定要有终点。在这个模板里,终点就是 2Sum

if n == 2:
    left, right = start, sz - 1
    while left < right:
        sum_val = nums[left] + nums[right]
        left_val, right_val = nums[left], nums[right]

        if sum_val < target:
            while left < right and nums[left] == left_val:
                left += 1
        elif sum_val > target:
            while left < right and nums[right] == right_val:
                right -= 1
        else:
            res.append([left_val, right_val])
            while left < right and nums[left] == left_val:
                left += 1
            while left < right and nums[right] == right_val:
                right -= 1

为什么停在 2Sum
因为有序数组里的 2Sum 可以在线性时间内解决,复杂度是 O(N)
如果继续递归到 1Sum,反而没有必要。2Sum 的双指针已经是这套模板里最高效、最稳定的终点。


五、去重逻辑是这道题的灵魂

nSum 难的不是找到答案,而是避免重复答案。

这份代码里有两层去重。

1. 递归层去重

n > 2 时,会枚举一个固定数:

for i in range(start, sz):
    if i > start and nums[i] == nums[i - 1]:
        continue

这段代码的意思是:在同一层递归里,如果当前固定的数和前一个固定的数相同,就跳过。

注意这里的判断是:

i > start

而不是简单写成:

i > 0

因为每一层递归都有自己的起点 start。我们只需要跳过同一层中的重复选择,不能误伤不同递归层里的合法选择。

2. 2Sum 层去重

2Sum 中,如果找到了一个答案:

res.append([left, right])

还要继续跳过所有重复的 leftright

while left < right and nums[left] == left_val:
    left += 1
while left < right and nums[right] == right_val:
    right -= 1

如果当前和太小,也不是简单 left += 1,而是跳过所有相同的 left_val

while left < right and nums[left] == left_val:
    left += 1

如果当前和太大,也跳过所有相同的 right_val

while left < right and nums[right] == right_val:
    right -= 1

这样可以保证结果集中不会出现重复组合。


六、完整模板代码

from typing import List
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        return self.nSumTarget(nums, 3, 0, 0)

    # 注意:调用这个函数之前一定要先给 nums 排序
    # n:要求几个数之和
    # start:从哪个索引开始找
    # target:目标和
    def nSumTarget(self, nums: List[int], n: int, start: int, target: int) -> List[List[int]]:
        sz = len(nums)
        res = []

        if n < 2 or sz < n:
            return res

        if n == 2:
            left, right = start, sz - 1

            while left < right:
                sum_val = nums[left] + nums[right]
                left_val, right_val = nums[left], nums[right]

                if sum_val < target:
                    while left < right and nums[left] == left_val:
                        left += 1
                elif sum_val > target:
                    while left < right and nums[right] == right_val:
                        right -= 1
                else:
                    res.append([left_val, right_val])
                    while left < right and nums[left] == left_val:
                        left += 1
                    while left < right and nums[right] == right_val:
                        right -= 1
        else:
            for i in range(start, sz):
                if i > start and nums[i] == nums[i - 1]:
                    continue

                sub = self.nSumTarget(nums, n - 1, i + 1, target - nums[i])

                for arr in sub:
                    res.append([nums[i]] + arr)

        return res

七、用一个例子走一遍

假设:

nums = [-1, 0, 1, 2, -1, -4]

先排序:

[-4, -1, -1, 0, 1, 2]

调用:

nSumTarget(nums, 3, 0, 0)

也就是找 3Sum = 0

第一层递归会依次固定一个数。


固定 -4

剩下要找 2 个数,目标和是 4

[-1, -1, 0, 1, 2] 里找不到和为 4 的两数。


固定第一个 -1

剩下要找 2 个数,目标和是 1

可以找到:

[-1, 2]
[0, 1]

拼回固定的 -1,得到:

[-1, -1, 2]
[-1, 0, 1]

继续到第二个 -1 时,因为它和前一个 -1 相同,并且处于同一层递归,所以跳过:

if i > start and nums[i] == nums[i - 1]:
    continue

最终结果就是:

[[-1, -1, 2], [-1, 0, 1]]

八、复杂度分析

排序需要:

O(N log N)

2Sum 是:

O(N)

3Sum 是固定一个数,然后做 2Sum

O(N^2)

4Sum 是固定一个数,然后做 3Sum

O(N^3)

所以一般来说,nSum 的时间复杂度是:

O(N^(n-1))

递归深度是 n,所以除结果集以外,额外空间复杂度大约是:

O(n)

如果把输出结果也算进去,空间复杂度还要加上结果集本身的大小。


九、可以加的优化:剪枝

如果想进一步优化,可以在递归时加入剪枝。

因为数组已经排序,所以可以提前判断:

最小的 n 个数之和 > target,后面不用找了
最大的 n 个数之和 < target,后面也不用找了

简单写法如下:

if start + n > sz:
    return res

min_sum = sum(nums[start:start + n])
max_sum = sum(nums[sz - n:sz])

if min_sum > target or max_sum < target:
    return res

这不是模板成立的必要条件,但在数据量较大时能减少很多无效递归。


总结

这道题最重要的不是背代码,而是理解递归降维:

nSum = 固定一个数 + (n-1)Sum

一直降到:

2Sum = 双指针

再配合排序后的去重逻辑,就能统一解决 3Sum4Sum 以及更一般的 KSum 问题。

可以把它记成一个四步模板:

  1. 排序。
  2. 如果 n == 2,用双指针。
  3. 如果 n > 2,枚举并固定一个数。
  4. 对剩余区间递归求 (n-1)Sum,拼接结果并去重。

这就是 nSum 问题最核心、最通用的解法框架。

posted @ 2026-08-28 16:48  江鸟Dev  阅读(16)  评论(0)    收藏  举报