← 返回题目列表

递增子序列如何用回溯去重?为什么每层要用 used 集合?

中等 第 23 / 30 题 更新于 2026/07/31
回溯去重子序列

简化版

递增子序列要求保持原数组顺序,并且长度至少为 2

回溯时从当前位置往后选数,如果当前数小于路径最后一个数,就不能选。

由于数组可能有重复值,同一层递归要用 used 集合避免选择相同数字,防止生成重复子序列。

详细版

递归函数 dfs(start) 表示接下来可以从下标 start 开始选择元素。

如果 path.length >= 2,当前路径就是一个合法递增子序列,可以加入答案。然后继续尝试后面的元素。

每层创建一个 used 集合,记录这一层已经尝试过哪些数。如果同一层再次遇到相同值,跳过。

注意不能先排序,因为子序列必须保持原数组相对顺序。排序会改变题目语义。

完整版教学

一、子序列和子集有什么不同

子集问题通常可以先排序再做同层去重,因为元素顺序不重要。但子序列要求保持原数组相对顺序,不能排序。比如 [4,6,7,7][4,7] 可以来自不同下标,但结果只需要一份;同时 [6,4] 不能出现,因为它违背原顺序和递增条件。

易错点:子序列问题不能随便排序,排序会改变可选顺序。

二、递增条件怎么维护

路径 path 保存当前已经选择的子序列。新候选 nums[i] 必须满足:

path 为空,或者 nums[i] >= path[path.length - 1]

这里是非严格递增,所以允许相等。例如 [7,7] 是合法递增子序列。如果题目要求严格递增,条件才改为 >

三、为什么每层要用 used

重复来自同一层选择了相同值。以 [4,6,7,7] 为例,在某一层如果第一个 7 已经作为“本层选择值 7”探索过,那么第二个 7 再作为同层起点会生成重复结果。每层的 used 集合只限制当前层,不限制不同深度,因为不同深度选择相同值可能是合法的 [7,7]

去重范围是否正确原因
全局 used错误会禁止合法重复值
每层 used正确只去掉同层重复分支
不去重错误会产生重复子序列

这就是这题去重最关键的点。

四、代码模板

实现如下:

function findSubsequences(nums) {
  const ans = []
  const path = []

  function dfs(start) {
    if (path.length >= 2) ans.push([...path])
    const used = new Set()
    for (let i = start; i < nums.length; i++) {
      if (used.has(nums[i])) continue
      if (path.length > 0 && nums[i] < path[path.length - 1]) continue
      used.add(nums[i])
      path.push(nums[i])
      dfs(i + 1)
      path.pop()
    }
  }

  dfs(0)
  return ans
}

used 放在递归函数内部,意味着每一层都有自己的去重集合。

五、带数字推演

数组 [4,6,7,7]。当路径是 [4,6] 时,后面两个 7 都能接上,但在同一层只允许第一个 7 开启分支。进入下一层后,路径变成 [4,6,7],另一个 7 又可以被选择,形成 [4,6,7,7]

同层重复:跳过
不同层重复:允许

这句话能帮你区分去重边界。

六、常见误区与追问

  • 误区:先排序再回溯。 子序列必须保持原数组顺序,排序会改变答案集合。
  • 误区:用全局 Set 禁止重复数字。 会错误删除 [7,7] 这类合法结果。
  • 误区:只在加入答案时用字符串 Set 去重。 能兜底但效率差,也没有体现回溯剪枝能力。
  • 追问:为什么 path.length >= 2 就加入? 题目要求长度至少 2,后续还能继续延长当前路径。
  • 追问:递增是严格的吗? 标准题通常是非严格递增,即允许相等。

这些问题考的是“顺序约束”和“同层去重”。

七、加强记忆

递增子序列记成“不能排序,路径递增,同层去重”。候选必须不小于路径末尾;长度达到 2 就可以收集;每层用一个 used 集合跳过相同起始值。全局禁止重复会误杀合法重复,同层去重才是刚刚好。