回溯法总结

约定不等于承诺〃 2024-04-03 09:50 182阅读 0赞

回溯法分为组合,子集,排列等问题,下面分别就这几个问题进行总结.

1.总体架构

总体来说,回溯法像是在遍历一棵树,而这棵树的深度,由回溯的终止条件以及for循环内部的变量控制组成

  1. LinkedList<Integer> temp = new LinkedList<>();//暂存数据的链表
  2. List<List<Integer>> re = new ArrayList<>();//待返回的结果集
  3. public void backtracing(int[] nums, int startIndex, int[] used){
  4. //三个参数分别表示 nums待遍历的数组,通常是题目中给出的数组
  5. //startIndex遍历的开始位置,并不一定是必须的,取决于题目类型(如果是一个数组的组合问题则一般需
  6. //要)
  7. //used 则用于父子结点间去重,在同父亲结点间的去重则在本函数内部定义一个hashMap或者used数组
  8. if(){
  9. //此处填写的是满足遍历结束的条件,通常为temp数组(或者linkedList)的长度满足给定的条件
  10. //将满足条件的temp加入到re中
  11. re.add(new ArrayList(temp));
  12. //在添加结束后是否是使用return则需要根据题目的条件判断:
  13. //如果是全排列,组合问题这种,也就是说都已经遍历到这个树的叶子结点的这种问题,应当采用return
  14. //而若是子序列问题,比如寻找递增子序列(力扣491),整棵树还需要继续向下进行遍历,则不需要return
  15. }
  16. //当然如果我们为了达到在同父结点的层内去重的目的,要在此处建立一个HashMap,或者used数组,
  17. //不过需要注意的是,如果再这里建立的uesed数组,他的意义和传参进来的used数组并不一样,传参进来的
  18. //used数组是用来标识数组中的下标,也就是说,数组中的数字按顺序来看是否被使用,而此处则是标记同
  19. 样的
  20. //数字是否被使用.所以used的数组的范围应该是nums数组中数字的范围,而不是nums数组的长度,传参进
  21. //来的used的范围是nums数组长度(目的是为了一对一表示数组中的元素)此处的给出示例:力扣46题,以及
  22. //自己写的力扣47题的used数组,(自己写的放在了力扣刷题专栏中)
  23. HashMAP<Integer,Integer> map = new HashMap<>();//本层去重(如果需要)
  24. //这里需要判断map中的key和value的相对关系了
  25. if(map.getOrDefault(nums[i],0) >= 1){
  26. //这里满足当前条件则说明在本层,这个数字已经出现过一次了
  27. continue;//如果满足条件的话,且需要去重
  28. }
  29. for(int i = ?; i < nums.length; i++){
  30. //在此处,i的起始值是一个问号,需要根据是否使用startIndex来进行修改,如果使用startIndex
  31. //那么i的值为startIdex,否则为0
  32. //对于是否使用startIndex给出示例: 对于[1,2,3]这个数组来说,如果求的是子序列问题,或者组合
  33. //问题,也就是说只能出现[1,2],[1,3]这种按照原始的相对顺序出现,则需要startIndex,若求的是
  34. //排列问题,也就是说要求[3,2,1]这种情况出现,则不能使用startIndex,因为要是使用了,则 3 对
  35. //应着最后的位置,没办法再向前遍历到1,2的位置
  36. if(添加一些判断条件,比如要求递增序列){
  37. }
  38. temp.add(nums[i]);
  39. map.put(nums[i],map.getOrDefault(nums[i],0) + 1);
  40. //!!!!!!
  41. //注意这个map不回溯!因为层间去重,回溯就没意义了!!!!!!!!!
  42. used[i] = 1 ;//将数组的当前位置记为使用过的形式,这种遍历方式是会让子元素不再使用
  43. //解决的问题主要是当出现重复的数字的时候,不超过重复次数的使用重复数字
  44. backtracing(nums, i + 1, used);//注意此处若采用了startIndex则向下一层应该是i+1,而不
  45. //是startIndex+1
  46. temp.remove(temp.size() - 1);//撤销修改
  47. used[i] = 0;
  48. }
  49. }

2.组合

力扣

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母

  1. class Solution {
  2. List<String> re = new ArrayList<>();
  3. public List<String> letterCombinations(String digits) {
  4. if (digits == null || digits.length() == 0) {
  5. return re;
  6. }
  7. //初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串""
  8. String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
  9. //迭代处理
  10. backTracking(digits, numString, 0);
  11. return re;
  12. }
  13. StringBuilder t = new StringBuilder();
  14. public void backTracking(String digits, String[]numString, int cur){
  15. if(cur == digits.length()){
  16. re.add(t.toString());
  17. return;
  18. }
  19. String str = numString[digits.charAt(cur) - '0'];
  20. for(int i = 0; i < str.length(); i++){
  21. t.append(str.charAt(i));
  22. backTracking(digits,numString,cur+1);
  23. t.deleteCharAt(t.length()-1);
  24. }
  25. }
  26. }

3.子序列问题

此处的子序列问题使用了层间去重的操作

层间去重使用了两个版本的方法,分别为HashMap以及used数组,注意此处的used数组和排列问题的used数组并不一样,无论从意义和大小来说.

491. 递增子序列

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

  1. 输入:nums = [4,6,7,7]
  2. 输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
  3. class Solution {
  4. LinkedList<Integer> li = new LinkedList<>();
  5. List<List<Integer>> re = new ArrayList<>();
  6. public List<List<Integer>> findSubsequences(int[] nums) {
  7. get(nums, 0);
  8. return re;
  9. }
  10. public void get(int[] nums, int startIndex){
  11. if(li.size() >= 2){
  12. re.add(new ArrayList(li));
  13. // return;
  14. }
  15. HashMap<Integer, Integer> map = new HashMap<>();
  16. for(int i = startIndex; i < nums.length; i++){
  17. if (li.size() > 0 && nums[i] < li.getLast()){
  18. continue;
  19. }
  20. if (map.getOrDefault(nums[i], 0) >= 1){
  21. continue;//说明使用过了
  22. }
  23. li.add(nums[i]);
  24. map.put(nums[i],map.getOrDefault(nums[i],0) + 1);
  25. get(nums, i + 1);
  26. li.remove(li.size() - 1);
  27. }
  28. }
  29. }
  30. class Solution {
  31. private List<Integer> path = new ArrayList<>();
  32. private List<List<Integer>> res = new ArrayList<>();
  33. public List<List<Integer>> findSubsequences(int[] nums) {
  34. backtracking(nums,0);
  35. return res;
  36. }
  37. private void backtracking (int[] nums, int start) {
  38. if (path.size() > 1) {
  39. res.add(new ArrayList<>(path));
  40. }
  41. int[] used = new int[201];
  42. for (int i = start; i < nums.length; i++) {
  43. if (!path.isEmpty() && nums[i] < path.get(path.size() - 1) ||
  44. (used[nums[i] + 100] == 1)) continue;
  45. used[nums[i] + 100] = 1;
  46. path.add(nums[i]);
  47. backtracking(nums, i + 1);
  48. path.remove(path.size() - 1);
  49. }
  50. }
  51. }

4.排列问题

47. 全排列 II

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

  1. 输入:nums = [1,1,2]
  2. 输出:
  3. [[1,1,2],
  4. [1,2,1],
  5. [2,1,1]]
  6. 输入:nums = [1,2,3]
  7. 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

自己写的代码段如下

  1. class Solution {
  2. LinkedList<Integer> temp = new LinkedList<>();
  3. List<List<Integer>> re = new ArrayList<>();
  4. public List<List<Integer>> permuteUnique(int[] nums) {
  5. int[] used = new int[nums.length];
  6. get(nums, used);
  7. return re;
  8. }
  9. public void get(int[] nums, int[] used){
  10. if(temp.size() == nums.length){
  11. re.add(new ArrayList(temp));
  12. return;
  13. }
  14. HashMap<Integer, Integer> map = new HashMap<>();
  15. for(int i = 0; i < nums.length; i++){
  16. //优先判断是否是使用过的数字used数组(因为如果先判断了是否是层间去重,可能直接就跳过了)
  17. //如果使用过则continue而不是return,因为continue就直接跳过向下一层的循环了
  18. if(used[i] == 1 || map.getOrDefault(nums[i],0) >= 1){//层间重复的数字,跳过
  19. continue;
  20. }
  21. used[i] = 1;
  22. temp.add(nums[i]);
  23. map.put(nums[i], map.getOrDefault(nums[i],0) + 1);
  24. get(nums,used);
  25. used[i] = 0;//回溯
  26. temp.remove(temp.size() - 1);
  27. // map不回溯
  28. }
  29. }
  30. }

解释:hashMap负责层间去重,used负责重复元素的使用次数不超过重复次数~

以1 1 3为例子画图说明

d1faa858370e447bbbf9a41c3f8c6ae8.png

5.回溯代码是否设置返回值的问题:

如果是要寻找出全部的路径则不用添加返回值,但是要是只需要找到一条路径,就应当设置返回值.类比树的返回值设置.

1.如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回

2.如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。

3.如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。

带返回值的回溯法举例如下:

332. 重新安排行程

题目描述:

9a78ff9133534f1d8ce93f9591ad0ee3.png

对应的代码如下:

  1. class Solution {
  2. private LinkedList<String> re ;//在回溯过程中如果符合要求就直接返回
  3. private LinkedList<String> path = new LinkedList<>();
  4. public List<String> findItinerary(List<List<String>> tickets) {
  5. //按照第二个元素的升序排列
  6. Collections.sort(tickets, (a,b)->(a.get(1).compareTo(b.get(1))));
  7. path.add("JFK");
  8. boolean[] used = new boolean[tickets.size()];
  9. backtracing((ArrayList)tickets, path, used);
  10. return re;
  11. }
  12. public boolean backtracing(ArrayList<List<String>> tickets, LinkedList path, boolean[] used){
  13. if(path.size() == tickets.size() + 1){
  14. re = new LinkedList(path);
  15. return true;
  16. }
  17. for(int i = 0 ; i < tickets.size(); i++){
  18. if(!used[i] && tickets.get(i).get(0).equals(path.getLast())){
  19. path.add(tickets.get(i).get(1));//没被使用过
  20. used[i] = true;
  21. if(backtracing(tickets, path, used)){
  22. return true;
  23. }
  24. used[i] = false;
  25. path.remove(path.size()-1);
  26. }
  27. }
  28. return false;
  29. }
  30. }

发表评论

表情:
评论列表 (有 0 条评论,182人围观)

还没有评论,来说两句吧...

相关阅读

    相关 回溯总结

    回溯法分为组合,子集,排列等问题,下面分别就这几个问题进行总结. 1.总体架构 总体来说,回溯法像是在遍历一棵树,而这棵树的深度,由回溯的终止条件以及for循环内部的变量控

    相关 回溯

    一、回溯法的基本思想   在问题的解空间树中,按深度优先策略,从根节点出发搜素解空间树。算法搜素至解空间树的任一结点时,先判断该结点是否包含问题的解,如果肯定不包含,则跳

    相关 回溯专题

    回溯法 全排列问题 N皇后问题 枚举,排列,组合问题都可以用回溯法来求解,它也是一个通用的求解问题的算法 全排列问题 比如给你数组1,2,3,

    相关 Leetcdoe刷题java之回溯总结

    刚开始的时候感觉回溯法很难,但是慢慢做下来,都是一个套路,并不是特别难。 回溯法,简单来说就是遍历所有排列组合,到头了再退回来。 先给一个回溯法框架,以及一道题来感觉一下:

    相关 回溯解决全排列问题总结

    1、了解全排列和回溯 所谓全排列就是从n个元素中取出n个元素按照一定的顺序进行排列,所有的排列情况叫做全排列。 这n个元素又分为两种情况,一种是n个元素存在重复元素,一

    相关 回溯算法(试探

    算法思路 基本思想: 为了求得问题的解,先选择某一种可能情况进行试探,在试探过程中,一旦发现原来选择的假设情况是错误的,就退回一步重新选择,继续向另一个方向试