回溯算法part1

回溯算法part1

理论基础

为什么要用回溯法:

那么既然回溯法并不高效为什么还要用它呢?

因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。

回溯法解决的问题:

  • 组合问题
  • 切割问题
  • 子集问题
  • 排列问题
  • 棋盘问题

如何理解:

回溯法解决的所有问题都可以抽象成树形结构(N叉树),回溯是递归的副产品,所有回溯函数也就是递归函数

时间复杂度,回溯法本身是暴力查找

回溯模板:

感觉跟递归模板类似

  • 函数返回值和参数
void backtracking(参数)
  • 函数终止条件
if (终止条件) {
    存放结果;
    return;
}

回溯搜索的遍历过程

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

77. 组合 - 力扣(LeetCode)

不太好理解

在模板中,有这样的图,是回溯的树形结构

那么在本题中,树形结构如下图所示:

本题的回溯模板:

  • 参数和返回值:数组范围[1, n]中的n, path返回条件的k,还有确定单层遍历的startIndex
  • 返回条件:不要忘记return
if(path.size() == k){
    res.add(new ArrayList<>(path));
    return; 
}
  • 单层递归逻辑:其中i的范围是剪枝后的表示,注意递归函数下一层搜索要从i+1开始
for(int i = startIndex; i <= n - (k - path.size()) + 1; i++){ // 控制树的横向遍历
    path.add(i); // 处理节点
    backtracking(n, k, i+1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
    path.removeLast(); // 回溯,撤销处理的节点
}

剪枝操作

直接看图说话,如果不剪枝,会有很多多余操作

那么如何剪枝?

本题就是指在每一个for循环的开始位置的优化

 for(int i = startIndex; i <= n; i++) // 未剪枝前

优化过程我总结不出来,直接copy原话:

接下来看一下优化过程如下:

  1. 已经选择的元素个数:path.size();
  2. 还需要的元素个数为: k - path.size();
  3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历

为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。

这里的+1我认为是因为要从[1, n]集合里找,因为此时的起始位置不是0

完整代码:

class Solution {

    List<List<Integer>> res = new ArrayList<>();

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

    public List<List<Integer>> combine(int n, int k) {
        backtracking(n, k, 1);
        return res;
    }

    //回溯函数
    public void backtracking(int n, int k, int startIndex){
        if(path.size() == k){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = startIndex; i <= n - (k - path.size()) + 1; i++){
            path.add(i);
            backtracking(n, k, i+1);
            path.removeLast();
        }
    }
}

216. 组合总和 III - 力扣(LeetCode)

感觉这种题目自己画一个图就清楚了,横向是单层的for循环,纵向是递归过程,这点要搞明白

本题与77. 组合 - 力扣(LeetCode)相比,在单层的for循环中,最多进行9次循环,所以,for循环条件注意

for(int i = startIndex; i <= 9; i++)

然后本题的剪枝操作是根据sum来算,如果sum > n(数字和),那么直接return

总体代码如下:

class Solution {

    List<List<Integer>> res = new ArrayList<>();

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

    int sum = 0;

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

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

17. 电话号码的字母组合 - 力扣(LeetCode)

画图:

本题注意数字和字母间的映射关系,用String数组确定

注意回溯三要素的参数中的digitsIndex,这个参数有两个作用,一个确定递归到哪一层了,一个确定返回条件是否成立

代码如下;

class Solution {

    List<String> res = new ArrayList<>();

    StringBuilder path = new StringBuilder();

    String[] nums = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

    public List<String> letterCombinations(String digits) {
        if(digits.length() == 0){
            return res;
        }
        backtracking(digits, 0);
        return res;
    }

    public void backtracking(String digits, int digitsIndex) {
        if(digitsIndex == digits.length()){
            res.add(path.toString());
            return ;
        }
        
        int index = digits.charAt(digitsIndex) - '0';
        String num = nums[index];

        for(int i = 0; i < num.length(); i++){
            char c = num.charAt(i);
            path.append(c);
            backtracking(digits, digitsIndex + 1);
            path.deleteCharAt(path.length() - 1);
        }
    }
}

心得体会:

没思路、思路混乱,一定要画画图,把图画出来就会清楚很多

posted @ 2025-06-14 20:53  泡芙猪  阅读(33)  评论(0)    收藏  举报