查找类问题(4)_LC:22、78、77、46

1.4 回溯法(LC:22 + 78 + 77 + 46)

回溯法:

利用了递归的思想,类似枚举

  • 对于枚举的理解:查看后面几题中的图示,都是将所有的情况枚举出来,画出树状图

一层一层向下递归,尝试搜索所有可能的答案:

  • 找到答案:返回答案,接着尝试有没有别的可能
  • 找不到答案:返回上一层递归,尝试别的路径

可以应用于:求已知序列的子序列,下面几道题都是这种类型

 

22. 括号生成 - 力扣(LeetCode) (leetcode-cn.com)

难度:中等

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例2:

输入:n = 1
输出:["()"]

提示:

1 <= n <= 8

思路:

以n = 2举例,图示如下:

记左括号的个数为left,右括号的个数为right

写递归,首先一定要明确每层递归的结束条件是什么,这是需要写在递归方法体的最前面的,这里递归的终止条件为(即该答案不对或得到正确答案了,要返回上一层递归,尝试别的路径):

  • 错误答案路径的递归的终止条件:right > left
  • 正确答案路径的递归的终止条件:right == 2 且 left == 2

因为一定要先有左括号,才能有右括号,所以需要先添加左括号,再添加右括号,这是代码中先判断:if(left < n),后判断:if(right < left)的原因

代码:

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> res = new ArrayList<>();
        backtracking(n, res, 0, 0, "");
        return res;
    }

    private void backtracking(int n, List res, int left, int right, String str){
        //错误路径的递归的终止条件
        //(因为后面先判断left < n,再判断right < left,就说明已经默认先添加左括号再添加右括号,即left < right的情况不会发生,这个if判断实际上是用不到的)
        if(left < right){
            return;
        }

        //正确路径的递归的终止条件
        if(left == n && right == n){
            res.add(str);
            return;
        }

        if(left < n){
            backtracking(n, res, left + 1, right, str + "(");
        }

        if(right < left){
            backtracking(n, res, left, right + 1, str + ")");
        }
    }
}

复杂度分析:

时间复杂度:hard to say

空间复杂度:O(n),除了返回的答案res之外,所需要的额外空间取决于递归栈的深度,每一层递归函数需要 O(1) 的空间,最多递归 2n 层,因此空间复杂度为 O(n)

 

78. 子集 - 力扣(LeetCode) (leetcode-cn.com)

难度:中等

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

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

示例 2:

输入:nums = [0]
输出:[[],[0]]

 提示:

1 <= nums.length <= 10

-10 <= nums[i] <= 10

nums 中的所有元素 互不相同

思路:

为了避免重复,在下面的图示中每次向下递归的元素都是该元素后面的元素

解法1:暴力法-扩展法

解法2和解法3中画图非常重要(图中的走法,就是避免重复的走法),才能弄清楚回溯的过程和DFS的过程(回溯和DFS都利用了递归的思想)

解法2:回溯法

对于剪枝的理解:本来是一棵完整的树,经过剪枝,去掉不想要的部分,得到如上图所示的树

递归的终止条件为(剪枝条件):

递归生成的子集长度 == 需要的子集长度

解法3:DFS

递归的终止条件为(剪枝条件):

递归到的数组元素是数组的最后一个元素(如上图中每条递归线最深的元素都是3):递归到的数组元素的索引 == 数组长度 - 1

辨析回溯法和DFS的区别:

  • 回溯法:使用for循环(这个for循环不是指回溯方法backtracking中的for循环),进行3次递归,每次递归生成确定的长度为k的子集(即每次递归的深度是一样的,都是k),每次递归添加到res中的子集长度都是k
  • DFS:不使用for循环,直接递归到最深层,并且边递归边添加子集,每次添加的子集长度是不固定的,一直添加到最深层

一个细节点:

  • List list1 = new ArrayList<>(list):(要求list也是ArrayList类型)创建一个 ArrayList类型的、和list中元素相同的 对象list1,这种操作(指:new ArrayList<>(list))相当于复制一份list中的元素,复制品起名为list1,list和list1的地址值是不同的,所以操作list不会对list1产生影响
    • 注意区分:如果list不是ArrayList类型,假设为一个int类型的数,则List list1 = new ArrayList<>(list)表示:创建一个初始化容量为list大小的ArrayList

代码:

解法1:暴力法-扩展法

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        res.add(new ArrayList<>()); //先加入一个空的子集[]

        for(int item : nums){
            List<List<Integer>> subset = new ArrayList<>();
            for(List<Integer> list : res){
                List<Integer> temp = new ArrayList<>(list); //ArrayList<>(list)表示:temp复制了一份list
                						 //不能在原本的list中直接添加item,这样就修改了res
                       					 //因此需要额外复制一个和list内容一样的temp,但是temp和list的内存空间不一样
                temp.add(item); 
                subset.add(temp);
            }
            for(List<Integer> list1 : subset){
                res.add(list1);
            }
        }
        return res;
    }
}

解法2:回溯法

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        res.add(new ArrayList<>()); //先添加空子集[]

        //循环添加:子集长度分别为1至nums数组长度
        for(int i = 1; i <= nums.length; i++){
            backtracking(nums, res, i, 0, new ArrayList<>());
        }

        return res;
    }
    
    //nums:数组,res:输出
    //length:子集的长度,index:当前递归到的数组的索引位置,subset:当前递归生成的子集
    private void backtracking(int[] nums, List<List<Integer>> res, int length, int index, List<Integer> subset){
        //递归的终止条件(剪枝条件):递归生成的子集长度 == 想要得到的子集长度
        if(subset.size() == length){
            res.add(new ArrayList<>(subset)); //一定不能写成res.add(subset)
            return;
        }

        //从数组的索引为i开始,进行向下递归
        for(int i = index; i < nums.length; i++){
            subset.add(nums[i]);
            //从数组的索引为i+1开始,进行向下递归
            backtracking(nums, res, length, i + 1, subset);
            subset.remove(subset.size() - 1); //remove(int index)(List中的删除指定索引上的元素),本行代码是在每层递归结束后,
            				   //将最后一个添加到subset中的元素删除,也就是在回溯的过程中不断删除元素		
                   			   //这一行代码非常重要,因为这一步实现了:图示中由 深层递归 向 浅层递归 回溯的关键(即从 下一层 回到 上一层)
        }
    }
}	

解法3:DFS

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();

        dfs(nums, 0, res, new ArrayList<>());
        return res;
    }

    private void dfs(int[] nums, int index,  List<List<Integer>> res, ArrayList<Integer> subset){
        //res.add(new ArrayList<>());
        res.add(new ArrayList<>(subset));
        //注意:一定不能写成res.add(subset)
        //原因:subset是引用类型,传递的是subset的地址值,而subset一直在变化,如果直接将subset添加到res中,则subset变化,之前添加的subset也会随之变化
        //由此出现的错误现象:res中的所有元素都是一样的
        //解决办法:新造一个对象,将subset对应的堆空间中的数据值复制一份到新的对象中,然后将新的对象add到res中即可

        //递归终止条件:index最终为i+1,所以应该和nums.length比较,而不是和nums.length-1比较
        if(index == nums.length){
            return;
        }

        for(int i = index; i < nums.length; i++){
            subset.add(nums[i]);
            dfs(nums, i + 1, res, subset);
            subset.remove(subset.size() - 1); //这一行代码非常重要,因为这一步实现了:图示中由 深层递归 向 浅层递归 回溯的关键(即从 下一层 回到 上一层)
        }
    }
}

 

77. 组合 - 力扣(LeetCode) (leetcode-cn.com)

难度:中等

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

示例:

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

思路:

本题和上题一致,可以理解为:

求一个长度为n的数组中,所有可能的长度为k的子集,子集中不能有重复元素

本题同样可以使用回溯法和DFS,但是由于需要生成的子集长度是固定的,所以回溯法和DFS的图示与写法是相同的

图示:

递归终止条件:

递归生成的子集长度 == k

代码:

class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> res = new ArrayList<>();
        
        if(k == 0){
            res.add(new ArrayList<>());
            return res;
        }
        
        backtracking(n, k, 0, res, new ArrayList<>());
        return res;
    }
    
    //n:数组长度及取值范围,k:生成的子集长度,res:输出
    //length:需要生成的子集的长度,index:当前递归到的数组的索引位置,subset:当前递归生成的子集
    private void backtracking(int n, int k, int index, List<List<Integer>> res, ArrayList<Integer> subset){
        int length = subset.size();
        if(length == k){
            res.add(new ArrayList<>(subset));
            return;
        }
        
        for(int i = index; i < n; i++){
            subset.add(i + 1); //数组中的元素是从1开始到n的,而索引i是从0开始到n-1的
            backtracking(n, k, i + 1, res, subset);
            subset.remove(subset.size() - 1); //这一行代码非常重要,因为这一步实现了:图示中由 深层递归 向 浅层递归 回溯的关键(即从 下一层 回到 上一层)
        }
    }
}

 

46. 全排列 - 力扣(LeetCode) (leetcode-cn.com)

难度:中等

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

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

思路:

本题和前两题一致,可以理解为:

对一个长度为n的数组nums,得到其可以重复的、长度为n的子集

本题同样可以使用回溯法和DFS,但是由于需要生成的子集长度是固定的,所以回溯法和DFS的图示与写法是相同的

图示:

递归的终止条件:

子集的长度 == 数组的长度n

一个细节点:

因为本题需要得到全排列,是可以重复的子集,所以在不同的递归线中,是可以出现相同的元素的

代码:

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();

        backtracking(nums, 0, nums.length, res, new ArrayList<>());

        return res;
    }
    
    //nums:数组,res:输出
    //n:数组的长度,index:当前递归到的数组的索引位置,subset:当前递归生成的子集
    private void backtracking(int[] nums, int index, int n, List<List<Integer>> res, ArrayList<Integer> subset){
        int length = subset.size();
        if(length == n){
            res.add(new ArrayList<>(subset));
            return;
        }

        for(int i = index; i < nums.length; i++){
            //只需要满足添加不同的元素即可,因为需要得到全排列,即[1,2]和[2,1]是两个不同的子集
            if(subset.contains(nums[i])){
                continue; //跳出本次循环,目的是找到subset中没有的元素,这样才能继续向下一层递归
            }else{
                subset.add(nums[i]);
                backtracking(nums, 0, n, res, subset);    
                subset.remove(subset.size() - 1); //这一行代码非常重要,因为这一步实现了:图示中由 深层递归 向 浅层递归 回溯的关键(即从 下一层 回到 上一层)
            }       
        }
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值