代码随想录算法训练营Day19| LeetCode 77 组合、216 组合总和 III、17 电话号码的字母组合

理论基础

回溯的本质是穷举,也就是暴力求解,它是递归的一部分。

所有回溯法解决的问题都可以抽象为树形结构,因为回溯法解决的都是在集合中递归查找子集,集合的大小构成了树的宽度,递归的深度就构成了树的深度(cr. 代码随想录)。

应用

回溯一般被用于以下几种问题(cr. 代码随想录)的求解中:

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

回溯法之所以常用于解决这些问题,是因为它能够把复杂的组合搜索问题转化为一系列简单的决策,并通过剪枝策略降低搜索量,而其他方法通常要么不够通用,要么求解效率更低。

代码模板(cr. 代码随想录)

  • for循环:横向遍历
  • 递归:纵向遍历
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

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

力扣 77 组合

思路是按照顺序取数放入集合,再在剩余的数组中取剩下的 k − 1 k-1 k1 个数。for循环中无需担心i+1>end的情况,因为那样下一层递归不会进入循环。算是半AC,绕了一下,但是最终改出来了。

class Solution {
private:
    int end;
    vector<vector<int>> ans;
    vector<int> count;

public:
    void backtracking(int begin, int end, int k){
        if(k == 0){
            ans.push_back(count);
        }
        for(int i = begin; i <= end; i++){
            if(end - i + 1 < k) return; //剪枝操作
            count.push_back(i);
            backtracking(i + 1, end, k - 1);
            count.pop_back();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backtracking(1, n, k);
        return ans;
    }
};

注意,代码随想录中提到的 O ( n × 2 n ) O(n × 2^n) O(n×2n) 是针对生成所有子集的情况,因为子集总数是 2 n 2^n 2n,每个子集需要 O ( n ) O(n) O(n) 的处理时间。

而本题是针对固定 k k k 的组合问题,因此解法的时间复杂度为 O ( C ( n , k ) × k ) O(C(n,k) × k) O(C(n,k)×k) ,其中 C ( n , k ) C(n,k) C(n,k) 是组合数。如果 k k k 是固定常数,则 C ( n , k ) C(n,k) C(n,k) 是多项式级别( 大约 O ( n k ) 大约 O(n^k) 大约O(nk)),所以整体复杂度是 O ( n k × k ) O(n^k × k) O(nk×k),当 n n n 较大时,其远低于 O ( n × 2 n ) O(n × 2^n) O(n×2n)

P.S. C ( n , k ) C(n,k) C(n,k)(也写作 ( n k ) \binom{n}{k} (kn)是组合数学中的一个符号,表示从 n n n 个元素中不考虑顺序选择 k k k 个元素的方案数。其计算公式是:

( n k ) = n ! k ! ( n − k ) ! \binom{n}{k} = \frac{n!}{k!(n-k)!} (kn)=k!(nk)!n!

力扣 216 组合总和 III AC

和前面一道题有点相似,套用回溯模板还算简单,时间复杂度同上。

class Solution {
private:
    vector<vector<int>> ans;
    vector<int> path;
public:
    void backTracking(int begin, int end, int k, int n){
        if(k == 0 && n == 0){
            ans.push_back(path);
            return;
        }
        for(int i = begin; i <= end; i++){
            if(end - i + 1 < k || i > n) return;
            path.push_back(i);
            backTracking(i+1, end, k-1, n-i);
            path.pop_back();
        }

    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backTracking(1,9,k,n);
        return ans;
    }
};

力扣 17 电话号码的字母组合 AC

以下是第一遍的代码,但是事实上并不需要一个额外的for循环,每次处理一个数字即可。并且通常来说对于固定大小和已知元素数量的数据,使用数组往往会比使用 vector 更高效。

class Solution {
private:
    string path;
    vector<string> ans;
    int len;
    const vector<string> keyboard = {"abc", "def", "ghi", "jkl", "mno","pqrs", "tuv", "wxyz"};

public:
    void backTracking(int start, string &digits, const vector<string> &k){
        if(path.size() == len){
            ans.push_back(path);
            return;
        }
        for(int i = start; i < len; i++){
            int num = digits[i] - '0' - 2;
            for(int j = 0; j < k[num].size(); j++){
                path.push_back(k[num][j]);
                backTracking(i + 1, digits, k);
                path.pop_back();
            }
        }
    }
    vector<string> letterCombinations(string digits) {
        len = digits.size();
        if(len == 0) return ans;
        backTracking(0, digits, keyboard);
        return ans;
    }
};

可以优化代码如下:

class Solution {
private:
    string path;
    vector<string> ans;
    int len;
    //mark一下: array在定义时必须指定大小
    const string keyboard[8] = {"abc", "def", "ghi", "jkl", "mno","pqrs", "tuv", "wxyz"};

public:
    void backTracking(int idx, string &digits){
        if(path.size() == len){
            ans.push_back(path);
            return;
        }

        int num = digits[idx] - '0' - 2;
        for(int j = 0; j < keyboard[num].size(); j++){
            path.push_back(keyboard[num][j]);
            backTracking(idx + 1, digits);
            path.pop_back();
        }
        
    }
    vector<string> letterCombinations(string digits) {
        len = digits.size();
        if(len == 0) return ans;
        backTracking(0, digits);
        return ans;
    }
};

你可能感兴趣的:(代码随想录算法训练营跟练,算法,leetcode,c++,数据结构,递归,回溯)