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

碎碎念:加油

参考:代码随想录

回溯算法理论基础

回溯和递归是相辅相成的,只要有递归,就会有回溯。回溯通常在递归函数的下面。
回溯搜索到法的效率: 它其实是纯暴力的做法,不是一个高效的算法。
回溯法能解决的问题: 组合问题(不强调元素的顺序)、切割问题、子集问题、排列问题(强调元素的顺序)、棋盘问题(N皇后)
如何理解回溯法: 所有回溯法都可以抽象为一个树行结构。一般来说树的宽度是回溯法中处理的集合的大小,树的深度就是递归的深度。
回溯法的模板:

cpp 复制代码
void backtracking(参数){
    if (终止条件) {
        收集结果;
        return;
    }
    for (遍历集合的每一个元素){
        处理节点;
        递归函数;
        回溯操作; // 撤销处理节点
    }
    return;
}

77. 组合

题目链接

77. 组合

思想

看到这题我们自然而然想到暴力做法,如果k是2,那么就用两层for循环,但是k稍微大点,这么做就显然做不出来了,时间复杂度太大了。

回溯法是通过递归来控制有多少层for的。

上面提到过,回溯法做题可以抽象出一个树形图,本题抽象出来的树形图如图。

叶子节点就是我们要求的所有组合。通过维护一个参数startindex来控制搜索的起始位置。

定义一个一维数组path,定义一个二维数组result
回溯三部曲:

  1. 递归函数的参数和返回值:一般返回值都是void,参数n,k,startindex
  2. 终止条件:path的大小等于k,就要收割结果。
  3. 单层搜索的逻辑:从startindex开始遍历后面的元素,用path收集路径上的元素,递归下一层,回溯(把之前收集到的一个元素pop出去)。

剪枝:

优化上面单层搜索的逻辑:

其实图里每个节点都是for循环。那么我们要做的优化就在于for循环里i的范围。

path存放着已经选取到的元素,还剩k-path.size()需要选取,这些元素至多要从n-(k-path.size())+1的位置开始。

x, n\]的数组长度起码应该是k-path.size(),这才有继续搜索的必要, 有 n-x+1 = k-path.size() ,得 x = n+1 - (k-path.size()), 而且这个x是可以作为起点往下搜的,也就是for(i = s; i\<=x; i++) 这里的x是可以取到的。 体现在代码里,修改为i \<= n-(k-path.size())+1即可。 ### 题解 ```cpp // cpp class Solution { private: vector> result; vector path; void backtracking(int n, int k, int startIndex) { if (path.size() == k) { result.push_back(path); return; } for (int i = startIndex; i <= n; i++) { path.push_back(i); backtracking(n, k, i + 1); path.pop_back(); } } public: vector> combine(int n, int k) { backtracking(n, k, 1); return result; } }; ``` ```python # python 剪枝优化 class Solution: def __init__(self): self.result = [] self.path = [] def backtracking(self, n, k, startIndex): if len(self.path) == k: self.result.append(self.path[:]) return for i in range(startIndex, n - (k - len(self.path)) + 2): self.path.append(i) self.backtracking(n, k, i + 1) self.path.pop() def combine(self, n: int, k: int) -> List[List[int]]: self.backtracking(n, k, 1) return self.result ``` ### 反思 注意组合是无序的,组合中的元素只能使用一次。 ## 216.组合总和III ### 题目链接 [216.组合总和III](https://leetcode.cn/problems/combination-sum-iii/description/) ### 思想 和上一题的区别在于多了一个关于和的限制。 注意本题不强调元素的顺序,比如126和216是同一个组合。 **树形结构:** ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/1dac3d39767c47cdb17712141d00c7bb.png) 定义一个一维数组path,定义一个二维数组result **回溯三部曲:** 1. 参数和返回值:返回值为void,参数targetSum,k,sum,startIndex。 2. 终止条件:path的size==k,判断sum和targetSum是否相等,相等就把path加入result,否则直接返回。 3. 单层递归逻辑:从startIndex开始遍历,取数加入path,计算sum,下一层递归(startIndex传入i+1),pop出刚加入的数,修改sum。 **剪枝优化:** 如果sum\>targetSum,直接返回; 在for循环那里也可以做剪枝,体现在代码里,修改为i \<= 9-(k-path.size())+1即可。 ### 题解 ```cpp // cpp class Solution { private: vector path; vector> result; void backtracking(int targetSum, int k, int sum, int startIndex) { if (path.size() == k) { if (sum == targetSum) result.push_back(path); return; } for (int i = startIndex; i <= 9; i ++) { sum += i; path.push_back(i); backtracking(targetSum, k, sum, i + 1); sum -= i; path.pop_back(); } } public: vector> combinationSum3(int k, int n) { result.clear(); path.clear(); backtracking(n, k, 0, 1); return result; } }; ``` ```python # python 剪枝优化 class Solution: def __init__(self): self.path = [] self.result = [] def backtracking(self, targetSum, k, currentSum, startIndex): if currentSum > targetSum: # 剪枝 return if len(self.path) == k: if currentSum == targetSum: self.result.append(self.path[:]) return for i in range(startIndex, 9 - (k - len(self.path)) + 2): currentSum += i self.path.append(i) self.backtracking(targetSum, k,currentSum, i + 1) currentSum -= i self.path.pop() def combinationSum3(self, k: int, n: int) -> List[List[int]]: self.backtracking(n, k, 0, 1) return self.result ``` ### 反思 注意组合是无序的,组合中的元素只能使用一次。 ## 17.电话号码的字母组合 ### 题目链接 [17.电话号码的字母组合](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/description/) ### 思想 **树形图如图:** ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/b7660b420aff4444bec7dec4627665c3.png) **回溯三部曲:** 1. 参数和返回值:返回值是void,参数是digits,index(当前遍历到哪个数字了) 2. 终止条件:index==digits.size() 收获结果 3. 单层递归逻辑:用index取数字,用数字找字符串,接下来遍历得到的字符串,把字母放入s,调用递归函数(index+1),再把s取出来(回溯)。 回溯过程也可以隐藏在参数里。 ### 题解 ```cpp 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 result; string s; void backtracking(const string& digits, int index) { if (index == digits.size()) { result.push_back(s); return; } string letters = letterMap[digits[index] - '0']; for (int i = 0; i < letters.size(); i ++) { s.push_back(letters[i]); backtracking(digits, index + 1); s.pop_back(); } } vector letterCombinations(string digits) { if (digits.size() == 0) return result; backtracking(digits, 0); return result; } }; ``` ```python class Solution: def __init__(self): self.letterMap = [ "", # 0 "", # 1 "abc", # 2 "def", # 3 "ghi", # 4 "jkl", # 5 "mno", # 6 "pqrs", # 7 "tuv", # 8 "wxyz" # 9 ] self.result = [] self.s = "" def backtracking(self, digits, index): if index == len(digits): self.result.append(self.s) return letters = self.letterMap[int(digits[index])] for i in range(len(letters)): self.s += letters[i] self.backtracking(digits, index + 1) self.s = self.s[:-1] def letterCombinations(self, digits: str) -> List[str]: if len(digits) == 0: return self.result self.backtracking(digits, 0) return self.result ``` ### 反思 前两题都是在一个集合里来求组合,所以需要用到startIndex,本题是在不同的集合里,不需要用参数来控制之前遍历的元素。

相关推荐
qystca18 分钟前
蓝桥云客---九宫幻方
算法·深度优先·图论
明月清了个风41 分钟前
数据结构与算法学习笔记----贪心区间问题
笔记·学习·算法·贪心算法
努力毕业的小土博^_^44 分钟前
【EI/Scopus双检索】2025年4月光电信息、传感云、边缘计算、光学成像、物联网、智慧城市、新材料国际学术盛宴来袭!
人工智能·神经网络·物联网·算法·智慧城市·边缘计算
神里流~霜灭1 小时前
数据结构:二叉树(三)·(重点)
c语言·数据结构·c++·算法·二叉树·红黑树·完全二叉树
网安秘谈1 小时前
非对称加密技术深度解析:从数学基础到工程实践
算法
luckyme_1 小时前
leetcode-代码随想录-哈希表-有效的字母异位词
算法·leetcode·散列表
zh_xuan1 小时前
LeeCode 57. 插入区间
c语言·开发语言·数据结构·算法
莫有杯子的龙潭峡谷1 小时前
4.4 代码随想录第三十五天打卡
c++·算法
luckyme_2 小时前
leetcode 代码随想录 数组-区间和
c++·算法·leetcode
好好学习^按时吃饭2 小时前
蓝桥杯2024年第十五届省赛真题-R 格式
算法·蓝桥杯