题目
- 46. 全排列(中等)
- 47. 全排列 II(中等):思考为什么造成了重复,如何在搜索之前就判断这一支会产生重复;
- 39. 组合总和(中等)
- 40. 组合总和 II(中等)
- 77. 组合(中等)
- 78. 子集(中等)
- 90. 子集 II(中等):剪枝技巧同 47 题、39 题、40 题;
- 60. 第 k 个排列(中等):利用了剪枝的思想,减去了大量枝叶,直接来到需要的叶子结点;
- 93. 复原 IP 地址(中等)
正文
1、 46. 全排列
给定一个不含重复数字的数组
nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque<>();
boolean used[] = new boolean[nums.length];
dfs(res, path, nums, used);
return res;
}
private void dfs(List<List<Integer>> res, Deque<Integer> path, int[] nums, boolean[] used) {
if (nums.length == path.size()) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i])
continue;
used[i] = true;
path.addFirst(nums[i]);
dfs(res, path, nums, used);
used[i] = false;
path.removeFirst();
}
}
2、47. 全排列 II
给定一个可包含重复数字的序列
nums
,按任意顺序 返回所有不重复的全排列。
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
int len = nums.length;
if (len == 0)
return res;
boolean[] used = new boolean[len];
Arrays.sort(nums);
dfs(res, new ArrayDeque<>(), nums, used);
return res;
}
private void dfs(List<List<Integer>> res, Deque<Integer> path, int[] nums, boolean used[]) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i])
continue;
if (i > 0 && !used[i - 1] && nums[i] == nums[i - 1])
continue;
path.push(nums[i]);
used[i] = true;
dfs(res, path, nums, used);
used[i] = false;
path.poll();
}
}
我们对比46、47两道题目,发现题目的区别在于,题47中会出现重复数字,假设46就是朴素回溯的话,那么47就是需要加上判断条件的回溯。
所以,问题就在于,怎么样子在回溯过程中加条件,可以让重复数字不被使用,即【对于数组[1,1’,2]而言,[1,1’]和[1’,1]只有前者可以选取成功。】
关键代码在于i > 0 && !used[i - 1] && nums[i] == nums[i - 1]
,此处使用了反向思维,即所有重复数字序列[…,a,b,…],只有a已经在path中,才能使用b。
3、39. 组合总和
给你一个 无重复元素 的整数数组
candidates
和一个目标整数target
,找出candidates
中可以使数字和为目标数target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为target
的不同组合数少于150
个。
public List<List<Integer>> combinationSum(int[] c, int target) {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque<>();
Arrays.sort(c);
dfs(res, path, c, target, 0);
return res;
}
private void dfs(List<List<Integer>> res, Deque<Integer> path, int[] c, int curSum, int start) {
if (curSum < 0) {
// 没有解了
return;
}
if (curSum == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i < c.length; i++) {
curSum -= c[i];
path.push(c[i]);
dfs(res, path, c, curSum, i);
path.poll();
curSum += c[i];
}
}
问题:这个start起到了什么作用?
什么时候使用 used 数组,什么时候使用 begin 变量?
有些朋友可能会疑惑什么时候使用 used 数组,什么时候使用 begin 变量。这里为大家简单总结一下:
排列问题,讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为不同列表时),需要记录哪些数字已经使用过,此时用 used 数组;
组合问题,不讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为相同列表时),需要按照某种顺序搜索,此时使用 begin 变量。
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/combination-sum/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-m-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
4、40. 组合总和 II
给定一个候选人编号的集合
candidates
和一个目标数target
,找出candidates
中所有可以使数字和为target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
**注意:**解集不能包含重复的组合。
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList();
int len = candidates.length;
Deque<Integer> path = new ArrayDeque<>();
Arrays.sort(candidates);
dfs(res, candidates, path, target, 0);
return res;
}
private void dfs(List<List<Integer>> res, int[] arr, Deque<Integer> path, int curSum, int start) {
if (curSum < 0)
return;
if (curSum == 0) {
res.add(new ArrayList(path));
return;
}
for (int i = start; i < arr.length; i++) {
if (i > start && arr[i] == arr[i - 1])
continue;
curSum -= arr[i];
path.push(arr[i]);
dfs(res, arr, path, curSum, i + 1);
path.poll();
curSum += arr[i];
}
}
}
不能选重复数字,加一层ab相同判断即可
5、77. 组合
给定两个整数
n
和k
,返回范围[1, n]
中所有可能的k
个数的组合。
你可以按 任何顺序 返回答案。
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque();
dfs(res, path, n, k, 1);
return res;
}
private void dfs(List<List<Integer>> res, Deque<Integer> path, int n, int k, int start) {
if (path.size() == k)
res.add(new ArrayList(path));
for (int i = start; i <= n; i++) {
path.push(i);
dfs(res, path, n, k, i + 1);
path.poll();
}
}
6、78. 子集
给你一个整数数组
nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque<>();
dfs(res, path, 0, nums);
return res;
}
private void dfs(List<List<Integer>> res, Deque<Integer> path, int start, int arr[]) {
res.add(new ArrayList(path));
for (int i = start; i < arr.length; i++) {
path.push(arr[i]);
dfs(res, path, i + 1, arr);
path.poll();
}
}
怎样选出边界条件?
不用选,可以发现,这个子集记录的就是回溯的过程,直接放入容器即可
7、90. 子集 II
给你一个整数数组
nums
,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Deque<Integer> path = new ArrayDeque<>();
Arrays.sort(nums);
dfs(res, path, 0, nums);
return res;
}
private void dfs(List<List<Integer>> res, Deque<Integer> path, int start, int arr[]) {
res.add(new ArrayList(path));
for (int i = start; i < arr.length; i++) {
if (i > start && arr[i] == arr[i - 1])
continue;
path.push(arr[i]);
dfs(res, path, i + 1, arr);
path.poll();
}
}
8、60. 排列序列
给出集合
[1,2,3,...,n]
,其所有元素共有n!
种排列。
按大小顺序列出所有排列情况,并一一标记,当n = 3
时, 所有排列如下:
"123"
"132"
"213"
"231"
"312"
"321"
给定n
和k
,返回第k
个排列。
public String getPermutation(int n, int k) {
int[] factorial = new int[n + 1];
factorial[0] = 1;
for (int i = 1; i <= n; i++) {
factorial[i] = i * factorial[i - 1];
}
List<Integer> numList = new ArrayList<>();
for (int i = 1; i <= n; i++)
numList.add(i);
StringBuilder res = new StringBuilder();
k--;
for (int i = n - 1; i >= 0; i--) {
int index = k / factorial[i];
res.append(numList.remove(index));
k -= index * factorial[i];
}
return res.toString();
}
其实是数学题。
9、93. 复原 IP 地址
有效 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
中的任何数字。你可以按 任何 顺序返回答案。
public List<String> restoreIpAddresses(String s) {
List<String> res = new ArrayList();
if (s.length() < 4 || s.length() > 12)
return res;
int len = s.length();
Deque<String> path = new ArrayDeque();
dfs(s, res, 0, 0, path, len);
return res;
}
private int validateIP(String s, int left, int right) {
int len = right - left + 1;
if (len > 1 && s.charAt(left) == '0')
return -1;
if (right >= s.length())
return -1;
int res = 0;
for (int i = left; i <= right; i++) {
res = res * 10 + s.charAt(i) - '0';
}
if (res > 255)
return -1;
return res;
}
private void dfs(String s, List<String> res, int begin, int splitTimes, Deque<String> path, int len) {
if (begin == len) {
if (splitTimes == 4) {
res.add(String.join(".", path));
}
return;
}
if (len - begin > (4 - splitTimes) * 3 || len - begin < 4 - splitTimes)
return;
for (int i = 0; i < 3; i++) {
int cur = validateIP(s, begin, begin + i);
if (cur == -1)
break;
path.addLast(cur + "");
splitTimes++;
dfs(s, res, begin + i + 1, splitTimes, path, len);
path.removeLast();
splitTimes--;
}
}
算是系列里面较为复杂的题目,之前固有的模型是不不好做的。
思路:
1、如何回溯:从0到2,作为从当前位置开始选取数字的step
2、判断条件:多个数字不能有前导0
3、剪枝:当后面的数字不够或者过多的时候
4、模拟结构问题,使用Deque的数据结构,可以直接用String.join(".",path)
拼接
总结
回溯题掌握:
1、排列OR组合?
排列需要记住数字是否使用过【used[i]】
组合需要保留回溯的位置
2、是否需要排除重复?
中途添加i > 0 && arr[i] > arr[i - 1]
的判断