77. 组合、216. 组合总和 III、17. 电话号码的字母组合

77. 组合

题目描述:

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

你可以按 任何顺序 返回答案。

解答:

如果按照暴力来想,直接for循环,但是这样对于k较小的是可以,但是如果k为100,总不能写100层的for循环吧,那也太离谱了。

所以还是应该想想其他办法。

组合问题——回溯!

在做二叉树的时候222. 完全二叉树的节点个数、110. 平衡二叉树、257.二叉树的所有路径_清榎的博客-CSDN博客t

提到过回溯。回溯和递归是一体的,属于是那种你中有我,我中有你的关系。 

回溯。回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

所以其实回溯法并不高效,但是遇到一些问题,没办法,除了暴力就是回溯了,最多是 回溯+剪枝。

先插个眼,回溯法,一般可以解决如下几种问题:

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

回溯可以把问题抽象成一棵树,树的宽度和深度都有限,其中深度也就是递归的层数,宽度就是问题的大小

表现在编程时也就是深度使用递归,宽度使用for循环,因此 回溯的一般写法为:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

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

接下来就正式解题了,

为了思路清晰仿照递归三部曲也整一个回溯三部曲:

(1)参数及返回值:

        n,k肯定需要当作参数,还有下次从哪开始找也需要记录

        返回值可以不需要,直接开全局变量记录即可 。

(2)终止条件:

        树搜索到叶子就可以终止了,叶子是什么情况呢?

        满足k个数——即为path中数量为k

(3)内部处理逻辑:

        先放入当前访问的节点,再递归,递归完成后进行回溯。

代码实现:

class Solution {
public:
    vector<vector<int>>result;
    vector<int>path;
    void backTrace(int n, int k, int start){
        if (path.size() == k){
            result.push_back(path);
            return;
        }
        for (int i = start; i <= n; i++){
            path.push_back(i);
            backTrace(n, k, i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backTrace(n, k, 1);
        return result;
    }
};

剪枝优化:

前面提到回溯往往可以进行剪枝,这道题也可以。

比如(4,4)这种情况。

后面的数根本没四个了,还判断啥,没必要了,直接剪枝。

所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置

如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了

修改for循环条件:n - (k - path.size()) + 1

216. 组合总和 III

题目描述:

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

只使用数字1到9
每个数字 最多使用一次 
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

解答:

这道题和上道题十分相似,不一样的是把树的宽度给隐藏了

很清楚,树的深度为k,那宽度应该取多少?

数字1-9,所以宽度为9

这样一来就简单了,每次取一个值放入path中,一直取到path中个数等于k为止。

再判断是否等于n,等于就放入result里。

然后返回,回溯 ,继续往下寻找即可。

回溯三部曲:

(1)参数及返回值:

        n,k肯定需要当作参数,还有下次从哪开始找也需要记录

        返回值可以不需要,直接开全局变量记录即可 。

(2)终止条件:

        树搜索到叶子就可以终止了,叶子是什么情况呢?

        满足k个数——即为path中数量为k

(3)内部处理逻辑:

        先放入当前访问的节点,再递归,递归完成后进行回溯。

代码实现:

class Solution {
public:
    vector<vector<int>>result;
    vector<int>path;
    void backTrace(int n, int k, int start){
        if (path.size() == k){
            int sum = 0;
            for (int i = 0; i < k; i++){
                sum += path[i];
            }
            if (sum == n)
                result.push_back(path);
            return;
        }
        for (int i = start; i <= 9; i++){
            path.push_back(i);
            backTrace(n, k, i + 1);
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backTrace(n, k, 1);
        return result;
    }
};

剪枝优化:

比较容易想到的是,如果我们当前节点的sum和已经大于等于n,那就没有继续往下寻找的必要了,直接剪枝。

这种需要将sum作为参数进行传递,同时回溯时也需要相应对sum进行修改。

代码实现:

class Solution {
public:
    vector<vector<int>>result;
    vector<int>path;
    void backTrace(int n, int k, int start, int sum){
        //剪枝操作
        if (sum > n)
            return;
        if (path.size() == k){
            if (sum == n)
                result.push_back(path);
            return;
        }
        for (int i = start; i <= 9; i++){
            path.push_back(i);
            sum += i;
            backTrace(n, k, i + 1, sum);
            sum -= i;
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backTrace(n, k, 1, 0);
        return result;
    }
};

当然也可以和上一题一样,对for循环进行剪枝,条件改为

9 - (k - path.size()) + 1

 即可。

17. 电话号码的字母组合

题目描述:

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

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

解答:

这个问题本质上和前两题还是类似。

需要考虑三个问题:

数字和字母如何映射?

树的宽度?

树的深度?

字母和数字的映射可以采用map或者数组,如下所示:

const string letterMap[10] = {
        "", // 0
        "", // 1
        "abc", // 2
        "def", // 3
        "ghi", // 4
        "jkl", // 5
        "mno", // 6
        "pqrs", // 7
        "tuv", // 8
        "wxyz", // 9
    };

 至于树的宽度深度,我们拿到一串数字,数字的长度其实就是树的深度;每一个数字都会对应三个或者四个字母,这便是树的宽度。如下图所示

17. 电话号码的字母组合

考虑清楚这三个问题就可以开始编写代码了。

回溯三要素:

(1)参数和返回值:

首先需要一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来,这两个变量依然定义为全局。

再来看参数,参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index,标识从哪个数字开始。

(2)终止条件:长度等于输入的数字长度,记录进结果中。

(3)内部处理逻辑:拿到数字先获取到其对应的字母,然后进入循环,递归。 

        int digit = digits[index] - '0';        // 将index指向的数字转为int
        string letters = letterMap[digit];      // 取数字对应的字符集
        for (int i = 0; i < letters.size(); i++) {
            s.push_back(letters[i]);            // 处理
            backTrace(digits, index + 1);    // 递归,注意index+1,一下层要处理下一个数字了
            s.pop_back();                       // 回溯
        }

代码实现:

class Solution {
public:
    const string letterMap[10] = {
        "", // 0
        "", // 1
        "abc", // 2
        "def", // 3
        "ghi", // 4
        "jkl", // 5
        "mno", // 6
        "pqrs", // 7
        "tuv", // 8
        "wxyz", // 9
    };
    vector<string> result;
    string s;
    void backTrace(const string& digits, int index){
        if (index == digits.size()) {
            result.push_back(s);
            return;
        }
        int digit = digits[index] - '0';        // 将index指向的数字转为int
        string letters = letterMap[digit];      // 取数字对应的字符集
        for (int i = 0; i < letters.size(); i++) {
            s.push_back(letters[i]);            // 处理
            backTrace(digits, index + 1);    // 递归,注意index+1,一下层要处理下一个数字了
            s.pop_back();                       // 回溯
        }
    }
    vector<string> letterCombinations(string digits) {
        if (digits.size() == 0) {
            return result;
        }
        backTrace(digits, 0);
        return result;
    }
};

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值