回溯算法part1 - 回溯算法基础|77.组合|216.组合总和III|17.电话号码的字母组合

回溯算法

什么是回溯法?

回溯法指的是回溯搜索法,是一种搜索方式。比如在二叉树的搜索中,在递归的过程中实际上蕴涵了回溯,有递归就有回溯,回溯函数即为递归函数

回溯法的性能

回溯并不算高效,其本质还是穷举所有可能,从中找出我们想要的答案,相当于暴力搜索

回溯法的题目类型

  1. 组合问题:给定一个集合,找出满足某个条件的所有组合,不考虑元素顺序
  2. 切割问题:给定一个字符串,问有哪几种切割方式
  3. 子集问题:给定一个集合,列出所有子集
  4. 排列问题:和组合问题类似,但强调元素顺序
  5. 棋盘问题:N皇后,数独等

这些问题用普通的迭代for循环是没有办法解决的,需要用到回溯的穷举。

如何理解回溯?

回溯法的相关问题其实都可以转化为树形结构。回溯法的思路都是在集合中递归查找子集,集合的大小为树的宽度,递归的次数决定了树的深度。回溯也一定会有终止条件,因此这个树是高度有限的N叉树

回溯法模板

回溯算法一般没有返回值,主要是用来在最后收集结果。参数可能在一开始没法明确确定下来,因此一般是先写逻辑再补需要的参数。和递归一样,首先要确认回溯终止条件并存放结果。回溯中的for循环是横向遍历,也就是遍历一个节点所有的孩子,每次循环中的递归函数就是纵向遍历。

public void backtracking(参数) {
    if (终止条件) {
        收集结果;
        return;
    }

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

LeetCode - 77. Combinations 组合

解题目标:给定两个整数n和k,列出所有长度为k的并且范围为在[1,n]的所有组合。

解题思路:假如没有回溯算法,这道题比较浅显的想法就是假如k=2,那么就嵌套两层for循环把所有的可能情况给打印出来,但是如果k是一个很大的值,那么显然这个思路是不可行的。回溯算法的目的就是用递归来简化for循环的操作。假如k=2的话,这里的回溯法就是两层回溯。注意组合的定义是不注重顺序的,e.g.  {1, 2}和{2, 1}是重复的组合。

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;
    }

    //startIdx是每一次循环开始的的元素下标
    public void backtracking(int n, int k, int startIdx) {
        //终止条件,当收集满k个元素后加入结果集
        if (path.size() == k) {
            res.add(new ArrayList(path));
            return;
        }
        //对于其中一层
        for (int i = startIdx; i <= n; i++) {
            path.add(i);
            backtracking(n, k, i+1);
            path.removeLast(); //回溯
        }
    }
}

LeetCode 216. Combination Sum III 组合总和III

解题目标:给定整数k和n,接触所有长度为k的由数字[1, 9]组成相加等于n的组合

解题思路:这题和上一题的思路类似,但是回溯的时候多了一个当前的sum,并且在终止条件要加上sum==n

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

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

    }
}

LeetCode - 17. 电话号码的字母组合

解题目标:给定一个数字字符串,找出对应的不同的数字相对应的字母组合

解题思路:这道题因为是涉及到数字映射为字母,这里可以使用二维数组来做映射,因为数字可以当成二维数组外层的下标来使用,这样就可以用数字直接找到数字所对应的字母集。但是当在搜索字母组合的时候,如果碰到多个数字还使用嵌套多个for循环的暴力解法依然会导致代码冗余,因此这题还是要用到回溯法。这里需要注意,回溯函数中这里不需要startIdx,因为我们这时候求的是两个集合中的组合,这个index应该是表示当前数字字符串中遍历到的index,和要去map中找字母的index不一样。map中对应的是数字字母对应的值,因此需要减去字母0来算出ASCII码转换成真正的index。

 重点是下一层的递归中我们要传入的是下一位数字的index,因此这道题关键点在于搞清楚什么是当前数字字符串遍历到的index,以及什么是map中寻找字母要用到的index。

class Solution {
    private List<String> res = new ArrayList<>();
    StringBuilder comb = new StringBuilder();
    private String[] letterMap = {
        "", // 0
        "", // 1
        "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 digitIdx) {
        //终止条件,当遍历到最后一个数字遍历完后才开始收集结果
        if (digitIdx == digits.length()) {
            res.add(new StringBuilder(comb).toString());
            return;
        }

        //获取数字(字母)对应下标
        int cur_digit = digits.charAt(digitIdx) - '0';
        //获取数字所对应的字母集
        String letters = letterMap[cur_digit];

        for (int i = 0; i < letters.length(); i++) {
                comb.append(letters.charAt(i));
                //下一层递归应该对应下一个数字
                backtracking(digits, digitIdx + 1);
                comb.deleteCharAt(comb.length() - 1); //回溯
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值