【算法训练营Day22】回溯算法part4
文章目录
- 非递减子序列
- 全排列
- 全排列 II
- 总结:常见的去重方法
- 总结:回溯算法
非递减子序列
题目链接:491. 非递减子序列
解题逻辑:
该题是在前文子集 II的基础上加了两个条件和一个变化:
- 结果至少有两个元素。解决办法:收集结果时对单个结果集的长度进行判断
- 结果元素非递减。在递归时传入当前元素,这样下一层元素如果比这个元素小则直接跳过。
- 变化:去重的方法。在子集 II中使用的去重方法只能在数组有序的时候才能用,而在这个题数组并不有序,所以本题中去重要通过hash表。
解题代码如下:
class Solution {List<Integer> item = new ArrayList<>();List<List<Integer>> result = new ArrayList<>();public List<List<Integer>> findSubsequences(int[] nums) {backtracking(nums,0,Integer.MIN_VALUE);return result;}public void backtracking(int[] nums,int startIndex,int lastNum){if(startIndex == nums.length) return;Set<Integer> box = new HashSet<>();for(int i = startIndex;i < nums.length;i++) {if(nums[i] < lastNum || box.contains(nums[i])) continue;item.add(nums[i]);if(item.size() >= 2) result.add(new ArrayList(item));backtracking(nums,i + 1,nums[i]);item.remove(item.size() - 1);box.add(nums[i]);}}
}
全排列
题目链接:46. 全排列
解题思路:
与组合的不同之处在于:
- 无需偏移量startIndex来规定每层循环的起点
- 通过set来存放该层循环中已经使用过哪些元素(或者通过一个相同长度的数组来标记原数组中哪些元素被使用过)
解题代码:
class Solution {List<Integer> item = new ArrayList<>();List<List<Integer>> result = new ArrayList<>();public List<List<Integer>> permute(int[] nums) {backtracking(nums,new HashSet<Integer>());return result;}public void backtracking(int[] nums,Set<Integer> box){if(item.size() == nums.length) {result.add(new ArrayList(item));return;}for(int i = 0;i < nums.length;i++) {if(box.contains(nums[i])) continue;item.add(nums[i]);box.add(nums[i]);backtracking(nums,box);item.remove(item.size() - 1);box.remove(nums[i]);}}
}
全排列 II
题目链接:47. 全排列 II
这个题与上一题的区别在于本体的数字序列可以包含重复数字,所以最后的排列可能会存在重复。
与上一题解决方法的变化在于:
- 不能使用set去存储已使用过的元素,而是使用record数组对元素进行标记
- 去重方法沿用老方法:将数组排序,如果下一个遍历的元素没发生变化的话那么跳过循环
解题逻辑:
class Solution {List<Integer> item = new ArrayList<>();List<List<Integer>> result = new ArrayList<>();public List<List<Integer>> permuteUnique(int[] nums) {Arrays.sort(nums);backtracking(nums,new int[nums.length]);return result;}public void backtracking(int[] nums,int[] record){if(item.size() == nums.length) {result.add(new ArrayList(item));return;}int old = Integer.MIN_VALUE;for(int i = 0;i < nums.length;i++) {if(record[i] != 0 || old == nums[i]) continue;item.add(nums[i]);record[i] = 1;backtracking(nums,record);item.remove(item.size() - 1);record[i] = 0;old = nums[i];}}
}
总结:常见的去重方法
在回溯算法中常用到的两种去重方法:
- 处理方法1:在数组有序的情况下,如果下一个遍历的元素没发生变化的话那么跳过循环。
- 处理方式2:在数组无序的情况下,将已遍历的元素添加到set中进行记录,如果下一次循环该元素已经在set中,则跳过循环。
总结:回溯算法
回溯算法能解决如下问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 棋盘问题:N皇后,解数独等等
组合问题的核心就在于循环中引入偏移量startIndex,通过这个偏移量保证了只会向后取数不会折返。而排列问题就不能再使用偏移量,而是使用set(只能适用于无重复数据的数组)或者标记数组来确保每一个数都取了一遍。切割问题本质是组合问题的变种,只不过将递归的取数字变成了递归的切割。至于子集问题也可以当作组合问题的变种,只不过是收集结果的地方发生了变化。
最后要说的是只要是回溯问题,那么只要能将该问题抽象成树形进行思考,那么这一题已经做对了80%