题目描述
思路解析动画文字版
记住这句,下面每一步都在套它。
收集第 1 个子集:根节点:空路径也是一个合法子集 → 收集 []。
选下标 0 的 1:这一层从下标 0 的 1 往下钻:把它加进 path。
path 接上 1:path → [1],进入下一层(从下标 1 继续,避免重复用同一个位置)。
收集第 2 个子集:每到一个节点先把当前 path 收进结果:[1]。
选下标 1 的 2:这一层从下标 1 的 2 往下钻:把它加进 path。
path 接上 2:path → [1,2],进入下一层(从下标 2 继续,避免重复用同一个位置)。
收集第 3 个子集:每到一个节点先把当前 path 收进结果:[1,2]。
选下标 2 的 2:这一层从下标 2 的 2 往下钻:把它加进 path。
path 接上 2:path → [1,2,2],进入下一层(从下标 3 继续,避免重复用同一个位置)。
收集第 4 个子集:每到一个节点先把当前 path 收进结果:[1,2,2]。
撤销下标 2 的 2:这条分支走完,撤销 2(path → [1,2]),回到上一层换下一个候选。
撤销下标 1 的 2:这条分支走完,撤销 2(path → [1]),回到上一层换下一个候选。
同层去重:跳过下标 2 的 2:下标 2 的 2 和同层前一个下标 1 的 2 相同,这一层已经从 2 开过分支了,再选会产生重复子集 → 画 ✗ 剪掉。
撤销下标 0 的 1:这条分支走完,撤销 1(path → []),回到上一层换下一个候选。
选下标 1 的 2:这一层从下标 1 的 2 往下钻:把它加进 path。
path 接上 2:path → [2],进入下一层(从下标 2 继续,避免重复用同一个位置)。
收集第 5 个子集:每到一个节点先把当前 path 收进结果:[2]。
选下标 2 的 2:这一层从下标 2 的 2 往下钻:把它加进 path。
path 接上 2:path → [2,2],进入下一层(从下标 3 继续,避免重复用同一个位置)。
收集第 6 个子集:每到一个节点先把当前 path 收进结果:[2,2]。
撤销下标 2 的 2:这条分支走完,撤销 2(path → [2]),回到上一层换下一个候选。
撤销下标 1 的 2:这条分支走完,撤销 2(path → []),回到上一层换下一个候选。
同层去重:跳过下标 2 的 2:下标 2 的 2 和同层前一个下标 1 的 2 相同,这一层已经从 2 开过分支了,再选会产生重复子集 → 画 ✗ 剪掉。
边界三连先过一遍。
同层去重是一类题的公共骨架。
参考代码
def subsetsWithDup(nums): nums.sort() # 排序: 相同元素相邻 res, path = [], [] def dfs(start): res.append(path[:]) # 每个节点都是一个子集 for i in range(start, len(nums)): if i > start and nums[i] == nums[i-1]: continue # 同层去重 path.append(nums[i]) dfs(i + 1) path.pop() # 回溯撤销 dfs(0) return res复杂度
- 时间:O(n · 2ⁿ),最多 2ⁿ 个子集,每个拷贝 O(n)
- 空间:O(n),递归栈 + path 深度 n(不计结果)
易错点
面试追问把动画讲成自己的话
追问和 LC78 子集(无重复)差在哪?
追问LC40 组合总和 II 也是这套吗?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
组合总和 II
LeetCode 40 · 中等 · 沿着 回溯 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题