1. 77. 组合
每次选择一个数,然后下一个数从他之后选择
剪枝:
当剩余元素加一起也不满足k时也不再遍历,
当元素个数==k时不再向下遍历
1.已经选择元素个数:path.size()
2.还需要选择元素个数k - path.size()
3.n=4,k=4,还需要四个元素,最多从倒数第4个元素选,倒数第四个元素为n - 3
比n - 还需要的元素个数(4),大1,所以要加1
即索引最多遍历结果为n - (k - path.size()) + 1
class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtracking(int n, int k, int startIndex){
if (path.size() == k){
res.push_back(path);
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){
path.push_back(i);
backtracking(n, k, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return res;
}
};
2. 216. 组合总和 III
与上题基本相同,但加了更多的限制条件
剪枝:
1.当元素个数==k时不再向下遍历
2.当元素之和大于要求时不再继续(接下来的数只会更大)
3.当剩余元素不足k个时停止遍历
参数n代表剩余需要数值大小,为0代表相等,为负数相当于当前元素和大于要求,直接剪枝
所以每次传参n - i,传型参之后n不变(相当于n + i),隐含了回溯
class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtrack(int n, int k, int startIndex){
if(n < 0) return ;
if (path.size() == k){
if (n == 0){
res.push_back(path);
}
return ;
}
for(int i = startIndex; i <= 9 - (k - path.size()) + 1; i++){
path.push_back(i);
backtrack(n - i, k, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
backtrack(n, k, 1);
return res;
}
};
3. 17. 电话号码的字母组合
多个集合求组合,用哈希把每个号码对应的字符串连接起来
index代表号码索引,表示选择哪个号码集合(当index==号码长度时,结束回溯,也可以用path.size(),但index更省时间
用引用传参是应为就不用拷贝一份了
注意:path + d[i]这种临时变量传引用必须加const
class Solution {
private:
string letterMap[10] = {
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz"
};
vector<string> res;
void backtrack(string& digits, int index,const string& path){
if(index == digits.size()){
res.push_back(path);
return;
}
int num = digits[index] - '0'; //当前电话号码
string cur = letterMap[num]; //对应的字符串
for(int i = 0; i < cur.size(); i++){
backtrack(digits, index + 1, path + cur[i]);
}
}
public:
vector<string> letterCombinations(string digits) {
if(digits.size() == 0) return res;
backtrack(digits, 0, "");
return res;
}
};
4. 39. 组合总和
树枝去重
防止重复元素:也采用startIndex,但不同的是因为可以访问重复元素,所以传startIndex为i
这样就不会出现(123,213)这种情况了,也同样能遍历到所有集合(12同时在的情况在访问1时已得到过,2就不需要访问1了)
回溯终止条件:当target==0,加入集合
target<0写在最上面,停止向下查找
剪枝:在for循环判断target,如果大于停止遍历(横向停止,纵向也停止),但需要排序数组,因为7可能在1前面,前面溢出不代表后面也溢出
class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtrack(vector<int> candidates, int target, int startIndex){
if (target == 0){
res.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size() && target - candidates[i] >= 0; i++){
path.push_back(candidates[i]);
backtrack(candidates, target - candidates[i], i);
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
if(candidates.size() == 0) return res;
sort(candidates.begin(), candidates.end());
backtrack(candidates, target, 0);
return res;
}
}
5. 40. 组合总和 II
树层去重
本题集合中是有重复元素的,集合中单个元素可以重复,但集合整体不能都相同(可以有112的存在,但不能还有个121)
所以每个树枝上的元素可以重复,但同一层的相同元素不可以重复遍历两次(会产生相同的集合)
每层的起始位置为startIndex,在他之前都是上层的元素了,可以重复,所以不可以是if (i > 0 && candidates[i] == candidates[i - 1]) ,这样树枝也会被去重
所以是 i > startIndex,处于同一层的才会被去除
妙妙妙
class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtrack(vector<int> candidates, int target, int startIndex){
if(target == 0){
res.push_back(path);
return;
}
for(int i = startIndex; i < candidates.size() && target - candidates[i] >= 0; i++){
if(i > startIndex && candidates[i] == candidates[i - 1]) continue;
path.push_back(candidates[i]);
backtrack(candidates, target - candidates[i], i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
if(candidates.size() == 0) return res;
sort(candidates.begin(), candidates.end());
backtrack(candidates, target, 0);
return res;
}
};
6. 131. 分割回文串
与求组合类似,startIndex之前相当于已经被切割过,每次从startIndex继续切割,同时判断当前切割下来的字符串是不是回文字符串,不是就直接停止该分支的回溯,这样最后到末尾的分支就都是符合要求的了
回溯出口:切割到了字符串末尾
剪枝:中途不是回文字符串的都终止该树枝,同时保证了res中都是已符合要求的字符串
class Solution {
private:
vector<string> path;
vector<vector<string>> res;
void backtrack(const string& s, int startIndex){
if (startIndex == s.size()) {
res.push_back(path);
return;
}
for (int i = startIndex; i < s.size(); i++){
if (isPalindrome(s, startIndex, i)){
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
backtrack(s, i + 1);
path.pop_back();
}
}
}
bool isPalindrome(const string& str, int left, int right){
while (left < right){
if (str[left++] != str[right--]){
return false;
}
}
return true;
}
public:
vector<vector<string>> partition(string s) {
backtrack(s, 0);
return res;
}
};
7. 93. 复原 IP 地址
与切割回文串相似,判断回文变为了判断该段ip地址是否合法
具体判断条件:
1.结束索引要大于开始索引(1111)结束索引为3,但开始索引为4,必须要有该判断
2.数字开头不能为0,除非这个数字真的是0
3.中间不能含有数字之外的字符
4.每段ip大小要小于等于255
回溯出口:每分一段记数加一,当已经分完三段后判断剩余的第四段是否合法,如果合法添加到res中
剪枝:该段IP地址不合法时,直接结束该树层和树枝的遍历(break),因为该段不合法该层后的ip地址都包含这个不合法的部分,直接结束即可
class Solution {
private:
vector<string> res;
void backtrack(string& str, int startIndex, int pointNum){
if (pointNum == 3){
if (isValid(str, startIndex, str.size() - 1)){
res.push_back(str);
}
return;
}
for(int i = startIndex; i < str.size(); i++){
if (isValid(str, startIndex, i)){
str.insert(str.begin() + i + 1, '.');
backtrack(str, i + 2, pointNum + 1);
str.erase(str.begin() + i + 1);
}
else {
break;
}
}
}
bool isValid(string str, int start, int end){
if(start > end){
return false;
}
if (str[start] == '0' && start != end){
return false;
}
int num = 0;
for (int i = start; i <= end; i++){
if (str[i] < '0' || str[i] > '9'){
return false;
}
num = num * 10 + str[i] - '0';
if (num > 255){
return false;
}
}
return true;
}
public:
vector<string> restoreIpAddresses(string s) {
backtrack(s, 0, 0);
return res;
}
};
8. 78. 子集
模板题,直接将所有节点填入res,无需任何剪枝
注意res的push_back()要写在最前面,代表将上一层加入结果集,这样就包含了空子集
回溯出口:当startIndex等于数组大小
class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtrack(vector<int>& nums, int startIndex){
res.push_back(path);
if (startIndex == nums.size()){
return;
}
for (int i = startIndex; i < nums.size(); i++){
path.push_back(nums[i]);
backtrack(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
backtrack(nums, 0);
return res;
}
};
9. 491. 递增子序列
只有path数组为空或当前元素大于上一个元素,且元素没有在同一层出现过,才进行回溯,直到startIndex==size为止
但本题不同的是不能用排序和i>startIndex的方式判断本层是否有重复元素,因为会打乱原数组顺序
所以就要用哈希来存放并判断当前层是否出现过重复元素
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
void backtrack(vector<int>& nums, int startIndex){
if(startIndex == nums.size()){
return;
}
vector<int> used(201, 0);
for(int i = startIndex; i < nums.size(); i++){
if((path.size() == 0 || nums[i] >= path.back()) && used[nums[i] + 100] != 1){
path.push_back(nums[i]);
used[nums[i] + 100] = 1;
if(path.size() > 1){
res.push_back(path);
}
backtrack(nums, i + 1);
path.pop_back();
}
}
}
public:
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtrack(nums, 0);
return res;
}
};
10. 46. 全排列
树枝去重
防止111,222的出现
取叶子节点,但注意去重,因为是排列所有元素都可以取(213,,132可以),但一个数只能在集合中出现一次,这样写used约束的树枝中不会有重复
used标记的是位置,而不是数值,所以相同位置的数不会出现在同一树枝
递归出口:path.size() == nums.size()
class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtrack(const vector<int>& nums, vector<bool>& used){
if (path.size() == nums.size()){
res.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++){
if(used[i] == false) {
used[i] = true;
path.push_back(nums[i]);
backtrack(nums, used);
path.pop_back();
used[i] = false;
}
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
if(nums.empty()) return res;
vector<bool> used(nums.size(), false);
backtrack(nums, used);
return res;
}
};
11. 47. 全排列 II
树枝去重和树层去重
防止111,222的出现,
树层去重(相同的数在一个树层使用了两次)同时防止112出现了两次
递归出口:path.size() == nums.size()
class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtrack(const vector<int>& nums, vector<bool>& used){
if (path.size() == nums.size()){
res.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++){
if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
if (used[i] == false) {
used[i] = true;
path.push_back(nums[i]);
backtrack(nums, used);
path.pop_back();
used[i] = false;
}
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
if(nums.empty()) return res;
sort(nums.begin(), nums.end());
vector<bool> used(nums.size(), false);
backtrack(nums, used);
return res;
}
};
12. 51. N 皇后
传说中的N皇后,
在每行选一个位置,然后继续遍历下一行(每次都要判断能否落在该位置)
合法性函数:
1.判断同一列是否有皇后
2.判断主对角线方向斜线,上半段(从上往下放棋子,下面的行没有棋子,不用判断),行列都减
3.判断副对角线方向斜线,上半段,行减列加
不用判断同一行的棋子情况,因为咱们回溯过程中同一行只会有一个棋子
递归出口:当最后一行也放完棋子后结束回溯
class Solution {
private:
vector<vector<string>> res;
void backtrack(int n, int row, vector<string>& chessboard){
if (row == n) {
res.push_back(chessboard);
return;
}
for (int col = 0; col < n; col++){
if (isValid(row, col, chessboard)){
chessboard[row][col] = 'Q';
backtrack(n, row + 1, chessboard);
chessboard[row][col] = '.';
}
}
}
bool isValid(int row, int col, const vector<string>& chessboard){
//判断同一列
for (int i = 0; i < row; i++){
if (chessboard[i][col] == 'Q'){
return false;
}
}
//判断主对角线方向斜线,上半段
for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q'){
return false;
}
}
//判断副对角线方向斜线,上半端
for(int i = row - 1, j = col + 1; i >= 0 && j < chessboard.size(); i--, j++){
if (chessboard[i][j] == 'Q'){
return false;
}
}
return true;
}
public:
vector<vector<string>> solveNQueens(int n) {
vector<string> chessboard(n, string(n, '.'));
backtrack(n, 0, chessboard);
return res;
}
};
13. 37. 解数独
与N皇后不同的是,N皇后是每行选择一个位置,数烛是每个位置都要填入数字,N皇后实质上只填写N个(一行一个),但数烛要填写N方个
按顺序填写,所以要用双重循环,该位置有数字直接跳过,当前位置为空且1到9都不能填入当前位置,直接返回false,一层层向上撤销结果,该树枝回溯结束
回溯出口:当所有位置都填入数字才会返回true,一层一层向上返回,但不会撤销填入的数字,最终获得想要的结果
判断合法性:横、竖、九宫格都不能一样,
整除3再乘3是每个数所在九宫格的起始位置,妙啊妙啊
class Solution {
private:
bool backtrack(vector<vector<char>>& board){
for (int i = 0; i < board.size(); i++){
for (int j = 0; j < board[0].size(); j++){
if(board[i][j] != '.') continue;
for (char k = '1'; k <= '9'; k++) {
if (isValid(i, j, k, board)){
board[i][j] = k;
if (backtrack(board)) return true;
board[i][j] = '.';
}
}
return false;
}
}
return true;
}
bool isValid(int row, int col, char val, const vector<vector<char>>& board){
for (int i = 0; i < 9; i++){
if (board[row][i] == val){
return false;
}
}
for (int i = 0; i < 9; i++){
if (board[i][col] == val){
return false;
}
}
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++){
for (int j = startCol; j < startCol + 3; j++){
if (board[i][j] == val){
return false;
}
}
}
return true;
}
public:
void solveSudoku(vector<vector<char>>& board) {
backtrack(board);
}
};
收获与体会
1.回溯模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
2.回溯三部曲
1.递归函数返回值即参数(返回值一般为void)
2.递归终止条件
3.单层搜索过程
3.回溯本质是穷举,可以用剪枝来减少穷举的次数
4. 回溯一般用于解决排列、组合、字符串切割、子集、棋盘问题
5. 子集是收集所有节点,组合是所有叶子节点
6. 组合时要避免使用当前层已经重复的元素,否则结果集元素会重复
当数组元素顺序可以改变时,用
sort(numns.begin(), nums.end());
if(i > startIndex && nums[i] == nums[i - 1]) continue;
但数组元素顺序不可以改变时,如递增子序列,就要用哈希记录当前层出现过的元素(而不是哈希结果集,这样存放数据更少),如果元素已经在哈希表中就不进行遍历
7.剪当前树枝在for循环中用continue,直接结束当前树层用break
8.排列问题由于没有startIndex,去重用used数组
记录该位置有没有被使用过,集合中没有重复元素,只需注意同一位置元素不可以被重复使用
集合中有重复元素,树枝可以有重复元素(112),但树层不能有重复元素(否则会有两个112)
used[i - 1] == true; 上一层被使用过(置为true还没有回溯撤销为false)
used[i - 1] == false; 同一层被使用过
树枝去重
if (used[i] == false) 才将该位置元素加到path
树层去重
不是首元素,与前元素相同,且前元素被使用过
if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
9.单一集合求组合都要用startIndex
10.排列组合求叶子节点,子集求每个节点