回溯
注意
- 回溯是递归的副产品,只要有递归就会有回溯。
- 回溯法就是暴力搜索,遍历出所有可选列表。
- 每一道回溯法的题目都可以抽象为树形结构。
待做事项&&疑问
- 回溯题目常参考灵茶山艾府(以下简称
灵神)的代码,发现灵神常用五个参数,与代码框架中的不同,需要比对这其中的差距。
代码框架(必背)
注意,本文涉及到回溯的代码都会用该框架来写!!但不会创建像下面这么多的函数,因为只有一两行代码。
详见 《Hello 算法》:在二叉树中搜索所有值为 7 的节点,请返回根节点到这些节点的路径,并要求路径中不包含值为 3 的节点。
Shut up and calculate! -David Mermin && Feynman
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
boolean isSolution(List<TreeNode> state) { return !state.isEmpty() && state.get(state.size() - 1).val == 7; }
void recordSolution(List<TreeNode> state, List<List<TreeNode>> res) { res.add(new ArrayList<>(state)); }
boolean isValid(List<TreeNode> state, TreeNode choice) { return choice != null && choice.val != 3; }
void makeChoice(List<TreeNode> state, TreeNode choice) { state.add(choice); }
void undoChoice(List<TreeNode> state, TreeNode choice) { state.remove(state.size() - 1); }
void backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) { if (isSolution(state)) { recordSolution(state, res); } for (TreeNode choice : choices) { if (isValid(state, choice)) { makeChoice(state, choice); backtrack(state, Arrays.asList(choice.left, choice.right), res); undoChoice(state, choice); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| void backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) { if (!state.isEmpty() && state.get(state.size() - 1).val == 7) { res.add(new ArrayList<>(state)); } for (TreeNode choice : choices) { if (choice != null && choice.val != 3) { state.add(choice); backtrack(state, Arrays.asList(choice.left, choice.right), res); state.remove(state.size() - 1); } } }
|
共有七步:检查是否为解、记录解、遍历所有选择、剪枝(检查选择是否合法)、尝试(做出选择,更新状态)、进行下一轮选择、回退(撤销选择)
背诵口诀:简(检)记便(遍)笺(剪)常(尝)晋(进)会(回):简单记录在便笺上,常晋集团要开会
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
1 2 3 4 5 6 7 8 9 10
| 输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
|
示例 2:
1 2
| 输入:n = 1, k = 1 输出:[[1]]
|
提示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Solution { private List<List<Integer>> ans = new ArrayList<>(); public List<List<Integer>> combine(int n, int k) { getCombine(n, k, 1, new ArrayList<>()); return ans; } public void getCombine(int n, int k, int start, List<Integer> list) { if(k == 0) { ans.add(new ArrayList<>(list)); return; } for(int i = start;i <= n - k + 1;i++) { list.add(i); getCombine(n, k - 1, i+1, list); list.remove(list.size() - 1); } } }
|
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
1 2 3 4 5
| 输入: k = 3, n = 7 输出: [[1,2,4]] 解释: 1 + 2 + 4 = 7 没有其他符合的组合了。
|
示例 2:
1 2 3 4 5 6 7
| 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]] 解释: 1 + 2 + 6 = 9 1 + 3 + 5 = 9 2 + 3 + 4 = 9 没有其他符合的组合了。
|
示例 3:
1 2 3 4
| 输入: k = 4, n = 1 输出: [] 解释: 不存在有效的组合。 在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| class Solution { public List<List<Integer>> combinationSum3(int k, int n) { List<List<Integer>> ans = new ArrayList<>(); List<Integer> path = new ArrayList<>(k); dfs(9, n, k, ans, path); return ans; }
private void dfs(int i, int leftSum, int k, List<List<Integer>> ans, List<Integer> path) { int d = k - path.size(); if (leftSum == 0 && d == 0) { ans.add(new ArrayList<>(path)); return; } for (int j = i; j >= d; j--) { if (leftSum < 0 || leftSum > (i * 2 - d + 1) * d / 2) { return; } path.add(j); dfs(j - 1, leftSum - j, k, ans, path); path.removeLast(); } } }
class Solution { public List<List<Integer>> combinationSum3(int k, int n) { List<List<Integer>> ans = new ArrayList<>(); List<Integer> path = new ArrayList<>(k); dfs(9, n, k, ans, path); return ans; }
private void dfs(int i, int leftSum, int k, List<List<Integer>> ans, List<Integer> path) { int d = k - path.size();
if (leftSum < 0 || leftSum > (i * 2 - d + 1) * d / 2) { return; } if (d == 0) { ans.add(new ArrayList<>(path)); return; } for (int j = i; j >= d; j--) { path.add(j); dfs(j - 1, leftSum - j, k, ans, path); path.removeLast(); } } }
|
注意
这里的回溯代码,剪枝操作放在了检查是否为解之前;需要总结在什么情况下剪枝操作要提前?
剪枝操作中包含的参数影响检查是否为解的判断时要把剪枝操作提前
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
1 2
| 输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
|
示例 2:
1 2
| 输入:digits = "2" 输出:["a","b","c"]
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Solution { private static final String[] MAPPING = new String[]{"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) { int n = digits.length(); if (n == 0) { return List.of(); }
List<String> ans = new ArrayList<>(); char[] path = new char[n]; dfs(0, ans, path, digits.toCharArray()); return ans; }
private void dfs(int i, List<String> ans, char[] path, char[] digits) { if (i == digits.length) { ans.add(new String(path)); return; } String letters = MAPPING[digits[i] - '0']; for (char c : letters.toCharArray()) { path[i] = c; dfs(i + 1, ans, path, digits); } } }
|
注意
此处回溯代码代码不需要回退,因为采用的是固定数组,每次重新选择都会覆盖之前的值
使用 ArrayList(需要回退)
1 2 3 4 5 6 7 8 9 10 11
| 递归树: [] / | \ [a] [b] [c] / \ / \ / \ [ad][ae][bd][be][cd][ce]
从 [a] 到 [ad]:path = [a,d] 回溯到 [a] 时,如果不删除 'd':path = [a,d] 长度2 尝试 'e':path.add('e') → [a,d,e] 长度3 ❌ 错误!
|
使用固定数组(不需要回退)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| char[] path = new char[2];
递归树: dfs(0): path[0] = 'a' → path = ['a', _ ] dfs(1): path[1] = 'd' → path = ['a','d'] 得到 "ad" 回溯到 dfs(0): 不需要删除!因为 path[1] 的值 'd' 会被下一轮覆盖 dfs(0): path[0] = 'b' → path = ['b','d'] dfs(1): path[1] = 'd' → path = ['b','d'] 得到 "bd"
|
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
1 2 3 4 5 6
| 输入:candidates = [2,3,6,7], target = 7 输出:[[2,2,3],[7]] 解释: 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7 。 仅有这两种组合。
|
示例 2:
1 2
| 输入: candidates = [2,3,5], target = 8 输出: [[2,2,2,2],[2,3,3],[3,5]]
|
示例 3:
1 2
| 输入: candidates = [2], target = 1 输出: []
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Solution { public List<List<Integer>> combinationSum(int[] candidates, int target) { Arrays.sort(candidates); List<List<Integer>> ans = new ArrayList<>(); List<Integer> path = new ArrayList<>(); dfs(0, target, candidates, ans, path); return ans; }
private void dfs(int i, int left, int[] candidates, List<List<Integer>> ans, List<Integer> path) { if (left == 0) { ans.add(new ArrayList<>(path)); return; }
for (int j = i; j < candidates.length; j++) { if (candidates[j] <= left){ path.add(candidates[j]); dfs(j, left - candidates[j], candidates, ans, path); path.remove(path.size() - 1); } } } }
|
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
1 2 3 4 5 6 7 8
| 输入: candidates = [10,1,2,7,6,1,5], target = 8, 输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]
|
示例 2:
1 2 3 4 5 6
| 输入: candidates = [2,5,2,1,2], target = 5, 输出: [ [1,2,2], [5] ]
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Solution { public List<List<Integer>> combinationSum2(int[] candidates, int target) { Arrays.sort(candidates); List<List<Integer>> ans = new ArrayList<>(); List<Integer> path = new ArrayList<>(); dfs(0, target, candidates, ans, path); return ans; }
private void dfs(int i, int left, int[] candidates, List<List<Integer>> ans, List<Integer> path) { if (left == 0) { ans.add(new ArrayList<>(path)); return; }
for (int j = i; j < candidates.length && candidates[j] <= left; j++) { if (j > i && candidates[j] == candidates[j - 1]) { continue; } path.add(candidates[j]); dfs(j + 1, left - candidates[j], candidates, ans, path); path.remove(path.size() - 1); } } }
|
注意
与 组合总和 相比,这里的剪枝更为复杂,所以对遍历之中和尝试之前的逻辑进行了修改,本质上还是剪枝
给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
示例 1:
1 2
| 输入:s = "aab" 输出:[["a","a","b"],["aa","b"]]
|
示例 2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class Solution { public List<List<String>> partition(String s) { List<List<String>> ans = new ArrayList<>(); List<String> path = new ArrayList<>(); dfs(0, s, path, ans); return ans; }
private void dfs(int i, String s, List<String> path, List<List<String>> ans) { if (i == s.length()) { ans.add(new ArrayList<>(path)); return; } for (int j = i; j < s.length(); j++) { if (isPalindrome(s, i, j)) { path.add(s.substring(i, j + 1)); dfs(j + 1, s, path, ans); path.removeLast(); } } }
private boolean isPalindrome(String s, int left, int right) { while (left < right) { if (s.charAt(left++) != s.charAt(right--)) { return false; } } return true; } }
|
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
- 例如:
"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
示例 1:
1 2
| 输入:s = "25525511135" 输出:["255.255.11.135","255.255.111.35"]
|
示例 2:
1 2
| 输入:s = "0000" 输出:["0.0.0.0"]
|
示例 3:
1 2
| 输入:s = "101023" 输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| class Solution { public List<String> restoreIpAddresses(String s) { int n = s.length(); List<String> ans = new ArrayList<>(); String[] path = new String[4]; dfs(0, 0, s, s.length(), path, ans); return ans; }
private void dfs(int i, int j, String s, int n, String[] path, List<String> ans) { if (n - i < 4 - j || n - i > (4 - j) * 3) { return; }
if (i == n) { ans.add(String.join(".", path)); return; }
int ipVal = 0; for (int right = i; right < n; right++) { ipVal = ipVal * 10 + (s.charAt(right) - '0'); if (ipVal > 255 || ) { break; } path[j] = s.substring(i, right + 1); dfs(right + 1, j + 1, s, n, path, ans); if (ipVal == 0) { break; } } } }
|
注意
该题目涉及到的剪枝操作过多,在实际中如果没记住可不考虑做出
步骤图解
递归过程较长,只显示前面的内容
例子: s = "255255"
目标:分割成 4 个 0–255 之间的数字,且不能有前导零(除非本身就是 “0”)。
初始调用
1
| dfs(i=0, j=0, s="255255", n=6)
|
此时 n - i = 6,4 - j = 4,(4 - j) * 3 = 12,满足:
4 <= 6 <= 12 ✅
第一层 (j=0, 枚举第 1 段)
s = “255255”, i = 0 开始枚举 right
right = 0
子串 "2"
ipVal = 0*10 + 2 = 2
2 <= 255 ✅
path[0] = "2"
进入 dfs(1, 1)
进入 dfs(1, 1)
1 2 3
| n - i = 5(剩下 "55255") 4 - j = 3 条件:3 <= 5 <= 9 ✅
|
枚举子串 right 从 1 开始(因为 i=1)
right=1:"5"
ipVal = 5 ✅
path[1] = "5"
进入 dfs(2, 2)
进入 dfs(2, 2)
1 2 3
| n - i = 4(剩下 "5255") 4 - j = 2 条件:2 <= 4 <= 6 ✅
|
枚举子串 right 从 2 开始
right=2:"5"
ipVal = 5 ✅
path[2] = "5"
进入 dfs(3, 3)
进入 dfs(3, 3)
1 2 3
| n - i = 3(剩下 "255") 4 - j = 1 条件:1 <= 3 <= 3 ✅
|
枚举子串 right 从 3 开始
right=3:"2"
ipVal = 2 ✅
path[3] = "2"
进入 dfs(4, 4)
进入 dfs(4, 4)
1 2 3 4
| i = 4,n = 6 n - i = 2 4 - j = 0 条件:0 <= 2 <= 0 ❌ (2 > 0)
|
直接 return
所以 right=3 这条路失败。
回溯到 dfs(3, 3) 继续循环
right=4:子串 "25"(i=3 到 4)
ipVal = 2*10 + 5 = 25 ✅
path[3] = "25"(覆盖掉之前的 "2")
进入 dfs(5, 4)
进入 dfs(5, 4)
1 2 3
| n - i = 1 4 - j = 0 条件:0 <= 1 <= 0 ❌
|
失败。
right=5:子串 "255"
ipVal = 2*10 + 5 = 25 → 25*10 + 5 = 255 ✅
path[3] = "255"
进入 dfs(6, 4)
进入 dfs(6, 4)
1 2
| i == n(6 == 6) ans.add(String.join(".", path))
|
1 2
| path` 当前内容: `["2", "5", "5", "255"]
|
组成 IP:2.5.5.255 ✅
回到 dfs(2,2) 继续
此时 dfs(2,2) 里的循环 right 继续:
之前 right=2 已经试过("5"),
现在 right=3:子串 "52"(i=2 到 3)
ipVal = 5*10 + 2 = 52 ✅
path[2] = "52"
进入 dfs(4, 3)
dfs(4, 3)
1 2 3
| n - i = 2(剩下 "55") 4 - j = 1 条件:1 <= 2 <= 3 ✅
|
枚举子串 right 从 4 开始
right=4:"5"
ipVal = 5 ✅
path[3] = "5"
进入 dfs(5, 4) → 失败
right=5:"55"
ipVal = 5*10 + 5 = 55 ✅
path[3] = "55"
进入 dfs(6, 4) → 成功
添加 IP:["2", "5", "52", "55"] → 2.5.52.55 ✅
继续回溯到 dfs(2,2),right=4
子串 "525"(i=2 到 4)
ipVal = 5*10 + 2 = 52 → 52*10 + 5 = 525
525 > 255 ❌ break
后续过程省略…
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
1 2
| 输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
|
示例 2:
1 2
| 输入:nums = [0] 输出:[[],[0]]
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Solution { private List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) { int n = nums.length; getCombine(0, n, nums, new ArrayList<>()); return ans; }
public void getCombine(int start, int n, int[] nums, List<Integer> list) { ans.add(new ArrayList<>(list)); for (int i = start; i < n; i++) { list.add(nums[i]); getCombine(i + 1, n, nums, list); list.remove(list.size() - 1); } } }
|
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
1 2
| 输入:nums = [1,2,2] 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
|
示例 2:
1 2
| 输入:nums = [0] 输出:[[],[0]]
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Solution { private List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) { Arrays.sort(nums); int n = nums.length; getCombine(0, n, nums, new ArrayList<>()); return ans; }
public void getCombine(int start, int n, int[] nums, List<Integer> list) { ans.add(new ArrayList<>(list)); for (int i = start; i < n; i++) { if (i > start && nums[i] == nums[i - 1]) { continue; } list.add(nums[i]); getCombine(i + 1, n, nums, list); list.remove(list.size() - 1); } }
}
|
注意
这里的剪枝条件 i > start确保了只有当具有相同值的数不处于同一层,才能纳入选择:如示例1,在start为0时,比较后两个2的值,只取其中一个2,而当start为1时,已经取得了[1,2],这时可以将最后一个2纳入其中。
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
1 2
| 输入:nums = [4,6,7,7] 输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
|
示例 2:
1 2
| 输入:nums = [4,4,3,2,1] 输出:[[4,4]]
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class Solution { private List<List<Integer>> ans = new ArrayList<>(); public List<List<Integer>> findSubsequences(int[] nums) { dfs(0, nums, new ArrayList<>()); return ans; } private void dfs(int start, int[] nums, List<Integer> path) { if (path.size() >= 2) { ans.add(new ArrayList<>(path)); } Set<Integer> used = new HashSet<>(); for (int i = start; i < nums.length; i++) { if (!path.isEmpty() && nums[i] < path.get(path.size() - 1)) { continue; } if (used.contains(nums[i])) { continue; } used.add(nums[i]); path.add(nums[i]); dfs(i + 1, nums, path); path.remove(path.size() - 1); } } }
|
注意
涉及到的剪枝条件过多,需要分成两部分
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
1 2
| 输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
|
示例 2:
1 2
| 输入:nums = [0,1] 输出:[[0,1],[1,0]]
|
示例 3:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Solution { public List<List<Integer>> permute(int[] nums) { int n = nums.length; List<Integer> path = Arrays.asList(new Integer[n]); boolean[] onPath = new boolean[n]; List<List<Integer>> ans = new ArrayList<>();
dfs(0, nums, ans, path, onPath); return ans; }
private void dfs(int i, int[] nums, List<List<Integer>> ans, List<Integer> path, boolean[] onPath) { if (i == nums.length) { ans.add(new ArrayList<>(path)); return; }
for (int j = 0; j < nums.length; j++) { if (!onPath[j]) { path.set(i, nums[j]); onPath[j] = true; dfs(i + 1, nums, ans, path, onPath); onPath[j] = false; } } } }
|
步骤图解
1. 初始状态
nums = [1, 2, 3]
n = 3
path = [_, _, _] (长度为 3,初始占位)
onPath = [false, false, false]
ans = []
调用 dfs(0, ...)。
2. 递归树展开
2.1 第一层:i = 0
循环 j = 0,1,2,尝试填充 path[0]。
j = 0
path[0] = 1, onPath[0] = true
- 调用
dfs(1, ...)
2.2 第二层:i = 1
当前 path: [1, _, _]
onPath: [T, F, F]
循环 j = 0,1,2:跳过 j=0(已用)。
j = 1
path[1] = 2, onPath[1] = true
- 调用
dfs(2, ...)
2.3 第三层:i = 2
当前 path: [1, 2, _]
onPath: [T, T, F]
循环 j = 0,1,2:跳过 j=0,1。
j = 2
path[2] = 3, onPath[2] = true
- 调用
dfs(3, ...)
3.1 第四层:i = 3(叶子)
- i == n(3),
ans.add([1,2,3])
- 返回上一层
3.2 回到第三层:i = 2,j = 2 执行完成
- 恢复现场:
onPath[2] = false
- 循环结束,返回上一层
2.4 回到第二层:i = 1,j = 1 执行完成
恢复现场:onPath[1] = false
继续循环 j=2。
j = 2(在 i=1 层)
path[1] = 3, onPath[2] = true
- 调用
dfs(2, ...)
第三层:i = 2
当前 path: [1, 3, _]
onPath: [T, F, T]
循环 j = 0,1,2:跳过 j=0,2。
j = 1
path[2] = 2, onPath[1] = true
- 调用
dfs(3, ...)
叶子
i=3 → ans 添加 [1, 3, 2]
返回上一级,恢复 onPath[1] = false。
i=2 层循环结束,返回上一层。
回到第二层 i=1,j=2 结束
恢复 onPath[2] = false。
第二层循环结束,返回第一层。
2.5 回到第一层 i=0,j=0 结束
恢复 onPath[0] = false。
2.6 第一层,i=0,j=1
1
| path[0] = 2`, `onPath[1] = true
|
- 继续递归,过程类似,生成以 2 开头的排列
[2, 1, 3] 和 [2, 3, 1]
2.7 第一层,i=0,j=2
1
| path[0] = 3`, `onPath[2] = true
|
- 生成以 3 开头的排列
[3, 1, 2] 和 [3, 2, 1]
3. 最终 ans 收集的排列顺序
(按递归深度优先遍历生成顺序):
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
1 2 3 4 5
| 输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]
|
示例 2:
1 2
| 输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class Solution { public List<List<Integer>> permuteUnique(int[] nums) { Arrays.sort(nums);
int n = nums.length; List<Integer> path = Arrays.asList(new Integer[n]); boolean[] onPath = new boolean[n]; List<List<Integer>> ans = new ArrayList<>();
dfs(0, nums, path, onPath, ans); return ans; }
private void dfs(int i, int[] nums, List<Integer> path, boolean[] onPath, List<List<Integer>> ans) { if (i == nums.length) { ans.add(new ArrayList<>(path)); return; }
for (int j = 0; j < nums.length; j++) { if (onPath[j] || (j > 0 && nums[j] == nums[j - 1] && !onPath[j - 1]) ) { continue; } path.set(i, nums[j]); onPath[j] = true; dfs(i + 1, nums, path, onPath, ans); onPath[j] = false; } } }
|
注意
- 去重问题需要排列。
- 该题的复杂度已超过了临场理解的范畴,需要结合 90. 子集 II 和 46. 全排列 来理解;实战中不要求做出。