回溯算法总结(1)组合问题

回溯算法刷题总结

回溯其实就是一种搜索方法。回溯是递归的副产品,只要有递归就会有回溯
回溯的本质是穷举,效率其实不高。如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。但是有些时候我们又不得不使用回溯算法。

一些需要用到回溯算法的问题 比如:
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等

回溯法解决的问题都可以抽象为树形结构.
因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。
递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。

模板代码

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

  • 回溯算法的返回值一般为void
  • 参数: 具体问题具体定
  • 终止条件: 很多都是收集叶子节点,当我们访问到树的叶子节点时候就可以return了。但是部分题是每个节点都要收集。
  • for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历

一.组合问题

1.组合问题
组合问题

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

这个题是回溯算法的经典问题,理解了这道题,后面的组合问题都是差不多的。
在分析题目的时候,我们对照模板。可以发现n就是我们树的宽度,k就是树的高度。也就是我们递归的深度。当递归到第k个节点时候,我们就要返回结果了。
如下图所示,树的每一层都相当于是取数操作。同时我们每次取得数不重复。最后收集最后一层的叶子节点。

在这里插入图片描述

代码

class Solution {
    List<Integer> temp;//记录已经递归的数
    List<List<Integer>> result;
    public List<List<Integer>> combine(int n, int k) {
        temp=new ArrayList<>();
        result=new ArrayList<>();
        int startIndex=1;
        backtracking(n, k,startIndex);
        return result;
    }
    public  void backtracking(int n, int k,int startIndex){
        if(temp.size()==k){//终止条件
            result.add(new ArrayList<>(temp));
            return;      
        }
        for(int i=startIndex;i<=n-(k-temp.size())+1;i++){
            temp.add(i);
            backtracking(n,k,i+1);//回溯
            temp.remove(temp.size()-1);
        }
    }
}

终止条件就是找到k个数
参数:n:我们需要遍历的区间1-n。k是我们的终止条件。startIndex就是记录我们此时递归的数。

2.组合总和3
组合总和3

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9
每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

这道题和上个题大致思路是一样的,上一个问题让把所有组合列出来。这个问题是让把组合中等于n的组合列出来。数字是从1-9,每次使用不能重复。那么树的宽度是9。
k代表只能使用k个数,因此数的高度是k。这里引出终止条件:当记录的数的个数为k,并且和为sum,记录下来并返回。如果和不是sum。那么直接返回。

class Solution {
    List<List<Integer>> result=new  ArrayList<List<Integer>>();
    List<Integer> path=new ArrayList<>();
    int sum;
    public List<List<Integer>> combinationSum3(int k, int n) {
         get(k,n,1,0);
         return result;
    }
    public void get(int k,int n,int index,int sum)
    {
        if(path.size()==k)//返回条件
        {
             if(sum==n)
             {
                result.add(new ArrayList<>(path));
             }
            return;      
        } 
        for(int i=index;i<= 9;i++){
            path.add(i);
            get(k,n,i+1,sum+i);
            path.removeLast();//回溯

        }
    }
}

这类题可以优化。也就是剪枝。举一个例子,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了。
在这里插入图片描述
图中每一个节点(图中为矩形),就代表本层的一个for循环。如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。
已经选择的元素个数:path.size();
所需需要的元素个数为: k - path.size();
列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())

在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历

因此我们这道题可以这样写

class Solution {
    List<List<Integer>> result=new  ArrayList<List<Integer>>();
    List<Integer> path=new ArrayList<>();
    int sum;
    public List<List<Integer>> combinationSum3(int k, int n) {
         get(k,n,1,0);
         return result;
    }
    public void get(int k,int n,int index,int sum)
    {
        if (sum > n) return;//剪枝 1-9越往后数越大。
        if(path.size()==k)//返回条件
        {
             if(sum==n)
             {
                result.add(new ArrayList<>(path));
             }
            return;      
        }
        
        for(int i=index;i<= 9 - (k - path.size()) + 1;i++){
            path.add(i);
            get(k,n,i+1,sum+i);
            path.removeLast();//回溯

        }
    }
}

3.电话号码的字母组合
电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]

每个数组对应一个字母列表。如2->“abc”.在前两道题时候,我们都是处理同一个集合的组合,这道题我们处理的不同集合的组合。根据所给的数组,处理它所代表的字母集合的组合。
这时候我们需要考虑用什么形式表示字母和数字的映射。可以考虑map,也可以用字符串数组来表示。数组下标代表按键上的数字。
我们的终止条件就是当前递归过程中记录的字母数和所给的digits的长度相同,保存结果并返回。
参数就是digits数组和当前需要遍历的字符串数组。

class Solution {
     StringBuilder temp = new StringBuilder();
    List<String> result=new ArrayList<>();
    public List<String> letterCombinations(String digits) {
       if (digits == null || digits.length() == 0) {
            return result;
        }
        String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        //迭代处理
        backTracking(digits, numString);
        return result;
    }
    public void backTracking(String digits,String[]  numString)
    {   
        if(temp.length()==digits.length())
        {
            result.add(temp.toString());
            return;
        }  
        String str = numString[digits.charAt(temp.length()) - '0'];
        for(int i=0;i<str.length();i++)
        {
            temp.append(str.charAt(i));
            backTracking(digits,numString);
            temp.deleteCharAt(temp.length()-1);

        }
    }
}

4.组合总和

组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]] 解释: 2 和 3
可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7
仅有这两种组合。

可以看出这个题的不同在与遍历的数组中,数字是可以重复选取的。因此我们只需要改动一个地方,就是每次回溯传入的index。之前是从index+1开始。如果允许重复遍历的话,就从index开始。

class Solution {
    List<List<Integer>> result=new ArrayList<>();
    List<Integer> path=new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
         back(candidates,target,0,0);
         return result;     
    }
    public void back(int [] candidates,int target,int index,int sum)
    {
        if(sum>target)
             return;
        if(sum==target){
            result.add(new ArrayList<>(path));
            return;
        }
        for(int i=index;i<candidates.length;i++){
            path.add(candidates[i]);
            back(candidates,target,i,sum+candidates[i]);
            path.removeLast();
        }

    }

5.组合总和II
组合总和Ⅱ

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target
的组合。

candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

与上面题最大的不同就是遍历的区间有重复的数字。如果我们当前选择1(第二个)和7.下次有可能会选到7和1(倒数第二个) 。这样造成了重复,不满足题意。因此我们应该在遍历for循环的时候做文章。假如我们对数组排序 1,1,2,5,6,7,10。我们如果从1开始,它取到1和7了。那么我们在回溯去找其他答案的时候,第二个1就不能用了,它一定会得到和第一个1开头时相同的答案。
在这里插入图片描述
如图所示,我们要先对数组排序,然后去重。
去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。
used是个boolean数组,它代表了当前位置的数是否被用过。
如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。

class Solution {
    List<List<Integer>> result=new ArrayList<>();
    List<Integer> path=new ArrayList<>();
    boolean[] used;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
         used = new boolean[candidates.length];
         Arrays.sort(candidates);//排序是为了在每层去重 不在一层拿同样的节点
         back(candidates,target,0,0);
         return result;
    }
    public void back(int [] candidates,int target,int index,int sum)
    {
        if(sum>target)
             return;
        if(sum==target){
            result.add(new ArrayList<>(path));
            return;
        }
        for(int i=index;i<candidates.length;i++){

            if(i>0&&candidates[i]==candidates[i-1]&& !used[i - 1])
            {
                continue;
            }
            used[i] = true;
            path.add(candidates[i]);
            
            back(candidates,target,i+1,sum+candidates[i]);
            used[i] = false;
            path.removeLast();
        }
    }
 }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值