回朔法简单总结

本文介绍了回溯法的概念、解题关键要素以及如何用递归实现回溯法。通过实例展示了LeetCode上的回溯法解题,包括全排列和组合问题,并讨论了剪枝操作对提高算法效率的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


(尊重劳动成果,转载请注明出处:http://blog.csdn.net/qq_25827845/article/details/77197782冷血之心的博客)


回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

在回溯法中,每次扩大当前部分解时,都面临一个可选的状态集合,新的部分解就通过在该集合中选择构造而成。这样的状态集合,其结构是一棵多叉树,每个树结点代表一个可能的部分解,它的儿子是在它的基础上生成的其他部分解。树根为初始状态,这样的状态集合称为状态空间树。

回溯法解题的关键要素:
确定了问题的解空间结构后,回溯法将从开始结点(根结点)出发,以深度优先的方式搜索整个解空间。开始结点成为活结点,同时也成为扩展结点。在当前的扩展结点处,向纵深方向搜索并移至一个新结点,这个新结点就成为一个新的活结点,并成为当前的扩展结点。如果在当前的扩展结点处不能再向纵深方向移动,则当前的扩展结点就成为死结点。此时应往回移动(回溯)至最近的一个活结点处,并使其成为当前的扩展结点。回溯法以上述工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已无活结点时为止。
运用回溯法解题的关键要素有以下三点:
(1) 针对给定的问题,定义问题的解空间;
(2) 确定易于搜索的解空间结构;
(3) 以深度优先方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。

我们来看一下递归实现回朔法的模板代码:

void BackTrace(int t) {
		if (t > n)
			Output(x);
		else
			for (int i = f(n, t); i <= g(n, t); i++) {
				x[t] = h(i);
				if (Constraint(t) && Bound(t))
					BackTrace(t + 1);
			}
	}


下边看几个LeetCode上几道典型的回朔法:

题目一:


题目的大意就是按照手机键盘上,每个数字可以表示的意义,给定一个字符串数字,列举出所有可能的字符组合~

结题思路:使用递归模板搞定

public class Solution {
    List<String> list = new ArrayList<String>();
    String[] letterMap = {
        " ", // 0
        "",  // 1
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz"
    };
    public List<String> letterCombinations(String digits) {
        if(digits==null||digits.equals(""))
            return list;
        
        generate(digits,0,"");
        
        return list;
    }
    private void generate(String digits,int index,String s){
        // 此时s是其中的一个解
        if(index==digits.length()){
            list.add(s);
            return ;
        }
        char c = digits.charAt(index);
        if(c>='0'&&c<='9'&&c!='1'){
            String letters = letterMap[c-'0'];
            for(int i = 0;i<letters.length();i++){
                generate(digits,index+1,s+letters.charAt(i));
            }
        }
        
    }
}


题目二:求全排列


题目大意就是给定一个数组,求其全排列组合

public class Solution {
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    public List<List<Integer>> permute(int[] nums) {
        List<Integer> list = new ArrayList<Integer>();
        if(null==nums||nums.length==0)
            return res;
        get1Result(nums,0,list);
        return res;
    }
    // list中保存了一个有index个元素的排列
    // 向这个排列的末尾添加第index+1个元素,获得一个有index+1个元素的排列
    private void get1Result(int[] nums,int index,List<Integer> list){
        if(index==nums.length){
            res.add(new ArrayList<Integer>(list));
            return ;
        }
        for(int i = 0;i<nums.length;i++){
            if(!list.contains(nums[i])){
                list.add(nums[i]);
                get1Result(nums,index+1,list);
                list.remove(list.size()-1);
            }
        }
            
    }
}


题目三:求所有组合


题目大意就是说从n个数中取出k个数,找出所有的组合,利用递归模板代码,我们可以得出以下的代码:

public class Solution {
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    public List<List<Integer>> combine(int n, int k) {
        if(n<=0||k<=0||k>n)
            return res;
        List<Integer> list = new ArrayList<Integer>();
        generateCombine(n,k,1,list);
        return res;
    }
    
    // 求解C(n,k),当前已经找到的组合存储在c中,需要从start开始搜索新的元素
    private void generateCombine(int n,int k,int start ,List<Integer> list){
        if(list.size()==k){  // 递归结束条件
            res.add(new ArrayList<Integer>(list));
            return ;
        }
        // 递归过程
         for(int i = start;i<=n;i++){
            list.add(i);
            generateCombine(n,k,i+1,list);
            list.remove(list.size()-1);
        }
    }
}


运行结果如下:用了25ms。



这个时候就涉及到了一种回溯剪枝的问题,我们还是依照例子中的n = 4, k = 2来说明,有如下的树形图:


也就是说,我们可以在递归过程中,将不必要的枝条从树中减掉,比如说本例中的取4!!!

剪枝之后的代码如下:

public class Solution {
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    public List<List<Integer>> combine(int n, int k) {
        if(n<=0||k<=0||k>n)
            return res;
        List<Integer> list = new ArrayList<Integer>();
        generateCombine(n,k,1,list);
        return res;
    }
    
    // 求解C(n,k),当前已经找到的组合存储在c中,需要从start开始搜索新的元素
    private void generateCombine(int n,int k,int start ,List<Integer> list){
        if(list.size()==k){
            res.add(new ArrayList<Integer>(list));
            return ;
        }
        // 递归过程
        // 还有k-list.size()个空位,所以,[i...n]中至少要有k-list.size()个元素
        // i最多为n-(k-list.size())+1,否则没有那么多元素可以放入list中了。
        for(int i = start;i<=n-(k-list.size())+1;i++){
            list.add(i);
            generateCombine(n,k,i+1,list);
            list.remove(list.size()-1);
        }
    }
}


运行结果如下:仅仅只用了4ms,也就是说少用了21ms,简直就是完美的提升~



总结:

针对递归实现的回溯法,我们的基本思路是,首先定义一个用于递归的函数,在主函数中调用该递归函数;然后在递归函数中先判断是否达到了递归结束条件,之后进行循环遍历,在循环过程中,依次进行递归操作,设计到list的操作,主要要remove进行回退;最后,如果可以的话,记得注意剪枝操作,可以大大提升运行效率哦~



以上就是关于递归实现回溯法的简单学习,如果对你有帮助,记得点赞哦~欢迎大家关注我的博客,可以进群366533258一起交流学习哦~



本群给大家提供一个学习交流的平台,内设菜鸟Java管理员一枚、精通算法的金牌讲师一枚、Android管理员一枚、蓝牙BlueTooth管理员一枚、Web前端管理一枚以及C#管理一枚。欢迎大家进来交流技术。




评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

温柔狠角色

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

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

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

打赏作者

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

抵扣说明:

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

余额充值