本节内容 - 216.组合总和III - 17.电话号码的字母组合
总和※ 建议 :如果把 组合问题理解了,本题就容易一些了。
题目链接: https://leetcode.cn/problems/combination-sum-iii/ 文章讲解: https://programmercarl.com/0216.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CIII.html 视频讲解: https://www.bilibili.com/video/BV1wg411873x
题目分析
示例 1:
1 2 3 4 5 输入: k = 3, n = 7 输出: [[1,2,4]] 解释: 1 + 2 + 4 = 7 没有其他符合的组合了。
示例 2:
1 2 3 4 5 6 7 输入: 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:
1 2 3 4 输入: k = 4, n = 1 输出: [] 解释: 不存在有效的组合。 在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
方案一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution { public List<List<Integer>> combinationSum3 (int k, int n) { List<List<Integer>> result = new ArrayList <>(); backtracking(1 , k, n, 0 , new ArrayList <>(), result); return result; } private void backtracking (int start, int k, int n, int sum, List<Integer> list, List<List<Integer>> result) { if (sum > n) return ; if (k == 0 ) { if (sum == n) { result.add(new ArrayList <>(list)); } return ; } for (int i = start; i <= 9 - k + 1 ; i++) { list.add(i); backtracking(i + 1 , k - 1 , n, sum + i, list, result); list.remove(list.size() - 1 ); } } }
结果 解答成功: 执行耗时:0 ms,击败了100.00% 的Java用户 内存消耗:39.1 MB,击败了52.72% 的Java用户
分析 时间复杂度: O( 9 ^ k )
代码随想录 https://programmercarl.com/0216.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CIII.html
思路 本题就是在[1,2,3,4,5,6,7,8,9]这个集合中找到和为n的k个数的组合。
相对于77. 组合 ,无非就是多了一个限制,本题是要找到和为n的k个数的组合,而整个集合已经是固定的了[1,…,9]。
想到这一点了,做过77. 组合 之后,本题是简单一些了。
本题k相当于树的深度,9(因为整个集合就是9个数)就是树的宽度。
例如 k = 2,n = 4的话,就是在集合[1,2,3,4,5,6,7,8,9]中求 k(个数) = 2, n(和) = 4的组合。
选取过程如图:
图中,可以看出,只有最后取到集合(1,3)和为4 符合条件。
回溯三部曲
和77. 组合 一样,依然需要一维数组path来存放符合条件的结果,二维数组result来存放结果集。
这里依然定义path 和 result为全局变量。
至于为什么取名为path?从上面树形结构中,可以看出,结果其实就是一条根节点到叶子节点的路径。
1 2 vector<vector<int >> result; vector<int > path;
接下来还需要如下参数:
targetSum(int)目标和,也就是题目中的n。
k(int)就是题目中要求k个数的集合。
sum(int)为已经收集的元素的总和,也就是path里元素的总和。
startIndex(int)为下一层for循环搜索的起始位置。
所以代码如下:
1 2 3 vector<vector<int >> result; vector<int > path;void backtracking (int targetSum, int k, int sum, int startIndex)
其实这里sum这个参数也可以省略,每次targetSum减去选取的元素数值,然后判断如果targetSum为0了,说明收集到符合条件的结果了,我这里为了直观便于理解,还是加一个sum参数。
还要强调一下,回溯法中递归函数参数很难一次性确定下来,一般先写逻辑,需要啥参数了,填什么参数。
什么时候终止呢?
在上面已经说了,k其实就已经限制树的深度,因为就取k个元素,树再往下深了没有意义。
所以如果path.size() 和 k相等了,就终止。
如果此时path里收集到的元素和(sum) 和targetSum(就是题目描述的n)相同了,就用result收集当前的结果。
所以 终止代码如下:
1 2 3 4 if (path.size () == k) { if (sum == targetSum) result.push_back (path); return ; }
本题和77. 组合 区别之一就是集合固定的就是9个数[1,…,9],所以for循环固定i<=9
如图:
处理过程就是 path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。
代码如下:
1 2 3 4 5 6 7 for (int i = startIndex; i <= 9 ; i++) { sum += i; path.push_back (i); backtracking (targetSum, k, sum, i + 1 ); sum -= i; path.pop_back (); }
别忘了处理过程 和 回溯过程是一一对应的,处理有加,回溯就要有减!
剪枝 这道题目,剪枝操作其实是很容易想到了,想必大家看上面的树形图的时候已经想到了。
如图:
已选元素总和如果已经大于n(图中数值为4)了,那么往后遍历就没有意义了,直接剪掉。
那么剪枝的地方可以放在递归函数开始的地方,剪枝代码如下:
1 2 3 if (sum > targetSum) { return ; }
当然这个剪枝也可以放在 调用递归之前,即放在这里,只不过要记得 要回溯操作给做了。
1 2 3 4 5 6 7 8 9 10 11 12 for (int i = startIndex; i <= 9 - (k - path.size ()) + 1 ; i++) { sum += i; path.push_back (i); if (sum > targetSum) { sum -= i; path.pop_back (); return ; } backtracking (targetSum, k, sum, i + 1 ); sum -= i; path.pop_back (); }
for循环的范围也可以剪枝,i <= 9 - (k - path.size()) + 1就可以了。
时间复杂度: O(n * 2^n)
空间复杂度: O(n)
代码实现 模板方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution { List<List<Integer>> result = new ArrayList <>(); LinkedList<Integer> path = new LinkedList <>(); public List<List<Integer>> combinationSum3 (int k, int n) { backTracking(n, k, 1 , 0 ); return result; } private void backTracking (int targetSum, int k, int startIndex, int sum) { if (sum > targetSum) { return ; } if (path.size() == k) { if (sum == targetSum) result.add(new ArrayList <>(path)); return ; } for (int i = startIndex; i <= 9 - (k - path.size()) + 1 ; i++) { path.add(i); sum += i; backTracking(targetSum, k, i + 1 , sum); path.removeLast(); sum -= i; } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution { LinkedList<Integer> path = new LinkedList <>(); List<List<Integer>> ans = new ArrayList <>(); public List<List<Integer>> combinationSum3 (int k, int n) { build(k, n, 1 , 0 ); return ans; } private void build (int k, int n, int startIndex, int sum) { if (sum > n) return ; if (path.size() > k) return ; if (sum == n && path.size() == k) { ans.add(new ArrayList <>(path)); return ; } for (int i = startIndex; i <= 9 ; i++) { path.add(i); sum += i; build(k, n, i + 1 , sum); sum -= i; path.removeLast(); } } }
其他方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution { List<List<Integer>> res = new ArrayList <>(); List<Integer> list = new ArrayList <>(); public List<List<Integer>> combinationSum3 (int k, int n) { res.clear(); list.clear(); backtracking(k, n, 9 ); return res; } private void backtracking (int k, int n, int maxNum) { if (k == 0 && n == 0 ) { res.add(new ArrayList <>(list)); return ; } if (maxNum == 0 || n > k * maxNum - k * (k - 1 ) / 2 || n < (1 + k) * k / 2 ) { return ; } list.add(maxNum); backtracking(k - 1 , n - maxNum, maxNum - 1 ); list.remove(list.size() - 1 ); backtracking(k, n, maxNum - 1 ); } }
17.电话号码的字母组合※ 建议 :本题大家刚开始做会有点难度,先自己思考20min,没思路就直接看题解。
题目链接: 文章讲解: https://programmercarl.com/0017.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E6%AF%8D%E7%BB%84%E5%90%88.html 视频讲解: https://www.bilibili.com/video/BV1yV4y1V7Ug
题目分析
方案一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 class Solution { public List<String> letterCombinations (String digits) { List<String> result = new ArrayList <>(); if (digits.length() == 0 ) return result; backtracking(digits, 0 , new StringBuilder (), result); return result; } private void backtracking (String digits, int start, StringBuilder path,List<String> result) { if (start == digits.length()) { result.add(path.toString()); return ; } for (Character character : chooseLetter(digits.charAt(start))) { path.append(character); backtracking(digits, start + 1 , path, result); path.deleteCharAt(path.length() - 1 ); } } private List<Character> chooseLetter (char ch) { List<Character> result = new ArrayList <>(); switch (ch) { case '2' : result.add('a' ); result.add('b' ); result.add('c' ); break ; case '3' : result.add('d' ); result.add('e' ); result.add('f' ); break ; case '4' : result.add('g' ); result.add('h' ); result.add('i' ); break ; case '5' : result.add('j' ); result.add('k' ); result.add('l' ); break ; case '6' : result.add('m' ); result.add('n' ); result.add('o' ); break ; case '7' : result.add('p' ); result.add('q' ); result.add('r' ); result.add('s' ); break ; case '8' : result.add('t' ); result.add('u' ); result.add('v' ); break ; case '9' : result.add('w' ); result.add('x' ); result.add('y' ); result.add('z' ); break ; } return result; } }
结果 解答成功: 执行耗时:1 ms,击败了47.26% 的Java用户 内存消耗:40.1 MB,击败了55.72% 的Java用户
分析 时间复杂度: O( 4 * n )
空间复杂度: O( 4 * n )
代码随想录 https://programmercarl.com/0017.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E6%AF%8D%E7%BB%84%E5%90%88.html
思路 从示例上来说,输入”23”,最直接的想法就是两层for循环遍历了吧,正好把组合的情况都输出了。
如果输入”233”呢,那么就三层for循环,如果”2333”呢,就四层for循环…….
理解本题后,要解决如下三个问题:
数字和字母如何映射
两个字母就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来
输入1 * # 按键等等异常情况
数字和字母如何映射 可以使用map或者定义一个二维数组,例如:string letterMap[10],来做映射,我这里定义一个二维数组,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 const string letterMap[10 ] = { "" , "" , "abc" , "def" , "ghi" , "jkl" , "mno" , "pqrs" , "tuv" , "wxyz" , };
回溯法来解决n个for循环的问题 例如:输入:”23”,抽象为树形结构,如图所示:
图中可以看出遍历的深度,就是输入”23”的长度,而叶子节点就是我们要收集的结果,输出[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”]。
回溯三部曲:
首先需要一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来,这两个变量依然定义为全局。
再来看参数,参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index。
这个index是记录遍历第几个数字了,就是用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。
代码如下:
1 2 3 vector<string> result; string s;void backtracking (const string& digits, int index)
例如输入用例”23”,两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。
那么终止条件就是如果index 等于 输入的数字个数(digits.size)了(本来index就是用来遍历digits的)。
然后收集结果,结束本层递归。
代码如下:
1 2 3 4 if (index == digits.size ()) { result.push_back (s); return ; }
首先要取index指向的数字,并找到对应的字符集(手机键盘的字符集)。
然后for循环来处理这个字符集,代码如下:
1 2 3 4 5 6 7 int digit = digits[index] - '0' ; string letters = letterMap[digit]; for (int i = 0 ; i < letters.size (); i++) { s.push_back (letters[i]); backtracking (digits, index + 1 ); s.pop_back (); }
本题每一个数字代表的是不同集合,也就是求不同集合之间的组合
注意:输入1 * #按键等等异常情况
代码中最好考虑这些异常情况,但题目的测试数据中应该没有异常情况的数据,所以我就没有加了。
但是要知道会有这些异常,如果是现场面试中,一定要考虑到!
时间复杂度: O(3^m * 4^n),其中 m 是对应四个字母的数字个数,n 是对应三个字母的数字个数
空间复杂度: O(3^m * 4^n)
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class Solution { List<String> list = new ArrayList <>(); public List<String> letterCombinations (String digits) { if (digits == null || digits.length() == 0 ) { return list; } String[] numString = {"" , "" , "abc" , "def" , "ghi" , "jkl" , "mno" , "pqrs" , "tuv" , "wxyz" }; backTracking(digits, numString, 0 ); return list; } StringBuilder temp = new StringBuilder (); public void backTracking (String digits, String[] numString, int num) { if (num == digits.length()) { list.add(temp.toString()); return ; } String str = numString[digits.charAt(num) - '0' ]; for (int i = 0 ; i < str.length(); i++) { temp.append(str.charAt(i)); backTracking(digits, numString, num + 1 ); temp.deleteCharAt(temp.length() - 1 ); } } }