算法学习day08(回溯算法)

回溯法

模板:
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}
本质:

穷举出所有可能,选出想要的答案。

解决方法:

回溯法解决的问题都可以抽象为树形结构,因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度

回溯三部曲:

1.递归函数参数和返回值

2.确定终止条件

3.单层递归的逻辑

循环的时候嵌套递归,深度就是递归的层数(和终止条件有关,在这道题中和k有关系)。循环到每一个数时候,都要递归下去。

一、组合问题(回溯法)

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

你可以按 任何顺序 返回答案。

三部曲:

1.返回值为void,参数为:int n,int k,startIndex。n和k是题目给的条件,startIndex是每次递归进去for循环的起始位置。

2.终止条件:当集合的size()=k的时候,就要收割结果了。

3.单层递归逻辑:

for(int i=startIndex;i<=n;i++){

    list.add(i);

    backtracing(n,k,i+1);

    list.removelast();

}

代码:

class Solution {
    private List<List<Integer>> lists=new ArrayList<>();
    private List<Integer> list=new ArrayList<>();
    public List<List<Integer>> combine(int n, int k) {
        backTracing(n,k,1);
        return lists;
    }
    public void backTracing(int n,int k,int startIndex){
        //收割结果 如果list集合的大小==k 就加到集合里面
        if(list.size()==k){
            lists.add(new ArrayList(list));
            return;
        }
        for(int i=startIndex;i<=n;i++){
            list.add(i);
            backTracing(n,k,i+1);
            list.removeLast();
        }
    }
}
剪枝:

k-list.size():还需要的元素。n-(k-list.size())+1;代表的是在该list.size()的情况下,至多从哪个元素开始刚好可以遍历完k个元素。

k-list.size()还需要元素的个数。一共有n个元素,我需要k-list.size()。我就需要从n-(k-list.size())+1开始,如果比这个还要大的话,就肯定是不够的。

也就是说n-(k-list.size())+1,是每一行右边的边界。

二、组合总和III(回溯法)

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次 

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

1->9里面 k个数 加起来和为n

仍然使用回溯法+剪枝操作:

全局变量:List<Integer> path  List<List<Integer>> paths

1.返回值:void 参数:int target(n),int k,int startIndex

2.终止条件:当path.size()==k&&sumOfList(path)==n 把集合放大paths中

剪枝条件:当sumOfList(path)>n return;

3.单层递归逻辑:for(int i=startIndex;i<=9-(k-path.size())+1;i++)

循环里面递归

代码:

class Solution {
    private List<Integer> path = new ArrayList<>();
    private List<List<Integer>> paths = new ArrayList<>();

    public List<List<Integer>> combinationSum3(int k, int n) {
        backTracing(k, n, 1);
        return paths;
    }

    public void backTracing(int k, int n, int startIndex) {
        if (sumOfList(path) > n)
            return;
        if (path.size() == k && sumOfList(path) == n) {
            paths.add(new ArrayList<>(path));
        }
        for (int i = startIndex; i <= 9-(k-path.size())+1; i++) {
            path.add(i);
            backTracing(k, n, i + 1);
            path.removeLast();
        }
    }

    public int sumOfList(List<Integer> list) {
        int sum = 0;
        for (int i : list) {
            sum += i;
        }
        return sum;
    }
}

三、电话号码的字母组合

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

输入:digits = "23" 字符串
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

输入2:abc 输入3:def

ad\ae\af\bd\.....

思路:digits的length()就是递归的深度。以前两道题递归的深度是k的大小。

递归的广度是每一个按键上字母的个数,

1.返回值为void 参数为:String digits(代表23),int index(代表递归到多深了)

2.终止条件(index==digits.length()) 将path添加到result集合中

3.单层递归逻辑:

  3.1首先根据index,然后从digits中获取到数字,根据数字在字符串letterMap中获取对应的字符串(abc/def...)

   3.2获取到对应的字符串之后,使用for循环进行遍历,取第i个字母,然后继续递归到下一层。

   3.3直到递归到index==digits.length();

代码:

class Solution {
    String[] letterMap = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };
    List<String> result=new ArrayList<>();// 存放结果集
    String path=new String();// 单次的结果
    StringBuilder sb=new StringBuilder();// 拼接字符串的工具

    public List<String> letterCombinations(String digits) {
        if(digits==null||digits.length()==0)
        return result;
        backTracing(digits,0);
        return result;
    }

    public void backTracing(String digits, int index) {
        // 终止条件
        if(index==digits.length()){
            result.add(new String(sb));
            return;
        }
        //找字符串
        Integer number=digits.charAt(index)-'0';
        String str=letterMap[number];
        //for循环遍历递归
        for(int i=0;i<str.length();i++){
            sb.append(str.charAt(i));
            backTracing(digits,index+1);
            sb.deleteCharAt(sb.length()-1);
        }
    }
}

四、组合总和

给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。candidates 中的数字可以无限使用。

遇到的问题:2355 3255 是一样的组合,要把这种重复的组合去掉
思路:和之前题目一样,终止条件和单层递归逻辑有些区别
2.终止条件:

当sumOfList(path)==target 把path添加到result中,return;

当sumOfList(path)>target rerun; 否则会无限递归下去

3.单层递归逻辑:for循环 i=startIndex i<candidates.length i++

先进行剪枝判断,然后加进去,最后回溯

代码:

class Solution {
    private List<List<Integer>> result = new ArrayList<>();
    private List<Integer> path = new ArrayList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        backTracing(candidates, target, 0);
        return result;
    }

    public void backTracing(int[] candidates, int target, int startIndex) {
        if (sumOfList(path) > target)
            return;
        if (sumOfList(path) == target) {
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i = startIndex; i < candidates.length; i++) {
            if(sumOfList(path)+candidates[i]>target)break;
            path.add(candidates[i]);
            backTracing(candidates, target, i);
            path.removeLast();
        }

    }
    public int sumOfList(List<Integer> list) {
        int sum = 0;
        for (Integer i : list) {
            sum += i;
        }
        return sum;
    }
}

五、组合总和II

和上一道题不一样的地方是这里的元素可能是重复的。给定一个可能有重复数字的整数数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。eg:如果candidates数组里面有1 1 2。target为:3。这样就会出现两个1,2

注意:一定要有去重操作:当i>startIndex,并且遇到candidates[i]==candidates[i-1],在下标为i-1的时候,已经遇到过所有的情况,在下标为i的时候就不需要考虑了。因此直接跳过continue就行。

代码:

class Solution {

    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();

    public List<List<Integer>> combinationSum2(int[] candidates, int target){
        if(candidates.length==0)return result;
        Arrays.sort(candidates);
        backTracings(candidates,target,0);
        return result;
    }

    public void backTracings(int[] candidates, int target, int startIndex){
        //终止条件
        if(sumOfList(path)>target)return;
        if(sumOfList(path)==target){
            result.add(new ArrayList<>(path));
            return;
        }

        //单层递归逻辑
        for(int i=startIndex;i<candidates.length;i++){
            //剪枝
            if(candidates[i]+sumOfList(path)>target)return;
            //去重代码
            if(i>startIndex&&candidates[i]==candidates[i-1])continue;
            //普通回溯
            path.add(candidates[i]);
            backTracings(candidates,target,i+1);
            path.removeLast();
        }

    }
    public int sumOfList(List<Integer> list) {
        int sum = 0;
        for (Integer i : list) {
            sum += i;
        }
        return sum;
    }
}

使用boolean[] flag 标记的那种方法是:if(i>0&&candidates[i]==candidaes[i-1]&&flag[i]!=1)

这种情况因为是i>0 对i没有限制

不使用标记的方法是:if(i>startIndex&&candidates[i]==candidaes[i-1])  要求i>startIndex,在第一遍的时候都不会满足i>startIndex,eg:1,1,2 在第一个1的时候 不会满足这个条件的.

六、分割回文串:

复习:String.substring(int start,int end);包头不包尾

问题:终止条件||哪段字符串需要进行判断||将集合转换成string字符串

思路:和平常的回溯题一样,水平方向是分割字符串的方法,可以分第一个,也可以直接分两个,也可以直接分三个。这个体现在for循环中(int i=startIndex;i<s.length();i++),切割的范围为(startIndex,i);当for循环进行第二次遍历的时候,就是以以前两个为整体切割的。

1.递归函数的返回值:void 参数:String s,int startIndex

2.终止条件:当startIndex到达s.length()的时候,就说明到了叶子节点,后面没有元素可以添加进去,这时候我们就把分割好的字符串加到Result中。(判断是否是回文串在单层递归逻辑中实现)

3.单层递归逻辑:for(int i=startIndex,i<s.length();i++)

然后切割的字符串就是[startIndex,i];如果是就加入到list中,如果不是就continue;

代码:

class Solution {

    List<String> list=new ArrayList<>();
    List<List<String>> result=new ArrayList<>();

    public String[][] partition(String s) {
        backTracing(s,0);
        String[][] res=new String[result.size()][];
        for(int i=0;i<res.length;i++){
            List<String> path=result.get(i);
            res[i]=path.toArray(new String[path.size()]);
        }
        return res;
    }
    public void backTracing(String s,int startIndex){
        //终止条件:如果切割到叶子节点说明 分割到底 并且都是回文串
        if(startIndex>=s.length()){
            result.add(new ArrayList<>(list));
            return;
        }
        //单层遍历逻辑
        for(int i=startIndex;i<s.length();i++){
            boolean flag=isPalindrome(s,startIndex,i);//判断是否是回文串
            if(flag){
                list.add(s.substring(startIndex,i+1));//含头不含尾
            }else{
                continue;
            }
            backTracing(s,i+1);
            list.remove(list.size()-1);
        }
    }
    public boolean isPalindrome(String s,int left,int right){
        for(int i=left,j=right;i<j;i++,j--){
            if(s.charAt(i)!=s.charAt(j))return false;
        }
        return true;
    }
}

     

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值