对回溯算法的理解

  最近一直在刷leetcode的回溯算法的题目,想趁着熟悉记录一下自己的理解。

  回溯算法虽然是暴力搜索,但是其实是有方法的暴力搜索。目前做的题目包括组合总和I,II,全排列I,II,组合,子集I,II。其中组合总和I中

 

这里的特点就是数组中的值都可以反复选取。因此再画遍历树的时候,每次都会重新遍历。然而解集需要去重,因此需要减枝操作。

对于这种求和等于目标值的,首先要解决遍历时候的深度问题。所以技巧一就是在递归的时候,将target换成target-candidates[j]。如下所示

 1 private static void tracback(int len, int index, int[] candidates, int target, List<List<Integer>> ans, List<Integer> path) {
 2 
 3         if(target==0){
 4             ans.add(new ArrayList<>(path));
 5             return;
 6         }
 7 
 8         for (int j = index; j < len; j++) {
 9             if(target-candidates[j]<0){
10                 return;
11             }
12             path.add(candidates[j]);
13             tracback(len,j,candidates,target-candidates[j],ans,path);
14             path.remove(path.size()-1);
15         }
16     }

返回条件就是target==0即找到了路径。另外因为每次遍历到下一个深度的节点的时候,都是数组内的元素都有可能成为选择项,所以在递归的时候,j并不需要加1.。

接下来就是去重的问题,首先去重的前提就是对数组就行排序。

1 public static List<List<Integer>> combinationSum(int[] candidates, int target) {
2         List<List<Integer>> ans = new ArrayList<>();
3         List<Integer> path = new ArrayList<>();
4         Arrays.sort(candidates);
5         int len = candidates.length;
6         tracback(len,0,candidates,target,ans,path);
7         return ans;
8     }

当目标值减去当前节点的值的时候小于0的时候,就代表着不用进行遍历。另外需要注意的是第四行代码。直接ans.add(path)则结果会全部为[]。原因是因为最后每次都回溯了。所以当需要存储的时候,需要对其进行复制。

组合总和II

 

 这道题与前一道的区别也很明显。在这题中。主要难点是再大剪枝的情况下还有一个小剪枝。引用这位大佬的解释

 

 在排列问题中。

 

 这里我们的想法是在之前代码模板不变的情况下,我们加入了替换顺序来使每种情况都有

 1 public static void tracback(List<List<Integer>> ans, List<Integer> path, int[] nums,int depth) {
 2         if(path.size()==nums.length){
 3             ans.add(new ArrayList<>(path));
 4             return;
 5         }
 6 
 7         for (int i = depth; i < nums.length; i++) {
 8             path.add(nums[i]);
 9             int temp = nums[i];
10             nums[i] = nums[depth];
11 
12             nums[depth] = temp;
13             tracback(ans, path, nums,depth+1);
14             int temp1 = nums[i];
15             nums[i] = nums[depth];
16             nums[depth] = temp1;
17             path.remove(path.size()-1);
18         }
19     }

即变换前1-2-3.在回溯的时候还能满足1-2.

另外就是可以引用一个boolean类型的used数组来代表是不是已经使用过了。这样for循环的起点就一直是0了。

 1 // 在非叶子结点处,产生不同的分支,这一操作的语义是:在还未选择的数中依次选择一个元素作为下一个位置的元素,这显然得通过一个循环实现。
 2         for (int i = 0; i < len; i++) {
 3             if (!used[i]) {
 4                 path.add(nums[i]);
 5                 used[i] = true;
 6 
 7                 dfs(nums, len, depth + 1, path, used, res);
 8                 // 注意:下面这两行代码发生 「回溯」,回溯发生在从 深层结点 回到 浅层结点 的过程,代码在形式上和递归之前是对称的
 9                 used[i] = false;
10                 path.remove(path.size() - 1);
11             }
12         }
13 
14 作者:liweiwei1419
15 链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
16 来源:力扣(LeetCode)
17 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 在子集问题中

 

 需要注意的是这种没有重复值的需要在每个节点都存储结果,所以不需要返回。

 1  public static void tranback(int[] nums, List<Integer> path, List<List<Integer>> ans, int start,int i){
 2        // if (path.size() == i){
 3             ans.add(new ArrayList<>(path));
 4          //   return;
 5         //}
 6 
 7         for (int j = start; j < nums.length; j++) {
 8             path.add(nums[j]);
 9             tranback(nums,path,ans,j+1,i);
10             path.remove(path.size()-1);
11          //   tranback(nums,path,ans,j+1,i);
12         }
13     }

另外j+1便可以实现去重。对于子集II数组重复的情况下去重就按照数组总和II那样剪枝处理即可。

posted @ 2021-03-11 20:32  =凌晨=  阅读(117)  评论(0编辑  收藏  举报