算法笔记之回溯法(一)——溯洄从之,道阻且长;溯游从之,宛在水中央。

回溯法理论基础

回溯法是一种搜索算法,从本质上来说,回溯法是一种穷举法,穷尽其所有可能而举其可行解;尽管回溯法有剪枝等操作,但也只是去除一些明显不可行的部分,仍改变不了回溯法暴力搜索的本质。

虽然回溯法是一种暴力求解算法,但很多时候我们也只能选择这种算法。

回溯法是以深度优先的方式系统地搜索问题的解,它适用于解一些组合数较大的问题。

回溯法可以解决的问题

  • 组合问题:从n个数的集合中选出k个数的组合问题
  • 排列问题:对n个数进行排列有多少种排列方法
  • 子集问题:一个集合种有多少符合条件的子集问题
  • 棋盘问题:典型的有n皇后问题、解数独问题

回溯法解题模板

void backtrack(参数) {
    if (终止条件) {
        收集结果;
        return;
    }
    for (选择:本层集合中元素(集合大小为本层节点的数量)) {
        处理节点;
        backtrack(路径,选择列表,其他参数); // 递归
        回溯,撤销处理操作;
    }
}

回溯法之组合问题

组合问题基础
力扣第39题39. 组合总和 - 力扣(LeetCode) (leetcode-cn.com)

求一个数组中使数字和为目标值的所有组合,属于典型的组合问题,接下来套用模板进行解决。

首先我们需要一个总的链表来存放已经找到的组合,还需要一个存放当前组合的链表

List> res=new ArrayList<>();   //存放可行解的链表
List l=new ArrayList<>();   //存放当前结果的链表

接下来定义我们的回溯函数backtrack(),这也是回溯法的重中之重,由于回溯法的参数一般比较复杂,我们可以先不填写参数,需要用到的时候再加入参数。

为了判断当前组合是否满足条件,我们需要设置一个整型变量sum,用于记录当前组合的数字和,回溯终止条件是sum==target即当前数字之和等于给定的目标值,并将其加入到结果集中;

if(sum==target){       //收集结果
    res.add(new ArrayList(l));
    return;
}

如果sum值小于目标值,继续进行深度搜索;在本层集合中依次选择元素都尝试将其加入当前组合中,在for循环中先将其加入组合中,并修改sum值,然后回溯递归进行下一层操作,回溯完成后,不要忘了撤销刚才的操作。

for(int i=startindex;i

我们还可以加入剪枝操作,当sum值已经大于目标值target时,就不需要进行接下来的操作了,直接break。加入剪枝操作之后的代码为:

for(int i=startindex;i

在主函数中我们需要对数组进行排序,可以减少重复的回溯组合,然后调用回溯函数进行求解。

总的代码如下:

class Solution {
    public List> combinationSum(int[] candidates, int target) {
        List> res=new ArrayList<>();
        List l=new ArrayList<>();
        Arrays.sort(candidates);
        backtrack(res,candidates,target,l,0,0);
        return res;
    }
    public void backtrack(List> res,int[] candidates,int target,List l,int sum,int startindex){
        if(sum==target){       //收集结果
            res.add(new ArrayList(l));
            return;
        }
        
        for(int i=startindex;i
组合问题(去重)

力扣第40题40. 组合总和 II - 力扣(LeetCode) (leetcode-cn.com)

此题和上题的描述大致相同,但有两个区别,这也导致了此题的答案与上题有两处不同

  1. 数组中的每个数字只能使用一次
  2. 解集不能包含重复的组合

这是两个不同的限制要求

  • 对于第一个要求,每个数字只能使用一次,我们考虑上题的backtrack函数中的startIndex参数,由于上题中没有要求每个数字只能使用一次,我们传给下一层的startIndex参数是i,这代表下一层还可以继续使用自己,所以在此题中,i这一个数字在下层肯定不能继续使用,于是传给下一层的参数是i+1

    backtrack(res, list, candidates, sum, target, i+1);
    
  • 对于第二个要求,是结果不能包含重复的组合,其实重复的组合是由于数组中有重复的元素造成的,比如此题中有两个1,第一个1和数字7组成一个组合得到target数字8,第二个1同样可以和7组成一个组合也可以得到target数字8,这两个组合都是可行解,且每个数字只使用一次,但是却是重复的组合。

    我们在回溯开始之前已经对数组进行了排序,在for循环中遇到第二个1、第三个1或是更多的1时可以直接跳过此循环,因为此时产生的组合都已经加入到结果集中了(第一个1产生的)。

    if(i>startIndex && candidates[i]==candidates[i-1]) continue;
    

完整的代码如下:

public List> combinationSum2(int[] candidates, int target) {
        List> res=new ArrayList<>();
        List list=new ArrayList<>();
        Arrays.sort(candidates);
        backtrack(res,list,candidates,0,target,0);
        return res;
    }
    void backtrack(List> res,List list,int[] candidates,int sum,int target,int startIndex){
        if(sum==target){
            res.add(new ArrayList(list));
            return;
        }
        for(int i=startIndex;istartIndex && candidates[i]==candidates[i-1]) continue ; //去重
            int temp=sum+candidates[i];
            if(temp<=target){
                list.add(candidates[i]);
                sum+=candidates[i];
                backtrack(res, list, candidates, sum, target, i+1);   //数组中每个数字只使用一次
                sum-=candidates[i];
                list.remove(list.size()-1);
            }else break;
        }
    }
组合问题练习

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

做完上面两个题之后相信对于回溯法做组合问题已经有了大致思路,不妨做一下此题,和上述题目异曲同工。

public List> combine(int n, int k) {
    List> res=new ArrayList<>();
    List list=new ArrayList<>();
    backtrack1(res,list,n,k,0,1);
    return res;
}
void backtrack1(List> res,List list,int n,int k,int count,int startIndex){
    if(count==k){
    res.add(new ArrayList<>(list));
    return;
    }
    for(int i=startIndex;i

你可能感兴趣的:(算法笔记,算法,leetcode)