刷题神器
往期回顾
>【回溯算法】|代码随想录算法训练营第21天|93. 复原 IP 地址、 78. 子集、90.子集II
题目
491.递增子序列
-
学后思路
递增子序列,注意无法进行排序,排序后影响结果集,去重方式依然是树层去重,但是去重得逻辑有所变化
解法1:
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums, 0);
return result;
}
private void backTracking(int[] nums, int startIndex){
if(path.size() >= 2)
result.add(new ArrayList<>(path));
HashSet<Integer> hs = new HashSet<>();
for(int i = startIndex; i < nums.length; i++){
if(!path.isEmpty() && path.get(path.size() -1 ) > nums[i] || hs.contains(nums[i]))
continue;
hs.add(nums[i]);
path.add(nums[i]);
backTracking(nums, i + 1);
path.remove(path.size() - 1);
}
}
}
- 题目总结
- 本地也是在节点收获结果,但是题目控制了结果长度
- 定义了set去重,在一层间进行去重
- 判断递增逻辑,加入节点是否大于path的最右边节点值
46.全排列
- 学后思路
全排列要注意和组合的区别,元素相同,顺序不同,算两个排列,结题思路和回溯三部曲类似
解法一:
class Solution {
List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
if (nums.length == 0){
return result;
}
used = new boolean[nums.length];
backtracking(nums);
return result;
}
private void backtracking (int[] nums){
if (path.size() == nums.length){
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++){
if (used[i]){
continue;
}
used[i] = true;
path.add(nums[i]);
backtracking(nums);
path.removeLast();
used[i] = false;
}
}
}
- 题目总结
- 注意每次循环都是从0开始
- 注意判断是否使用的精妙判断,使用used,然后continue
- 收获结果全部都在叶子节点,判断逻辑就是path.size()=numbers.size()
47.全排列 II
- 学后思路
全排列2的结题思路和全排列类似,因为有了重复元素的出现,所以在树层上要进行去重
解法一:
class Solution {
//存放结果
List<List<Integer>> result = new ArrayList<>();
//暂存结果
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
boolean[] used = new boolean[nums.length];
Arrays.fill(used, false);
Arrays.sort(nums);
backTrack(nums, used);
return result;
}
private void backTrack(int[] nums, boolean[] used) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// used[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过
// used[i - 1] == false,说明同⼀树层nums[i - 1]使⽤过
// 如果同⼀树层nums[i - 1]使⽤过则直接跳过
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
if(used[i] == true) continue;
//如果同⼀树⽀nums[i]没使⽤过开始处理
if (used[i] == false) {
used[i] = true;//标记同⼀树⽀nums[i]使⽤过,防止同一树枝重复使用
path.add(nums[i]);
backTrack(nums, used);
path.remove(path.size() - 1);//回溯,说明同⼀树层nums[i]使⽤过,防止下一树层重复
used[i] = false;//回溯
}
}
}
}
- 题目总结
- 题目排列和去重的一个组合体,要注意去重和跳过的逻辑
- 因为重复元素要去重,所以数组要优先进行排列
总结
回溯算法主要解决以下问题
- 组合问题:N个数里面按一定规则找出k个数的集合
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 棋盘问题:N皇后,解数独等等
解题思路主要是回溯三部曲
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
重点掌握组合、排列、切割、子集 中间有相似的解法,在去重上有相似的思路