细说递归+回溯:通过构造可能结果的树来求解算法问题(Leetcode77.组合、Leetcode78.子集)

我们经常会遇到如下问题:给定一列数组或数,然后举例出所有可能的组合结果,这种问题我们直观的想法可能是:逐个遍历元素,求所有可能结果,这样当所给数量级较大时,可能会爆炸;此时我们通常的解法是将所有可能结果构造成一棵树,树中每一层都是一种可能的结果。(例如下图举例构造的树)
在这里插入图片描述
这篇文章,我们使用Java来实现递归+回溯构造树,在构造树之前我们需要定义数据结构来存储所有可能的元素,我经常会采用的是双向队列Deque<>和列表List<List<>>,其中Deque通常用来存储当前某条路径满足条件的解,而List用来存储所有可能的解。
定义代码如下:

		//用来满足条件的结果
        List<List<Integer>> list = new ArrayList<>();
        //定义双向队列
        Deque<Integer> que = new ArrayDeque<>();

而我们通常采用向Deque队尾当中加元素实现递归,删除元素实现回溯;
递归、回溯代码如下:(通常在一个for循环体当中)

			que.addLast(i);
			//递归
            dfs(n,k,i+1,que,list);
            //回溯
            que.removeLast();

有了以上的简单概述,可能对此类问题还没有较深的印象,那么接下来举例两道通过构造递归树来求解的问题:

题1:Leetcode77.组合

题目概述:
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

算法思路:
我们可以一次遍历,比如说第一个数取1、取2…取n的时候,然后在第一个数取1情况下第二个数取2、第一个数取2情况下第二个数取3…(这里为了实现所有递归的可能不重复,我们在for循环初始值设置的时候往往是前一个数+1,这样就能避免重复);通过这种方法构造递归树,然后当Deque当中元素个数等于k的时候(递归边界)结束递归,然后进行回溯(向树的上一层退一格然后再往没有走过的枝杈遍历),递归所有可能的结果。

算法实现(Java版):

class Solution {
    
    
    public List<List<Integer>> combine(int n, int k) {
    
    
        /**本题还是和上一次的题目类似使用递归+回溯方法 */
        //用来满足条件的结果
        List<List<Integer>> list = new ArrayList<>();
        //定义双向队列
        Deque<Integer> que = new ArrayDeque<>();
        dfs(n,k,1,que,list);
        return list;

    }
    public void dfs(int n,int k,int begin,Deque<Integer> que,List<List<Integer>> list){
    
    
        if(que.size()==k){
    
    
            //达到递归条件
            list.add(new ArrayList<>(que));
            return;
        }
        for(int i=begin;i<=n;i++){
    
    
        	//这里从begin开始就是为了避免重复元素
            que.addLast(i);
            //递归
            dfs(n,k,i+1,que,list);
            //回溯寻找下一种可能
            que.removeLast();
        }
    }
}

题2:Leetcode78.子集

题目概述:
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

算法思路:
这题,我们通过读题一看,同样也是列举所有可能的结果,这样的问题常常通过构造递归树来实现;和上一题不同的是,这题的结果长度可能为0、1…nums.length;那么我们只要通过设置递归结束的条件为Deque的长度等于子集中元素的个数即可。
ps:
在这里我们一定要注意,在for循环中实现递归的时候,for循环的结束条件是为了找到所有可能的解,这里必须将nums数组全部遍历到。

算法实现:

class Solution {
    
    
    public List<List<Integer>> subsets(int[] nums) {
    
    
        List<List<Integer>> list = new ArrayList<>();
        Deque<Integer> que = new ArrayDeque<>();
        //实现子集的求解
        for(int i=0;i<=nums.length;i++){
    
    
            dfs(nums,i,0,que,list);
        }
        return list;
    }
    public void dfs(int[] nums,int len,int begin,Deque<Integer> que,List<List<Integer>> list){
    
    
        if(que.size()==len){
    
    
            list.add(new ArrayList<>(que));
            return;
        }
        //将所有可能的解都遍历到
        for(int i=begin;i<nums.length;i++){
    
    
            que.addLast(nums[i]);
            dfs(nums,len,i+1,que,list);
            que.removeLast();
        }
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_56068397/article/details/123709513
今日推荐