题目描述
思路解析动画文字版
组合题常用「排序 + 跳过相邻相同」去重。但这题求的是子序列——必须保持原数组顺序,一排序就破坏了顺序关系,[3,1,2] 排完成了 [1,2,3],意义全变了。所以这条路走不通,得换个去重办法。
用回溯逐个往后挑数:每选一个数就从它后面继续选(start 保证只往后、维持原顺序),且要求新数 ≥ path 末尾来保证不下降。path 长度到 2 就随时收一个。选→递归→撤销,老三步。
去重靠一个本层 set:在同一个递归层里(同一个父节点下),如果某个数值已经被选过当过分支头,再遇到相同数值就跳过。注意是「同层」,不同层的相同数值(比如两个 7 一前一后被选进同一条 path)是允许的。下面用 [4,6,7,7] 走一遍。
起点 · path 空,4 个数都可选:开搜:path 还是空的,4 个数(下标 0~3 = 4,6,7,7)都没用过。从下标 0 开始,逐个挑头。先看下标 0 的 4。
顶层 · 选 4 · path=[4]:顶层第一个选 4,path=[4]。本层 set 记下 {4}。长度才 1,还不能收(要 ≥2)。接着从下标 1 往后,找 ≥4 的数继续接。
选 6 · 收下 [4,6]:在 [4] 后面选 6:6 ≥ 4 满足不下降,path=[4,6],长度到 2 了,收下 [4,6]!还能再往后接,继续。
选 7 · 收下 [4,6,7]:在 [4,6] 后面选下标 2 的 7:7 ≥ 6,path=[4,6,7],收下 [4,6,7]。还有下标 3 的另一个 7 可以接。
选第二个 7 · 收下 [4,6,7,7]:在 [4,6,7] 后面选下标 3 的 7:7 ≥ 7 也算不下降(允许相等),path=[4,6,7,7],收下 [4,6,7,7]!后面没数了,撤销这个 7 往回退。
撤销末尾7 · 回到 [4,6,7]:撤销刚收进来的下标 3 的 7,path 退回 [4,6,7],下标 3 重新变回可选(灰恢复)。[4,6,7] 这层后面已经没有数了,继续往上退回 [4,6]。
回到 [4,6] · 本层换下一个候选:撤销下标 3 的 7,退回到 [4,6] 这一层。刚才在这层已经拿下标 2 的 7 当过分支头,本层 set 里记着 {7}。下一个候选是下标 3 的 7——数值也是 7,要看会不会被去重挡住。
同层去重 · 下标3的7被跳过:同层去重实演:在 [4,6] 这层,下标 2 的 7 已经当过分支头,本层 set 含 7。现在下标 3 又是 7,同一层同数值不能再开第二个分支,直接跳过(打叉那个)。否则会重复搜出一模一样的 [4,6,7…]。[4,6] 这层探完,撤销 6。
回到 [4] · 换选下标2的7:撤销 6 回到 [4] 这层。本层([4] 之后这一层)刚才拿 6 当过分支头,set 含 {6},现在换下标 2 的 7:7 ≥ 4,path=[4,7],收下 [4,7]。注意 6 已撤走、重新变回可选。
[4,7] 后选下标3的7 · 收下 [4,7,7]:在 [4,7] 后面接下标 3 的 7:7 ≥ 7 满足,path=[4,7,7],收下 [4,7,7]。这是不同层的两个 7(一个当头、一个接尾),允许同时进 path。后面没数,撤销回退。
回 [4] 层 · 下标3的7再被同层去重:退回 [4] 这层,本层在下标 2 选过 7,set 含 7。轮到下标 3 的 7:数值重复,同层跳过(打叉)。4 打头的所有分支探完,撤销 4,顶层换下一个头。
顶层换头 · 选 6 · path=[6]:撤销 4,顶层 set 现在含 {4},换选下标 1 的 6 当头,path=[6]。4 已不在 path(恢复成可选)。长度才 1 不收,从下标 2 往后接 ≥6 的数。
选下标2的7 · 收下 [6,7]:在 [6] 后面选下标 2 的 7:7 ≥ 6,path=[6,7],收下 [6,7]!本层 set 记下 {7}。继续往后接。
[6,7] 后接下标3的7 · 收下 [6,7,7]:在 [6,7] 后接下标 3 的 7:7 ≥ 7,path=[6,7,7],收下 [6,7,7]。撤销回退。
回 [6] 层 · 下标3的7同层去重:退回 [6] 这层,本层在下标 2 选过 7,下标 3 又是 7,同层跳过(打叉)。6 打头探完,撤销 6,顶层再换头。
顶层换头 · 选下标2的7 · path=[7]:撤销 6,顶层换选下标 2 的 7 当头,path=[7],顶层 set 记下 {…,7}。长度 1 不收,往后接 ≥7 的数。
[7] 后接下标3的7 · 收下 [7,7]:在 [7] 后面接下标 3 的 7:7 ≥ 7,path=[7,7],收下 [7,7]!这是最后一个解。撤销回到顶层。
顶层 · 下标3的7被同层去重:回到顶层,下标 2 的 7 已经当过头、顶层 set 含 7,下标 3 的 7 数值重复,同层跳过(打叉)。所有分支探尽。
搜索完成 · 收齐 8 个子序列:整棵搜索树走完,8 个不下降子序列全部收齐,没有一个重复。「同层 set 去重」是这题不出重复的核心,「新数 ≥ path 末尾」保证不下降。
三个机制各管一件事:传 i+1 维持原数组顺序(这是子序列的本质),新数 ≥ 末尾保证不下降,每层独立的 set 拦住同层重复值。三者合起来才能不漏不重地搜全。
易错实演 · used 写成全局会漏解:负例:如果 used 是全局的,4 打头时已经把 7 加进 set 了;轮到 [6] 想接 7 时,7 被全局 set 误判为「用过」直接跳过(打叉),[6,7]、[6,7,7] 全被漏掉。所以 used 必须每层各一个、进函数才新建。
边界三连:全递减就返回空;有相等的数允许组成不下降(4,4 合法)。回溯天然处理这些情况,搜不到就是空。
面试常拿本题和 LC90/LC40 对比,核心都在「同层去重」,区别在于能不能排序。
参考代码
class Solution: def findSubsequences(self, nums): ans = [] path = [] def backtrack(start): if len(path) >= 2: ans.append(path[:]) used = set() # 本层去重 for i in range(start, len(nums)): if path and nums[i] < path[-1]: continue # 不下降 if nums[i] in used: # 同层重复 continue used.add(nums[i]) path.append(nums[i]) backtrack(i + 1) path.pop() backtrack(0) return ans复杂度
- 时间复杂度:O(2^n · n),最多 2^n 个子序列,每个拷贝 O(n)
- 空间复杂度:O(n),递归深度最多 n,path 与每层 set 共 O(n)
可套用的代码模板
骨架记牢:每层新建 used 集合(不是全局!)、约束不满足 continue、本层重复值 continue、递归传 i+1、选完 pop。求子序列且不能排序时的去重母模板。
模板
def bt(start): if len(path) >= 2: res.append(path[:]) used = set() # 每层新建 for i in range(start, n): if path and a[i] < path[-1]: continue # 约束 if a[i] in used: continue # 同层去重 used.add(a[i]) path.append(a[i]) bt(i + 1) # 关键 i+1 path.pop()易错点
面试追问把动画讲成自己的话
追问和 LC90 子集 II 的去重有什么异同?
追问为什么收集结果不放在递归出口(return 处),而是一进函数就判 len≥2 收?
追问用什么去重更省?数组下标 set 还是数值 set?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
划分为 k 个相等的子集
LeetCode 698 · 中等 · 沿着 回溯套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题