代码随想录算法训练营打卡Day25 | LeetCode216 组合总和III、LeetCode17 电话号码的字母组合

摘要

  • 剪枝操作并不会改变回溯算法的暴力搜索的本质,但确实可以减少一些不必要的搜索。
  • 在单层递归中,要注意区分组合问题和排列问题,仔细分析结果的构造过程。

LeetCode77 组合

77. 组合 - 力扣(Leetcode)

  • 先回顾一下没有剪枝的回溯
class Solution {
public:
    void backtracking(vector<vector<int>>& res, int& n, int& k, vector<int>& cur, int j) {
        if (cur.size() == k) {
            res.push_back(cur);
            return;
        }
        for (int i = j; i <= n; i++) {
            cur.push_back(i);
            backtracking(res, n, k, cur, i + 1);
            cur.pop_back();
        }
        return;
    } 
    vector<vector<int>> combine(int n, int k) {
        vector<vector<int>> res;
        vector<int> cur;
        backtracking(res, n, k, cur, 1);
        return res;
    }
};
剪枝前
  • 剪枝前是28ms。
  • 从答案的构造过程来看,不是所有的cursize都能到达k,而且这是可以提前判断的。当剩余能加入组合的元素不足以让cur.size()达到k时,就不需要再搜索这个节点之后的子树。
  • 例如n = 4, k = 4,其答案构造过程的树形如下
答案构造过程的树
  • 可见,当剩余能加入组合元素的个数已经不足以让cur.size()达到k时,就不需要再向下搜索。体现在上述例子中,比如第二层的{2}{3}{4},向下构造答案,都不能再加入3个数使得cur.size()达到k,所以是不需要再搜索的,可以直接剪枝。
  • 本题的剪枝条件为剩余可选元素个数不足以让组合中的元素个数达到k。体现在代码中就是for语句的循环条件。即集合中剩余可选元素个数n-i >= 所需需要的元素个数k - path.size()
class Solution {
public:
    void backtracking(vector<vector<int>>& res, int& n, int& k, vector<int>& cur, int j) {
        if (cur.size() == k) {
            res.push_back(cur);
            return;
        }
        for (int i = j; i <= n - (k - cur.size()) + 1; i++) {
            cur.push_back(i);
            backtracking(res, n, k, cur, i + 1);
            cur.pop_back();
        }
        return;
    } 
    vector<vector<int>> combine(int n, int k) {
        vector<vector<int>> res;
        vector<int> cur;
        backtracking(res, n, k, cur, 1);
        return res;
    }
};
剪枝后

可见剪枝能在一定程度上提升回溯法的性能。

LeetCode216 组合总和III

216. 组合总和 III - 力扣(Leetcode)

  • 初见题目的想法:和组合问题类似。复习回溯法的思考步骤:
    • 确定递归函数的终止条件:当前组合的元素个数等于k;需要注意的是递归的终止条件不一定是收集答案的条件,在这道题中还要加额外的判断,即当前组合内元素的和为n时才收集答案。
    • 在分析问题的过程中确定递归函数需要的参数:
    • 确定单层递归的处理逻辑
    • 思考可能的剪枝条件

初见的题解代码如下,只考虑了使用最大和和最小和来进行剪枝

class Solution {
public:
    void backtracking(int& k, int& n, vector<vector<int>>& res, 
                    vector<int>& cur, int& sum, int start) 
    {
        if (cur.size() == k) {
            if (sum == n) res.push_back(cur);
            return;
        }
        for (int i = start; i <= 9; i++) {
            cur.push_back(i);
            sum += i;
            backtracking(k, n, res, cur, sum, i + 1);
            sum -= i;
            cur.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        vector<vector<int>> res;
        
        int minSum = 0;
        for (int i = 1; i <= k; i++) minSum += i;
        if (n < minSum) return res;

        int maxSum = 0;
        for (int i = 9; i > 9 - k; i--) maxSum += i;
        if (n > maxSum) return res;

        vector<int> cur;
        int sum = 0;
        int start = 1;
        backtracking(k, n, res, cur, sum, start);
        return res;
    }
};
  • 剪枝:由于sum是递增的,如果当前的sum已经大于n,则不需要再向下搜索。

添加剪枝条件的题解代码,有两种实现方式,一是在递归终止条件里添加,二是在for循环中添加

  • 通过递归终止条件进行剪枝
void backtracking(int& k, int& n, vector<vector<int>>& res, 
                vector<int>& cur, int& sum, int start) 
{
    if (sum > n) return;
    if (cur.size() == k) {
        if (sum == n) res.push_back(cur);
        return;
    }
    for (int i = start; i <= 9; i++) {
        cur.push_back(i);
        sum += i;
        backtracking(k, n, res, cur, sum, i + 1);
        sum -= i;
        cur.pop_back();
    }
}
  • for循环中进行剪枝
void backtracking(int& k, int& n, vector<vector<int>>& res, 
                    vector<int>& cur, int& sum, int start) 
{   
    if (cur.size() == k) {
        if (sum == n) res.push_back(cur);
        return;
    }
    for (int i = start; i <= 9; i++) {
        cur.push_back(i);
        sum += i;
        if (sum <= n) backtracking(k, n, res, cur, sum, i + 1);
        sum -= i;
        cur.pop_back();
    }
}

LeetCode17 电话号码的字母组合

17. 电话号码的字母组合 - 力扣(Leetcode)

  • 初见题目时的想法:按回溯法的三部曲进行思考
    • 递归的终止条件:当前字母组合的长度与输入数字的长度相等,收集结果。
    • 递归函数需要的参数:保存输入数字的字符串,收集结果的结果集,当前字母组合,当前处理到第几位输入数字。
    • 单层递归的逻辑:遍历当前输入的数字对应的字母,向下搜索答案。

初见题目的题解代码如下

class Solution {
public:
    vector<char> button(char d) {
        switch (d) {
            case '2': return {'a', 'b', 'c'};
            case '3': return {'d', 'e', 'f'};
            case '4': return {'g', 'h', 'i'};
            case '5': return {'j', 'k', 'l'};
            case '6': return {'m', 'n', 'o'};
            case '7': return {'p', 'q', 'r', 's'};
            case '8': return {'t', 'u', 'v'};
            case '9': return {'w', 'x', 'y', 'z'};
            default: return {};
        }
    }
    void backtracking(const string& digits, vector<string>& res,
                    int start, string& cur)
    {
        if (cur.size() == digits.size()) {
            res.push_back(cur);
            return;
        }
        for (int i = start; i < digits.size(); i++) {
            for (auto& iter : button(digits[i])) {
                cur.push_back(iter);
                backtracking(digits, res, i + 1, cur);
                cur.pop_back();
            }
        }
    }
    vector<string> letterCombinations(string digits) {
        vector<string> res;
        if (digits.size() == 0) return res;
        string cur;
        backtracking(digits, res, 0, cur);
        return res;
    }
};
  • 外面这一层for循环是没有必要的。
  • 什么时候需要这外面一层的for循环呢?如果是组合问题,则需要外面这层for。如果是排列问题,则不需要这层for。因为输入数字是有序的,其对应的字母组合也是有序的,所以不存在跳过一个输入数字,去处理再下一位输入数字的情况,
    • 其实有外面这层for循环也不影响结果,因为跳过一位输入数字会使得最终构造出的字母串不够长,不会被收入结果集。但是,这样会做很多不必要的搜索。
// for (int i = start; i < digits.size(); i++) {
    for (auto& iter : button(digits[i])) {
        cur.push_back(iter);
        backtracking(digits, res, i + 1, cur);
        cur.pop_back();
    }
// }
  • 要对组合问题和排列问题做好区分。

修改后的题解代码如下

class Solution {
public:
    vector<char> button(char d) {
        switch (d) {
            case '2': return {'a', 'b', 'c'};
            case '3': return {'d', 'e', 'f'};
            case '4': return {'g', 'h', 'i'};
            case '5': return {'j', 'k', 'l'};
            case '6': return {'m', 'n', 'o'};
            case '7': return {'p', 'q', 'r', 's'};
            case '8': return {'t', 'u', 'v'};
            case '9': return {'w', 'x', 'y', 'z'};
            default: return {};
        }
    }
    void backtracking(const string& digits, vector<string>& res,
                    int start, string& cur)
    {
        if (cur.size() == digits.size()) {
            res.push_back(cur);
            return;
        }
        for (auto& iter : button(digits[start])) {
            cur.push_back(iter);
            backtracking(digits, res, start + 1, cur);
            cur.pop_back();
        }
    }
    vector<string> letterCombinations(string digits) {
        vector<string> res;
        if (digits.size() == 0) return res;
        string cur;
        backtracking(digits, res, 0, cur);
        return res;
    }
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343