【搜素算法02】—回溯法

本文深入探讨了回溯算法的概念、三部曲,以及如何应用于组合问题的求解,包括组合、组合总和、电话号码字母组合等编程题目。通过递归和回溯策略,展示了如何在给定约束下找到所有可能的解决方案。此外,还介绍了分割回文串的回溯算法实现。
摘要由CSDN通过智能技术生成

目录

一、回溯法(试探法)

1.1回溯概念

1.2 回溯三部曲

二、组合问题

2.1【编程题】组合问题

 2.2【编程题】组合总和

 2.3【编程题】电话号码的字母组合

 2.4【编程题】组合总和

三、分割回文串

3.1【编程题】分割回文


一、回溯法(试探法)

1.1回溯概念

通过穷举所有可能情况来找到所有解法,若发现当前选择并不是可行解,舍弃当前值,并对前面的步骤作出修改,并尝试重新选择找到可行解。走不通就回退再走的方法就是回溯算法解决回溯问题,实际上就是一个决策树的遍历过程,只需考虑如下问题:

  • 路径:也就是已经做出的选择。
  • 选择列表:也就是你当前可以做的选择。
  • 结束条件:也就是到达决策树底层,无法再做选择的条件。

回溯本质:穷举所有可能,选出需要的答案,其效率并不高,若想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

回溯核心: for 循环里面的递归,递归调用前「做选择」,在递归调用之后「撤销选择」

回溯算法和递归函数通常是放在一起说,相辅相成,递归的实现过程就是回溯过程

回溯法解决的都是在集合中递归查找子集,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

回溯算法中函数返回值一般为void,回溯算法属于树形结构,遍历树形结构一定要有终止条件叶子节点, 把这个答案存放起来,并结束本层递归。

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

for循环理解为横向遍历,backtracking(递归)纵向遍历,搜索叶子节点就是终止条件。

回溯函数伪代码(回溯三部曲):

  • 递归函数和参数返回值
  • 确定终止条件
  • 单层递归逻辑
void backtracking(参数) {  //回溯法一般没有返回值
    if (终止条件) {
        //存放结果;
        return;
    }

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

1.2 回溯三部曲

(1)递归函数的返回值以及参数

定义两个全局变量result数组、path数组,path数组存放符合条件的单一结果result数组存放符合条件结果的集合。同时定义一个变量startIndex,记录每一层开始遍历起始位置

(2)回溯函数终止条件

即到达叶子节点,path数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。

 result二维数组,把path保存起来,并终止本层递归。

(3)单层搜索的过程

搜索过程就是一个树型结构的遍历过程,如下图,for循环横向遍历,递归过程是纵向遍历。

 for循环每次从startIndex开始遍历,然后用path保存取到的节点i。backtracking(递归函数)通过不断调用一直往深处遍历,遇到叶子节点就返回。


二、组合问题

2.1【编程题】组合问题

 

【编程题】给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

  解析:n相当于树的宽度,k相当于树的深度

1.假设开始集合是 1,2,3,4, k为2,从左向右取数,取过的数,不重复取。

2.第一次取1,集合变为2,3,4 ,只需要再取一个数即可,得到集合[1,2] [1,3] [1,4],以此类推。

3.每次搜索到叶子节点,就找到了一个结果只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合。

注意!!!

res.add(list)浅拷贝:添加一个list后,list如果改变,res里的值跟着改变。

res.add(new ArrayList(list))深拷贝:添加一个list后,list若改变,res里的值不会着改变。

class Solution {
        List<List<Integer>> result = new ArrayList<>();
        LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        backtracing(n,k,1);
        return result; 
    }

    public void backtracing(int n,int k,int startIndex) {
        if(path.size() == k) {  //终止条件
            result.add(new ArrayList<>(path)); // path大小为k,即找到其中一个子集,需要将其放入result
            return;
        }
        for(int i = startIndex; i <= n-(k-path.size()) + 1; i++) {  //i <= n-(k-path.size()) + 1 属于剪枝操作
            path.add(i);
            backtracing(n,k,i+1);  //回溯
            path.removeLast();   //撤销上一个元素,进行下一步操作
        }
    }
}

 2.2【编程题】组合总和

216. 组合总和 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]]

class Solution {
    List<List<Integer>> result = new ArrayList<>();   //定义两个数组用于存放搜索到的结果
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {
        backtracing(k, n, 1, 0);
        return result;
    }

    private void backtracing(int k, int n, int startIndex, int sum) {  //回溯函数
        if (sum > n) return;     //和不能 > n 
        if (path.size() > k) return;    //每个子集大小不能 > k
        if (sum == n && path.size() == k) {  //符合求和为n,大小为k表示符合结果,存入result
            result.add(new ArrayList<>(path));
            return;
        }
        for(int i = startIndex; i <= 9; i++) { // 遍历每一层,及其实现构成
            path.add(i);  //path数组存入每一次遍历的结果
            sum += i;  //求和,
            backtracing(k, n, i + 1,sum); //判断是否是n
            sum -= i;  //不符合则退回上一步,继续判断
            path.removeLast();  //不符合时,path也需要拿出上一次放入的值
        }
    }
}

 2.3【编程题】电话号码的字母组合

17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

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

k个元素对应k层,每层元素只取一个元素

class Solution { 
    List<String> result = new ArrayList<>();  //设置全局列表 存储 最后组合结果
    public List<String> letterCombinations(String digits) {
        if (digits == null || digits.length() == 0) { //若字符串为空,直接返回result,空值[]
            return result;
        }
        //设置字符串数组,对应下标为数字0-9,下标对应处的字母为其映射对应字母。 为保持对应关系,新增了两个无效数组""
        String[] str = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        backTracking(digits, str, 0);
        return result;  //结束时 返回  组合
    }

    StringBuilder temp = new StringBuilder();  //StringBuild更为高效,后续大量拼接可用append,线程安全的
    public void backTracking(String digits, String[] str, int num) { //如digits为"23",num 记录遍历的第几个数字,则str表示2对应的 abc
        if (num == digits.length()) {  //遍历完成,记录每次得到的字符串
            result.add(temp.toString());
            return;
        }

        String str1 = str[digits.charAt(num) - '0'];   // 将num指向的数字转为int,并取下表对应的字符串
        for (int i = 0; i < str1.length(); i++) {
            temp.append(str1.charAt(i));
            backTracking(digits, str, num + 1);
            temp.deleteCharAt(temp.length() - 1);    //剔除末尾的继续尝试
        }
    }
}

 2.4【编程题】组合总和

39. 组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的所有不同组合 ,并以列表形式返回。

candidates 中的 同一个 数字可无限制重复选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 对于给定的输入,保证和为 target 的不同组合数少于 150 个。

解析:先对数组进行排序,可更好的进行剪枝操作

1.确定函数返回值和参数(arr, target, sum, startIndex)

2.确定终止条件:

if(sum > target) return;
if(sum == target) {
    result.push(path);
    return;
}

3.单层搜索逻辑:单层for循环依然是从startIndex开始,搜索candidates集合。

for (int i = startIndex; i < candidates.size(); i++) {
    sum += candidates[i];
    path.push(candidates[i]);
    backtracking(candidates, target, sum, i); // 关键点:不用i+1了,表示可以重复读取当前的数
    sum -= candidates[i];   // 回溯
    path.pop_back();        // 回溯
}

剪枝优化Java代码实现

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);   // 先进行排序,方便剪枝
        backtracking(candidates, target, 0, 0);   //回溯操作
        return res;
    }

    public void backtracking(int[] candidates, int target, int sum, int index) {
        if (sum == target) {   // 找到数字 和为 target 的组合
            res.add(new ArrayList<>(path));   //添加到path组合
        }

        for (int i = index; i < candidates.length; i++) {
            if (sum + candidates[i] > target) break;   // 若求和 > target 则终止遍历
            path.add(candidates[i]);   //未超出target则加入到path,再次进行回溯操作
            backtracking(candidates, target, sum + candidates[i], i);
            path.remove(path.size() - 1); // 回溯之后,移除 path 最后添加的一个元素
        }
    }
}

三、分割回文串

3.1【编程题】分割回文串

131. 分割回文串

给一个字符串 s,请将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。回文串 是正着读和反着读都一样的字符串。

输入:s = "aab"

输出:[["a","a","b"],["aa","b"]]

递归纵向遍历,for循环横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。 

解析:伪代码编写  

1.确定函数的返回值和参数(str,startIndex)

2.确定终止条件

void backtracking (String str, int startIndex) {
    // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
    if (startIndex >= str.size()) {
        result.push(path);
    }
}

3.单层搜索逻辑:到达叶子节点收集结果

for (int i = startIndex; i < str.size(); i++) {
    if (isPalindrome(str, startIndex, i)) { // 是回文子串
        // 获取[startIndex,i]在str中的子串
        string str1 = str.substr(startIndex, i - startIndex + 1);
        path.push(str1);
    } else {                // 如果不是则直接跳过
        continue;
    }
    backtracking(str, i + 1); // 寻找i+1为起始位置的子串
    path.pop();        // 回溯过程,弹出本次已经填在的子串
}

整体代码实现:

class Solution {
    List<List<String>> lists = new ArrayList<>();  //lists存放子串属于回文串的集合
    Deque<String> deque = new LinkedList<>();

    public List<List<String>> partition(String s) {
        backTracking(s, 0);
        return lists;
    }

    private void backTracking(String s, int startIndex) {
        if (startIndex >= s.length()) {  //终止条件:遍历到最后一个元素后
            lists.add(new ArrayList(deque));  //每次都把找到的子串放入到lists ,终止处是符合回文的
        }
        for (int i = startIndex; i < s.length(); i++) {
            
            if (isPalindrome(s, startIndex, i)) {   //如果是回文子串,则将其添加到deque队列
                String str = s.substring(startIndex, i + 1);
                deque.addLast(str);
            } else {
                backTracking(s, i + 1);   //起始位置后移,保证不重复
                deque.removeLast();
            }
        }
    }
    //判断是否是回文串
    private boolean isPalindrome(String s, int startIndex, int end) {
        for (int i = startIndex, j = end; i < j; i++, j--) {
            if (s.charAt(i) != s.charAt(j)) {
                return false;
            }
        }
        return true;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值