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

回溯法理论基础

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

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

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

回溯法可以解决的问题
  • 组合问题:从n个数的集合中选出k个数的组合问题
  • 排列问题:对n个数进行排列有多少种排列方法
  • 子集问题:一个集合种有多少符合条件的子集问题
  • 棋盘问题:典型的有n皇后问题、解数独问题
回溯法解题模板
void backtrack(参数) {
    if (终止条件) {
        收集结果;
        return;
    }
    for (选择:本层集合中元素(集合大小为本层节点的数量)) {
        处理节点;
        backtrack(路径,选择列表,其他参数); // 递归
        回溯,撤销处理操作;
    }
}
回溯法之组合问题
组合问题基础
力扣第39题39. 组合总和 - 力扣(LeetCode) (leetcode-cn.com)

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

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

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

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

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

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

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

for(int i=startindex;i<candidates.length;i++){
	sum+=candidates[i];
    l.add(candidates[i]);
    backtrack(res,candidates,target,l,sum,i);
    l.remove(l.size()-1);    //回溯
    sum=sum-candidates[i];
}

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

for(int i=startindex;i<candidates.length;i++){
    int tmp=sum+candidates[i];
    if(tmp<=target){
        sum+=candidates[i];
        l.add(candidates[i]);
        backtrack(res,candidates,target,l,sum,i);
        l.remove(l.size()-1);      //回溯
        sum=sum-candidates[i];
	}else break;                //剪枝操作
}

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

总的代码如下:

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> l=new ArrayList<>();
        Arrays.sort(candidates);
        backtrack(res,candidates,target,l,0,0);
        return res;
    }
    public void backtrack(List<List<Integer>> res,int[] candidates,int target,List<Integer> l,int sum,int startindex){
        if(sum==target){       //收集结果
            res.add(new ArrayList<Integer>(l));
            return;
        }
        
        for(int i=startindex;i<candidates.length;i++){
            int tmp=sum+candidates[i];
            if(tmp<=target){
                sum+=candidates[i];
                l.add(candidates[i]);
                backtrack(res,candidates,target,l,sum,i);
                l.remove(l.size()-1);    //回溯
                sum=sum-candidates[i];
            }else break;
        }
    }
}
组合问题(去重)

力扣第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<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> res=new ArrayList<>();
        List<Integer> list=new ArrayList<>();
        Arrays.sort(candidates);
        backtrack(res,list,candidates,0,target,0);
        return res;
    }
    void backtrack(List<List<Integer>> res,List<Integer> list,int[] candidates,int sum,int target,int startIndex){
        if(sum==target){
            res.add(new ArrayList<Integer>(list));
            return;
        }
        for(int i=startIndex;i<candidates.length;i++){
            if(i>startIndex && 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<List<Integer>> combine(int n, int k) {
    List<List<Integer>> res=new ArrayList<>();
    List<Integer> list=new ArrayList<>();
    backtrack1(res,list,n,k,0,1);
    return res;
}
void backtrack1(List<List<Integer>> res,List<Integer> list,int n,int k,int count,int startIndex){
    if(count==k){
    res.add(new ArrayList<>(list));
    return;
    }
    for(int i=startIndex;i<n+1;i++){
    list.add(i);
    count++;
    backtrack1(res,list,n,k,count,i+1);
    count--;
    list.remove(list.size()-1);
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

图灵的喵酱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值