代码随想录第二十二天 | Leecode 77. 组合、216. 组合总和 III、17. 电话号码的字母组合

Leecode 77. 组合

题目描述

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

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

  • 示例 1:

输入:n = 4, k = 2
输出:

[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]
  • 示例 2:

输入:n = 1, k = 1
输出:[[1]]

解题思路与代码展示

本题要求给出所有满足组合情况,使用穷举法逐个尝试,如果满足则存入结果数组中。但直接写的过程中会遇到两个比较难处理的点:

  • 如果每次要求的数的个数固定,比如满足条件的3个数存入数组中,那么就只需使用3层for循环即可,即需要几个数就使用几层for循环。但题目所给的数的个数并不固定,故此时需要使用回溯(递归)的方式来进行处理。
  • 此外,还需要确保输出的数组没有重复,即每次搜索过的数不会进行二次搜索。为了避免重复查找,可以设置一个查找的初始值,每次递归的时候传入该值并从这个值开始往后递归搜索。

综合上面思路,可以写出代码如下:

class Solution {
public:
    vector<vector<int>> result; // 使用两个全局变量来存储结果值和过程中每一次搜索的结果
    vector<int> curVec;

    void combineHelper(int n, int k, int startindex){ // 递归函数
        if(curVec.size() == k) { // 如果vector当前大小为k,则说明已经符合条件,直接push存放入结果中
            result.push_back(curVec); 
            return;
        }
        for(int i = startindex; i <= n-(k-curVec.size()) + 1; i++){ // 使用for循环遍历,相当于每次递归调用都有一层for循环;并从startindex开始搜索。其中终止条件中i的上界是剪枝后的结果,排除了一些不可能的情况,可以加速搜索。
            curVec.push_back(i);  // 将当前数存入当前vector中
            combineHelper(n, k, i+1); // 递归调用
            curVec.pop_back(); // 回溯退回
        }
    }

    vector<vector<int>> combine(int n, int k) {
        combineHelper(n,k,1); // 递归调用辅助函数
        return result; // 返回结果
    }
};

Leecode 216. 组合总和 III

题目描述

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

  • 只使用数字19
  • 每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

  • 示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

  • 示例 2:

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。

  • 示例 3:

输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
[1,9]范围内使用4个不同的数字,我们可以得到的最小和是 1 + 2 + 3 + 4 = 10,因为10 > 1,没有有效的组合。

解题思路与代码展示

本题和上面一题比较类似,使用相似的方法即可,区别在于递归终止条件中要多考虑如果求和值大于目标的情况,此时不满足条件,直接退回即可。

class Solution {
public:
    vector<int> curVec; // 使用vector和curSum来记录当前状态,用于判断是否满足条件
    int curSum;
    vector<vector<int>> result; // result用于存放最终结果

    void combHelper(int k, int n, int startNum){
        if(curSum == n && curVec.size() == k){ // 如果数求和与数的个数都恰好满足条件,则说明此时已经满足
            result.push_back(curVec); // 将满足条件的vector存入结果中
        }
        else if(curSum > n) return; // 如果求和大于目标值,则一定不满足,此时直接返回即可
        
        for(int i = startNum; i < 10 ; i++){ // 每一位数都从startNum开始递归求解
            curSum += i; // 更新当前总和,更新存储的vec向量
            curVec.push_back(i);
            combHelper(k,n,i+1); // 递归搜索下一个数
            curSum -= i; // 回溯,此时要返回上一次的状态,故需要同时恢复curSum和curVec
            curVec.pop_back(); 
        }
    }

    vector<vector<int>> combinationSum3(int k, int n) {
        combHelper(k, n, 1);
        return result;
    }
};

上面代码关键在于递归返回进行回溯时,需要恢复所有递归过程中被修改的变量,在本题中需要同时回复curSum和curVec。

Leecode 17. 电话号码的字母组合

题目描述

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

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

  • 示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

  • 示例 2:

输入:digits = ""
输出:[]

  • 示例 3:

输入:digits = "2"
输出:["a","b","c"]

解题思路与代码展示

本题思路上不算难,在确定了使用回溯的方法后,只需要根据每次数字的不同从相应可能得字母字符中依次选取进行回溯递归即可。但困难的点在于,总共有字母的按键包括2-9总共8个键,如果每一个键用一个if,同时内部使用一个for循环进行回溯,会导致写很多没必要的重复代码。为了避免这种情况需要使用一些方法来进行处理。我首先想到的第一种方法是合并前边2-6个数字,因为前5个数每个数都刚好对应3个字母,因此可以使用乘除法和取余运算来进行计算当前字母对应哪些字母。但需要注意的是,这个计算过程中需要非常小心int类型与char类型之间的转换,1的ASCII码并不等于1,中间需要加减一个'0'才能进行转换。

使用这种方法可以写出代码如下:

class Solution {
public:
    vector<string> result; // 同样是使用全局变量来存储结果和当前状态的字符串
    string curStr;

    void combinationHelper(string digits, int curIndex){
        if(curIndex == digits.size()) { // 如果所有数字已经遍历结束,则将当前字符串存入结果中,并返回
            result.push_back(curStr);
            return;
        }
        if(digits[curIndex] < '7'){ // 对于前2-6的数,每个数都对应三个字母,故可以放在一起处理
            for(int i = 0; i < 3; i++){ // for循环遍历每个数对应的三个字母
                curStr += ('a' + (digits[curIndex]-'0'-2)*3 ) + i%3; // 存入一个字母,此时要注意字符与int之间转换需要减去一个值
                combinationHelper(digits, curIndex+1); // 递归调用存入下一个数字对应的字母
                curStr.pop_back(); // 回溯
            }
        }
        else if(digits[curIndex] == '7' ){ // 处理完前2-6之后,采用同样的方式处理7,8,9,区别在于7和9对应4个字母,其余都一样
            for(int i = 0 ; i < 4; i++){
                curStr += 'p' + i;
                combinationHelper(digits, curIndex+1);
                curStr.pop_back();
            }
        }
        else if(digits[curIndex] == '8'){
            for(int i = 0; i < 3; i++){
                curStr += 't' + i;
                combinationHelper(digits, curIndex+1);
                curStr.pop_back();
            }
        }
        else if(digits[curIndex] == '9'){
            for(int i = 0 ; i < 4; i++){
                curStr += 'w' + i;
                combinationHelper(digits, curIndex+1);
                curStr.pop_back();
            }
        }
    }

    vector<string> letterCombinations(string digits) {
        if(!digits.size()) return result;  // 如果输入的字符串为空,则直接返回
        combinationHelper(digits, 0); // 使用回溯递归调用,将所有可能结果都存入result
        return result; // 返回result结果
    }
};

可以看到,上面代码虽然确实在合并了一些情况的同时也能够处理本题中的所有情况,但是对于数字取到7-9时,还是有大量重复代码。为了简化这部分内容,考虑使用一个string类型的数组来存放每个数字对应的字母。即:

string numLetterMap[10]{
     "",
     "",
     "abc",
    "def",
    "ghi",
    "jkl",
    "mno",
    "pqrs",
    "tuv",
    "wxyz"
};

这样每次使用将按键数字转换为字母字符的时候,直接去数组中查找即可,这样我们可以修改得到下面的代码:

class Solution {
public:
    string numLetterMap[10]{ // 存放每个按键对应的字母字符
    "",
    "",
    "abc",
    "def",
    "ghi",
    "jkl",
    "mno",
    "pqrs",
    "tuv",
    "wxyz"
    };

    void combHelper(vector<string>& result, string digits, string curStr){
        if(curStr.size() == digits.size()){ // 如果已经长度相等,说明已经完成,此时需要存入结果中
            result.push_back(curStr);
            return;
        }
        int num = digits[curStr.size()] - '0'; // 首先将char类型的数字转换为int类型
        for(int i = 0; i < numLetterMap[num].size(); i++){ // for循环下遍历所有取值,注意此时循环次数取决于字符串数组中对应位置的长度
            curStr += numLetterMap[num][i]; // 当前字符串上加一个字母
            combHelper(result, digits, curStr); // 递归
            curStr.pop_back(); // 回溯
        }
    }

    vector<string> letterCombinations(string digits) {
        vector<string> result;
        if(!digits.size()) return result;
        string curStr = "";
        combHelper(result, digits ,curStr);
        return result;
    }
};

通过上面代码可以学到,之后再遇到讨论情况比较多的时候,为了避免使用多个if或是switch等造成讨论情况过多,可以考虑使用一些数据类型来存储一部分信息,从而简便讨论。

今日总结

今天学习到了回溯算法,了解到回溯和递归的密不可分。回溯过程中会有一个或多个变量用于存放“当前状态”,每一次使当前状态变为下一状态之后调用递归函数进行处理,递归结束之后还需要将当前进行恢复。同时回溯可以考虑使用一个全局变量来存放结果和当前状态,或者是用引用传递的方式来确保可以进行修改。

此外,当遇到需要讨论的情况比较多的时候,考虑能否使用一个数组来存储不同情况下的一些取值,从而减少if讨论分支,减少代码量。

力扣当前刷到82题了,继续坚持

posted on 2025-04-16 21:11  JQ_Luke  阅读(456)  评论(0)    收藏  举报