算法练习第21天|216.组合总和|||、17.电话号码的字母组合

216.组合总和 III

216. 组合总和 III - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/combination-sum-iii/

题目描述:

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

  • 只使用数字1到9
  • 每个数字 最多使用一次 

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

示例 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,没有有效的组合。

思路分析:

这道题是在77.组合的基础上做了改进,相关回溯解法可以参考上一篇博文:

算法练习第20天|回溯算法 77.组合问题 257. 二叉树的所有路径-CSDN博客

和77组合题目一样,本题抽象成树型结构的逻辑如下(图片来自卡哥的代码随想录): 

  使用回溯的解法,我们依然要使用path和result两个vector,一个用于记录遍历路径,另一个用于记录满足条件的结果:

vector<vector<int>> result;  //满足条件的结果
vector<int> path;  //当前路径

下面我们回顾一下上一篇博文中所讲的回溯三部曲

第一步:确认回溯函数的参数和返回值。

void backtracking(参数)
{
}

一般来说,回溯函数的返回值类型为void,至于参数,为了表达方便,我们定义了目标和targetSum(即题目中的n)、k、遍历到当前路径的和sum、以及每一层回溯的开始索引startIndex。具体代码如下:

// int targetSum; //目标和
// int k;  //k个元素
// int sum;  //当前path记录的元素的和
// int startIndex;  //开始的索引
//回溯第一步:确认回溯函数的参数以及返回值
void backTracking(int targetSum, int k, int sum, int startIndex)
{
}

第二步:确认回溯的终止条件。

if (终止条件) {
    存放结果;
    return;
}

什么时候本次回溯终止?那就是我们成功的找到了一组数据,里面有k个元素,且它们的和为n。那么终止条件就是 path.size() == k && sum == targetSum。找到这一组数据则需要将其记录在结果result中,然后return。具体代码如下:

if(path.size() == k && sum == targetSum)
{
    result.push_back(path);
    return;
}

但其实更严谨的逻辑应该是, 先检查是否遍历了k个元素,即path的size是否为k。如果遍历了k个元素,则判断它们的和sum是否等于targetSum,如果相等,则记录该组数据;不相等则不记录。但是不论是否相等,此时已经是遍历了k个元素,已经不能再继续遍历了,所以直接return,结束掉本次的回溯。代码如下:

if(path.size() == k )
{
     if(sum == targetSum)
          result.push_back(path);
     return;
}

如果不满足上述的第一个条件,则说明还没有遍历到k个元素,应该执行单层回溯的具体逻辑。

第三步:确认单层回溯的遍历过程

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

这一过程要做哪些事情?

首先,从startIndex开始遍历元素,将当前元素加到sum中,并用path加以记录;然后就可以从startIndex+1的位置进行递归了,递归之后,该记录的结果会进行记录。然后要进行抽象成的二叉树遍历过程回溯过程,即当前节点退出sum和path,具体代码如下:

for(int i = startIndex; i<=9; i++)
        {
            //处理节点
            sum += i;
            path.push_back(i);

            //递归
            backTracking(targetSum, k, sum, i+1);  //注意将i+1调整为startIndex

            //回溯
            sum -= i;
            path.pop_back(i);
        }

完整代码:

class Solution {
public:
    vector<vector<int>> result;  //满足条件的结果
    vector<int> path;  //当前路径
    // int targetSum; //目标和
    // int k;  //k个元素
    // int sum;  //当前path记录的元素的和
    // int startIndex;  //开始的索引

    //回溯第一步:确认回溯函数的参数以及返回值
    void backTracking(int targetSum, int k, int sum, int startIndex)
    {
        //回溯第二步:确认回溯函数的终止条件
        //什么时候终止此次回溯?那就是找到了一组数(k个数),且它们的和为n
        if(path.size() == k )
        {
            if(sum == targetSum)
                result.push_back(path);
            return;
        }

        //回溯第三步:确认单层回溯的遍历过程
        //单层回溯要做那些事情?遍历!从startIndex开始遍历
        for(int i = startIndex; i<=9; i++)
        {
            //处理节点
            sum += i;
            path.push_back(i);

            //递归
            backTracking(targetSum, k, sum, i+1);  //注意将i+1调整为startIndex

            //回溯
            sum -= i;
            path.pop_back(i);
        }


    }


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

进一步剪枝处理优化代码:

剪枝其实就是在二叉树遍历逻辑的基础上,去点一些明显没有必要的遍历路径,如下图所示:

当遍历元素的和大于题目要求的n时,其实已经没有意义了。 

那么剪枝的地方可以放在递归函数开始的地方,剪枝代码如下:

if (sum > targetSum) { // 剪枝操作
    return;
}

 除此之外,遍历过程也可以再剪枝:

接下来看一下优化过程如下:

  1. 已经选择的元素个数:path.size();

  2. 所需需要的元素个数为: k - path.size();

  3. 列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())

  4. 在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历,往后找还能找到k个数。

代码如下:

class Solution {
private:
    vector<vector<int>> result; // 存放结果集
    vector<int> path; // 符合条件的结果
    void backtracking(int targetSum, int k, int sum, int startIndex) {
        if (sum > targetSum) { // 剪枝操作
            return; 
        }
        if (path.size() == k) {
            if (sum == targetSum) result.push_back(path);
            return; // 如果path.size() == k 但sum != targetSum 直接返回
        }
        for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) { // 剪枝
            sum += i; // 处理
            path.push_back(i); // 处理
            backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
            sum -= i; // 回溯
            path.pop_back(); // 回溯
        }
    }

public:
    vector<vector<int>> combinationSum3(int k, int n) {
        result.clear(); // 可以不加
        path.clear();   // 可以不加
        backtracking(n, k, 0, 1);
        return result;
    }
};

17.电话号码的字母组合

17. 电话号码的字母组合 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/letter-combinations-of-a-phone-number/description/题目描述:

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

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

示例 1:

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

示例 2:

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

示例 3:

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

数字和字母如何映射

可以使用map或者定义一个二维数组,例如:string letterMap[10],来做映射,我这里定义一个二维数组,代码如下:

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

回溯法来解决n个for循环的问题

例如:输入:"23",抽象为树形结构,如图所示:

17. 电话号码的字母组合

 这就和之前的77组合以及216组合III有了相似之处。然后只用回溯三部曲。

回溯三部曲:

  • 确定回溯函数参数

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

再来看参数,参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index。这个index用来表示遍历到digits中的第几个数字了。与之前的startIndex不一样。

    vector<string> result;
    string path;
    //回溯第一步
    void backTracking(const string &digits, int index)
    {
    }
  •  确认回溯的终止条件

index表示遍历到了digits的第几个数字,其初始值为0,遍历过一个数字后就会+1,那么当index等于digits.size()时,表明遍历完毕。代码如下:

if (index == digits.size()) {
    result.push_back(s);
    return;
}
  • 确认单层回溯逻辑

首先,要根据index把数字对应的字符串取出来,然后遍历该字符串,将字母加入到路径path中。添加完一个字母(对应模板中的处理节点),则 该去执行递归,index+1,然后回溯:

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

整体代码如下: 

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

public:
    vector<string> result;
    string path;
    //回溯第一步
    void backTracking(const string digits, int index)
    {
        //回溯第二步:确认回溯终止条件
        if(index == digits.size())
        {
            result.push_back(path);
            return ;
        }

        //回溯第三步:确认单层回溯逻辑操作
        int num = digits[index] - '0';  //获取对应的数字
        string letters = letterMap[num];  //获取该数字对应的字母 
        for(int i = 0 ; i < letters.size(); i++)
        {
            path.push_back(letters[i]);  //加入一个字母
            backTracking(digits, index+1);  //找下一个数字对应的字母
            path.pop_back();  //回溯
        }

    }
    vector<string> letterCombinations(string digits) {
        if(digits.empty())
            return result;
        backTracking(digits, 0);
        return result;
    }
};

  • 24
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值