day22|| 回溯算法 ||lc77组合 ||lc216组合总和III || lc17电话号码的字母组合

day22_回溯算法

基础知识

回溯和递归是相辅相成的, 只要有递归 就会有回溯 通常在递归函数的下面出现递归

通常用于解决 组合问题 切割问题 子集问题 排列问题 棋盘问题(n皇后)

**组合 : [1, 2] 和 【2, 1】是相同的组合 **

**排列: 【1,2】 和 【2,1】 是两个不同的排列 **

回溯法 其实是一个纯暴力的搜索算法

比如 给你1 2 3 4 在这里面找到大小为2的组合 那么组合分别是多少

切割问题: 给你一个字符串 问有几种切割方法 或者如何保证子串 都是回文子串 几种方式


回溯法都可以抽象成n叉树结构

树的宽度就是回溯法处理的集合的大小 使用for循环

树的深度就是递归的深度 因为递归一定是有终止的 一层一层向上返回

回溯模版

**一般来说 回溯的函数是没有返回值的 一般情况起名叫backtracking **

参数的话 一般情况是非常多的 一次性无法完全确定

回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。


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

    }

}

其中 for循环就是便利集合区间 , 一个节点有多少孩子 这个for就执行多少多少次


lc77_组合

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]

此时 我们会想到一个非常非常普通暴力的解法 就是k=2的时候 使用两层for暴力

但是当k=40 50 60的情况下呢? 难道要写60个for??


使用回溯算法

回溯算法 其实就是通过递归来控制有多少for循环 递归里面的每一次其实就是一个for循环

下一次递归就是下一层for循环


回溯三部曲

  1. 确定递归函数的参数和返回值

一般情况 返回值都是void 极个别情况才会有返回值 函数名通常叫backtracking

  1. 确定终止条件

到了叶子节点也就是路径大小为2的时候

  1. 确定单层搜索逻辑

startIndex用来控制每个for循环从哪里开始的

下一层startIndex应该传入i + 1

class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        backtracking(n, k, 1, res, path);
        return res;        
    }
    public void backtracking(int n, int k, int sIndex, List<List<Integer>> res, List<Integer> path){
        if(path.size() == k){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = sIndex; i <= n; i++){
            path.add(i);
            backtracking(n, k, i + 1, res, path);
            path.remove(path.size() - 1); //移除最后一个
        }
    }
}

其中这个代码可以进行剪枝操作

假设n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了

注意i,就是for循环里选择的起始位置。

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

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

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

举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。

从2开始搜索都是合理的,可以是组合[2, 3, 4]。

所以剪枝只需要控制for循环的范围就可以

i <= n - (k - path.size()) + 1;

for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置

lc216_组合总和III

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

  • 所有数字都是正整数。
  • 解集不能包含重复的组合。

示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]

示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]

跟上一个题lc77的区别就是 本题给定了和的限制 让我们求和为n

public List<List<Integer>> combinationSum3(int k, int n) {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    backtracking(k, n, 1, 0, res, path);
    return res;

}

private void backtracking(int k, int n, int sIndex, int sum, List<List<Integer>> res, List<Integer> path) {
    if(sum > n) return;
    if(path.size() == k){
        if(sum == n) res.add(new ArrayList<>(path));
        return;
    }
    for(int i = sIndex; i <= 9; i++){
        path.add(i);
        sum += i;
        backtracking(k, n, i + 1, sum, res, path);
        path.remove(path.size() - 1);
        sum -= i;
    }
}

剪枝:

** if(sum > n) return;**

i <= 9 - (k - path.size()) + 1;


lc17_电话号码的字母组合(手撕高频)

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

  • 输入:"23"
  • 输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

首先要解决映射的问题, 数字2 对应 abc 数字3 对应def .....

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

为什么要0和1也要呢 因为要对应下标 2就是对应abc


这个num是记录遍历第几个数字了,就是用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。

那么终止条件就是如果index 等于 输入的数字个数(digits.size)了(本来index就是用来遍历digits的)。 然后收集结果,结束本层递归。

class Solution {
    public List<String> letterCombinations(String digits) {
        
        List<String> res = new ArrayList<>();
        StringBuilder path = new StringBuilder();

        if(digits == null || digits.length() == 0) return res;
        String[] numString = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
        backtracking(digits, numString, 0, res, path); 
        return res;
    }
    public void backtracking(String d, String[] numString, int num, List<String> res, StringBuilder path){
        if(num == d.length()){
            res.add(path.toString());
            return;
        }
        String str = numString[d.charAt(num) - '0'];  // 获取当前数字的字母映射
        for(int i = 0; i < str.length(); i++){
            path.append(str.charAt(i));
            backtracking(d, numString, num + 1, res,path);
            path.deleteCharAt(path.length() - 1);
        }
    }
}
posted @ 2024-11-24 18:48  小杭呀  阅读(31)  评论(0)    收藏  举报