组合(优化)
先给出组合问题的回溯部分代码:
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> 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(); // 回溯,撤销处理的节点
}
}
剪枝操作优化
首先明白为什么可以剪枝,以此处的n个元素操作k个元素为例,当处理的后序元素数量小于k时可以直接剪枝掉,以节省内存空间;
例如,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了;在第二层for循环,从元素3开始的遍历都没有意义了;依次类推:(图里的每一个节点,都代表着一个for循环)
由图可知,由于回溯算法的实现可以视为for循环里嵌套着递归;(此处补充一下回溯到底是怎么实现的,如图)
实际上剪枝操作就是在缩小for循环的范围,即改变for循环的循环条件,缩小范围:
因为如果for循环选择的起始位置之后的元素个数已经不足我们需要的元素个数了,那么就没有必要搜索了;
具体剪枝逻辑如下:
已经选择了path.size()个元素;
则后续剩余所需的元素个数为k - path.size();
那么要求剩余元素个数(n-i) >= (k - path.size());
则要求i <= n - (k - path.size()) + 1;
为啥要**+1**?,因为这个地方是i可以取到的上界,而取的集合要求是闭区间,比如n=4,k=3的时候,当path.size() = 0时,那么[1,2,3]和[2,3,4]数组都是合法的目标数组;
剪枝后即为
//略
for(int i = startIndex; i <= n - (k - path.size()) + 1; i++)
//略
组合总和Ⅲ
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数。
- 解集不能包含重复的组合。
示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
思路和77.组合的思路是一致的,但是多了一个相加等于n的限制条件;
很容易知道,k就是树的深度,9就是树的宽度,树形结构如图所示:
可以看出,这里很明显是需要进行剪枝操作的,多余的节点太多了;
vector<vector<int>> res;
vector<int> path;
void backtracking(int targetSum, int k, int sum, int startIndex){
if(sum > targetSum) return;//剪枝
if(path.size() == k){
if(sum == targetSum) res.push_back(path);
return;
}
for(int i = startIndex; i <= 9 - (k - path.size()) + 1; i++ ){//剪枝,注意此处不是减k而是k-path.size()
sum += i;
path.push_back(i);
backtracking(targetSum, k, sum, i+1);
sum -= i;
path.pop_back();
}
}
整体代码如下:
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
void backtracking(int targetSum, int k, int sum, int startIndex){
if(sum > targetSum) return;//剪枝
if(path.size() == k){
if(sum == targetSum) res.push_back(path);
return;
}
for(int i = startIndex; i <= 9 - (k - path.size()) + 1; i++ ){//剪枝
sum += i;
path.push_back(i);
backtracking(targetSum, k, sum, i+1);
sum -= i;
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
path.clear();
res.clear();
backtracking(n, k, 0, 1);
return res;
}
};
电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
- 输入:“23”
- 输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].
说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
题目输入的是字符串,但是字符串中的字符同时是对应着另一组字符串的数字,所以这里要进行一个映射的操作;
可以用const string的形式进行映射,也可以使用map进行映射存储:
//写法1
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
//写法2
std::map<int, std::string> letterMap;
letterMap[0] = "";
letterMap[1] = "";
letterMap[2] = "abc";
letterMap[3] = "def";
letterMap[4] = "ghi";
letterMap[5] = "jkl";
letterMap[6] = "mno";
letterMap[7] = "pqrs";
letterMap[8] = "tuv";
letterMap[9] = "wxyz";
同理,将回溯过程以树形结构表达出来:
即树的深度由输入的字符串决定;
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
vector<string> res;
string s;
void backtracking(const string& digits, int index){
if(index == digits.size()){//终止条件就是如果index 等于输入的数字个数(digits.size)
res.push_back(s);
return;
}
//单层遍历逻辑
int digit = digits[index] - '0';//char类型运算得到int型
string letters = letterMap[digit];//取对应集合的字符串
for(int i = 0; i < letters.size(); i++){
s.push_back(letters[i]);
backtracking(digits, 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
};
vector<string> res;
string s;
void backtracking(const string& digits, int index){
if(index == digits.size()){//终止条件就是如果index等于输入的数字个数(digits.size)
res.push_back(s);
return;
}
//单层遍历逻辑
int digit = digits[index] - '0';//char类型运算得到int型
string letters = letterMap[digit];//取对应集合的字符串
for(int i = 0; i < letters.size(); i++){
s.push_back(letters[i]);
backtracking(digits, index + 1);
s.pop_back();
}
}
public:
vector<string> letterCombinations(string digits) {
s.clear();
res.clear();
if (digits.size() == 0) {
return res;
}
backtracking(digits, 0);
return res;
}
};