回溯算法-组合

原题

https://leetcode.cn/problems/combinations/
回溯算法-组合_第1张图片

思路

回溯算法实际上就是深度优先算法的一种。也就是说,当我们在使用一个深度优先算法(一般用递归)的时候,有时候操作完一个步骤需要回退到上一步,这就叫回溯。

那么,既然回溯就是dfs的一种,我们前面也讲过dfs的步骤,最重要的一步就是一次操作。首先,这道题的是想要求组合。那么假设k=2时,是不是表示我们从数字中选取两次,记录所有的结果,那么其实所谓的一次操作其实就是选取一个数字,并且,我们知道组合【1,2】和组合【2,1】是一样的(看做一个),所以说,当我们选择了【1,2】之后,我们在第一次选择【2】的时候,第二次就不能选择【1】了。那么我们既然已经知道了单次操作是啥,是不是只需要递归就好了。(回溯等后面来讲)。
回溯算法-组合_第2张图片
分析的过程正如上面这张图,因为k等于2,也就是说我们每个小集合需要选取两次。第一次操作时,我们可以选择任何一个数【1 2 3 4】,第二次操作时,我们要去除掉前面已经选过的,防止【1 2】和【2 1】这种情况;也要去除掉上一次(k=1)时选取的,即防止集合变成【1 1】这种。

好的,以上这些内容和我前面写的深度优选算法(递归)是一样的(其实回溯算法一般来说,本质也是在递归的基础上实现的)。

那么我们为什么要使用回溯呢?
这里考虑一个问题。首先,我们因为要存储子集对吧,所以我们一开始要定义一个大的集合存储所有的子集,其次要定义一个子集,用于存储一次选择,例如【1,2】。那么我们思考一下上面递归的过程。这里一定要结合上面的图,才能体会到回溯的精髓。

  1. 一开始我们的子集是空的。【】
  2. 第一次调用递归函数,选择一个1,此时的临时子集为【1】
  3. 接着,再次调用递归函数(即第二次选择数字),选择一个2,此时的集合为【1 2】
  4. 好了,此时我们选择了了两次(因为k=2,所以已经选完了),我们将子集装入大集合中。
  5. 大集合拥有了一个子集【1 2】,那么此时函数执行完毕,回到上一次,也就是{2 3 4}给你选。
  6. 那么我们这时候如果没有回溯,是不是临时子集目前还是【1 2】?答案是是的
  7. 这里很多新人就会想那我能不能把临时子集清空呢?其实完全没必要,因为临时子集前面的1是我们已经选了的,我们回退到第二层,此时的1是有意义的,那么我们只需要回退掉2就好了(也就是回退掉一次操作,一般都是递归一次就得回退一次)。
  8. 那么当我们把【1 2】回退到【1】的时候,我们就可以从{ 2 3 4 }中选择,由于2已经选过了,也就是说接下来是选择3,子集变成【1 3】.
  9. 上面的过程实际上就是深度优先算法(递归),只是我们在每次递归的时候都需要一次【回退】,也就是【回溯】。
  10. 而回溯在代码中的体现也非常的接地气,无非就是我们在递归前添加了元素,递归后将最后一个元素删除而已。操作不难,主要是理解思想。

代码

class Solution {
    List<List<Integer>> all = new ArrayList<>();
    List<Integer> part = new ArrayList<>();
    public List<List<Integer>> combine(int n, int k) {
        tCombine(n,k,0,new ArrayList());
        return all;
    }
    public void tCombine(int n,int k,int index,List<Integer> part) {
        if(k == 0){
            all.add(new ArrayList(part));
        }
        if(index == n){
            return;
        }
        for(int i = index;i < n;i++){
            part.add(i+1);
            tCombine(n,k-1,i+1,part);
            part.remove(part.size()-1);
        }
    }
}

回溯算法-组合_第3张图片

补充

这道题我们说了【回溯】,但是这道题的还可以进一步优化,即通过【剪枝】来减少递归次数。后面会讲。

你可能感兴趣的:(执章学长-数据结构与算法,算法,深度优先,leetcode)