题目1:216组合的总和Ⅲ
题目链接:组合总和Ⅲ
对题目的理解
使用数字1~9,找出k个数,这k个数的和为n,每个数字最多使用一次
k控制树的深度
回溯法
回溯三部曲
i)确定递归函数参数
全局变量一维数组path来存放符合条件的结果,二维数组result来存放结果集。
- targetSum(int)目标和,也就是题目中的n。
- k(int)就是题目中要求k个数的集合。
- sum(int)为已经收集的元素的总和,也就是path里元素的总和。
- startIndex(int)为下一层for循环搜索的起始位置。
ii)确定终止条件
①k其实就已经限制树的深度,因为就取k个元素,树再往下深了没有意义。所以如果path.size() 和 k相等了,就终止。
②如果此时path里收集到的元素和(sum) 和targetSum(就是题目描述的n)相同了,就用result收集当前的结果。
iii)单层搜索过程
处理过程就是 path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。
!!!!别忘了处理过程 和 回溯过程是一一对应的,处理有加,回溯就要有减!
代码
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backtracking(int targetsum,int sum,int k,int startindex){
//终止条件
if(path.size()==k){
if(targetsum==sum){
result.push_back(path);
return;
}
}
//单层递归逻辑
for(int i=startindex;i<=9;i++){
sum+=i;
path.push_back(i);
backtracking(targetsum,sum,k,i+1);//递归
sum-=i;//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum3(int k, int n) {
path.clear();
result.clear();
int sum=0;
backtracking(n,sum,k,1);
return result;
}
};
剪枝
①已选元素总和如果已经大于n了,那么往后遍历就没有意义了,直接剪掉,剪枝可以放在递归函数开始的地方
②for循环的范围也可以剪枝,i <= 9 - (k - path.size()) + 1就可以了。
代码
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backtracking(int targetsum,int sum,int k,int startindex){
if(sum>targetsum) return;
//终止条件
if(path.size()==k){
if(targetsum==sum){
result.push_back(path);
return;
}
}
//单层递归逻辑
for(int i=startindex;i<=9-(k-path.size())+1;i++){
sum+=i;
path.push_back(i);
backtracking(targetsum,sum,k,i+1);//递归
sum-=i;//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum3(int k, int n) {
path.clear();
result.clear();
int sum=0;
backtracking(n,sum,k,1);
return result;
}
};
- 时间复杂度: O(n * 2^n)
- 空间复杂度: O(n)
题目2:17.电话号码的字母组合
题目链接:电话号码的字母组合
对题目的理解
仅含数字2-9的字符串,返回所有能表示的字母组合,注意是根据数字返回字母!!!
要解决如下三个问题:
1)数字和字母如何映射
可以使用map或者定义一个二维数组,例如:string letterMap[10],来做映射,
2)两个字母就两个for循环,三个字符我就三个for循环,以此类推,.......想到使用回溯法
回溯法
回溯三部曲
输入数字的个数就是树的深度,树的宽度就是数字所代表的字母的长度
i)确定回溯函数参数
全局变量:一个字符串s来收集叶子节点的单个结果,用一个字符串数组result所有结果保存
再来看参数,有题目中给的string digits,然后还要有一个参数就是int型的index。index是记录遍历第几个数字了,就是用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。
ii)确定终止条件
叶子节点就是要收集的结果集,那么终止条件就是如果index 等于输入的数字个数(digits.size)
iii)确定单层遍历逻辑
首先要取index指向的数字,接着找到对应的字符集(手机键盘的字符集),然后for循环来处理这个字符集,每一个数字代表的是不同集合,也就是求不同集合之间的组合
3)输入1 * #按键等等异常情况,知道会有这些异常,如果是现场面试中,一定要考虑到!
代码1:
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:
string s;//存放单个结果
vector<string> result;//存放所有结果
void backtracking(int index,const string& digits){
//index 当前遍历到哪个数字了
//终止条件
if(index==digits.size()){
result.push_back(s);
return;
}
//单层遍历
//找到数字
int dig = digits[index]-'0';
//找到数字对应的字符串
string letter = letterMap[dig];
for(int i=0;i<letter.size();i++){
s.push_back(letter[i]);//处理
backtracking(index+1, digits);//递归
s.pop_back();//回溯
}
}
vector<string> letterCombinations(string digits) {
s.clear();
result.clear();
if(digits.size()==0) return result;
backtracking(0, digits);
return result;
}
};
代码1和代码2的不同之处,就在于终止条件的if判断处,代码1判断index遍历到第几个数字和digits的大小进行比较,当等于时,说明遍历完成,可以收获结果了;代码2是比较单个结果内的元素个数和digits中的元素个数,如果相等,则说明遍历完成,可以收获结果了
代码2:
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:
string s;//存放单个结果
vector<string> result;//存放所有结果
void backtracking(int index,const string& digits){
//index 当前遍历到哪个数字了
//终止条件
if(s.size()==digits.size()){
result.push_back(s);
return;
}
//单层遍历
//找到数字
int dig = digits[index]-'0';
//找到数字对应的字符串
string letter = letterMap[dig];
for(int i=0;i<letter.size();i++){
s.push_back(letter[i]);//处理
backtracking(index+1, digits);//递归
s.pop_back();//回溯
}
}
vector<string> letterCombinations(string digits) {
s.clear();
result.clear();
if(digits.size()==0) return result;
backtracking(0, digits);
return result;
}
};
- 时间复杂度: O(3^m * 4^n),其中 m 是对应四个字母的数字个数,n 是对应三个字母的数字个数
- 空间复杂度: O(3^m * 4^n)