写一些自己LeetCode的刷题过程及总结05
##leetcode上有2000+的题,不可能都刷完,我的刷题顺序是先分类型,然后再分难度,不断提升,当然过程中也参考了其他大神们的一些建议,光刷题收获不大,最重要的还是不断归纳总结,没事刷两道,坚持写总结其实也挺有意思的。##
##还在不断更新总结!##
##本文仅用来记录自己平时的学习收获##
##有朝一日我也能写出漂亮的代码!##
一、回溯
1.1 关于回溯
1.1.1 回溯的相关介绍
这个是我最开始刷题时学到的第一个算法,刚开始感觉这个算法很牛的样子,但是随着理解的深入,其实也就那么回事。
首先,说一下什么是回溯。回溯其实就是一种搜索方式,并且回溯与递归是分不开的,所以如果有人说道回溯函数,其实也就是递归函数。简单的理解回溯就是:先沿一条路走,当走不通的时候往回退一步,看看有没有其它的路可以走,如果还是没有那再回退。
其次,说一下回溯算法的效率。虽然回溯算法很难,并且也不好理解(跟递归扯上关系的貌似都不好理解。。。),但是回溯算法的效率其实并不高。从上面对回溯的解释不难看出,回溯其实就是穷举,就是暴力解法,穷举所有的可能,然后找到符合要求的答案。如果想要提高回溯的效率,可以在递归过程中进行一些剪枝操作,但其时间复杂度仍是暴力解法的时间复杂度。
最后,说一下为什么要用回溯,也就是回溯能解决什么问题。刚说了回溯其实就是暴力解法,效率并不高,那为什么还要用回溯?因为有些问题那怕是用暴力解法也没有办法解决,即没有办法找到所有的可能。这时就需要用到回溯了,这也就是为什么回溯给人一种很难的感觉,竟然还有用暴力解法都没法解决的问题,那会有多难。
1.1.2 回溯能解决的问题
这里总结一下回溯法能解决的问题:
1、组合问题:N个数里按一定规则找出k个数的集合。
2、切割问题:一个字符串按一定规则有几种切割方式。
3、子集问题:一个N个数的集合里有多少符合条件的子集。
4、排列问题:N个数按一定规则全排列,有几种排列方式。
5、棋盘问题:著名的N皇后、解数独等。
1.2 leetcode部分回溯题目及代码
77.组合
39.组合总和
40.组合总和II
216.组合总和III
17.电话号码的字母组合
131.分割回文串
93.复原IP地址
78.子集
90.子集II
46.全排列
47.全排列II
51.N皇后
52.N皇后II
37.解数独
491.递增子序列
1.2.1 组合问题
1、77.组合
class Solution {
public:
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> ret;
if (n < k || n < 1) return ret;
vector<int> cur;
backTracking(ret, cur, n, k, 1);
return ret;
}
//回溯三步:
//1、确定递归函数参数及返回值
void backTracking(vector<vector<int>>& ret, vector<int>& cur, int n, int k, int val) {
//2、确定终止条件
if (cur.size() == k) {
ret.push_back(cur);
return;
}
//3、单次递归过程
for (int i = val; i <= n; ++i) {
cur.push_back(i);
backTracking(ret, cur, n, k, i + 1);
cur.pop_back();//回溯
}
}
};
2、39.组合总和
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> ret;
if (candidates.size() == 0) return ret;
vector<int> cur;
backTracking(ret, cur, candidates, target, 0);
return ret;
}
//回溯三步:
//1、确定递归函数参数及返回值
void backTracking(vector<vector<int>>& ret, vector<int>& cur, vector<int>& candidates, int target, int index) {
//2、确定终止条件
if (target == 0) {
ret.push_back(cur);
return;
}
if (target < 0) return;
//3、单词递归逻辑
for (int i = index; i < candidates.size(); ++i) {
cur.push_back(candidates[i]);
backTracking(ret, cur, candidates, target - candidates[i], i);
cur.pop_back(); //回溯
}
}
};
3、40.组合总和II
class Solution {
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<vector<int>> ret;
vector<int> cur;
sort(candidates.begin(), candidates.end());
backTracking(ret, cur, candidates, target, 0);
return ret;
}
//回溯三步:
//1、确定递归函数参数及返回值
void backTracking(vector<vector<int>>& ret, vector<int>& cur, vector<int>& candidates, int target, int index) {
//2、确定终止条件
if (target == 0) {
ret.push_back(cur);
return;
}
if (target < 0) return;
//3、单次递归逻辑
for (int i = index; i < candidates.size(); ++i) {
if (i > index && candidates[i] == candidates[i - 1]) continue;
cur.push_back(candidates[i]);
backTracking(ret, cur, candidates, target - candidates[i], i + 1);
cur.pop_back();
}
}
};
4、216.组合总和III
class Solution {
public:
vector<vector<int>> combinationSum3(int k, int n) {
vector<vector<int>> ret;
vector<int> cur;
backTracking(ret, cur, k, n, 0, 1);
return ret;
}
//回溯三步:
//1、确定递归函数参数及返回值
void backTracking(vector<vector<int>>& ret, vector<int>& cur, int k, int n, int sum, int level) {
//2、确定终止条件
if (cur.size() == k && sum == n) {
ret.push_back(cur);
return;
}
//3、单次递归逻辑
for (int i = level; i <= 9 && sum + i <= n; ++i) {
cur.push_back(i);
backTracking(ret, cur, k, n, sum + i, i + 1);
cur.pop_back();
}
}
};
5、17.电话号码的字母组合
class Solution {
public:
vector<string> letterCombinations(string digits) {
vector<string> ret;
if (digits.size() == 0) return ret;
string str;
backTracking(ret, str, digits, 0);
return ret;
}
//回溯三步:
//1、确定递归函数参数及返回值
void backTracking(vector<string>& ret, string& str, string& digits, int index) {
//2、确定终止条件
if (str.size() == digits.size()) {
ret.push_back(str);
return;
}
//3、单次递归逻辑
int digit = digits[index] - '0';
string s = letterMap[digit];
for (int i = 0; i < s.size(); ++i) {
str.push_back(s[i]);
backTracking(ret, str, digits, index + 1); //这里是index + 1,与之前不同,因为下一次要处理下一个数字了
str.pop_back(); //回溯
}
}
private:
string letterMap[10] = {
"", //0
"", //1
"abc", //2
"def", //3
"ghi", //4
"jkl", //5
"mno", //6
"pqrs", //7
"tuv", //8
"wxyz" //9
};
};
小总结:组合问题根据模板去写就好,关于回溯的模板我放在了最后的总结里。
1.2.2 分割
1、131.分割回文串
class Solution {
public:
vector<vector<string>> partition(string s) {
vector<vector<string>> ret;
if (s.size() == 0) return ret;
vector<string> cur;
backTracking(ret, cur, s, 0);
return ret;
}
//回溯三步:
//1、确定递归函数参数及返回值
void backTracking(vector<vector<string>>& ret, vector<string>& cur, string& s, int startIndex) {
//2、确定终止条件
if (startIndex >= s.size()) {
ret.push_back(cur);
return;
}
//3、单次递归逻辑
for (int i = startIndex; i < s.size(); ++i) {
if (isPalindrome(s, startIndex, i)) {
string str = s.substr(startIndex, i - startIndex + 1);
cur.push_back(str);
} else continue;
backTracking(ret, cur, s, i + 1);
cur.pop_back(); //回溯
}
}
//判断是否是回文串
bool isPalindrome(const string& s, int start, int end) {
int left = start;
int right = end;
while (left < right) {
if (s[left] != s[right]) return false;
++left;
--right;
}
return true;
}
};
2、93.复原IP地址
class Solution {
public:
vector<string> restoreIpAddresses(string s) {
vector<string> ret;
if (s.size() < 4 || s.size() > 12) return ret;
backTracking(ret, s, 0, 0);
return ret;
}
//回溯三步:
//1、确定递归函数参数及返回值
void backTracking(vector<string>& ret, string s, int startIndex, int pointNum) {
//2、确定终止条件
//当“.”的个数为3时结束
if (pointNum == 3) {
if (isValid(s, startIndex, s.size() - 1)) {
ret.push_back(s);
}
return;
}
//3、单次递归逻辑
for (int i = startIndex; i < s.size(); ++i) {
if (isValid(s, startIndex, i)) {
s.insert(s.begin() + i + 1, '.');
backTracking(ret, s, i + 2, pointNum + 1);//因为加入了’.‘所以要往后移动2位
s.erase(s.begin() + i + 1);//回溯删掉’.‘
} else break;
}
}
bool isValid(const string& s, int start, int end) {
if (start > end) return false;
//0开头的数字不合法
if (s[start] == '0' && start != end) return false;
int num = 0;
for (int i = start; i <= end; ++i) {
//遇到非法的数字,不合法
if (s[i] > '9' || s[i] < '0') return false;
num = num * 10 + (s[i] - '0');
//大于255,不合法
if (num > 255) return false;
}
return true;
}
};
小总结:分割问题相对比较难,需要考虑切割的位置,和其它判断,比如是否是回文串、IP是否合法等。
1.2.3 子集
1、78.子集
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> ret;
if (nums.size() == 0) return ret;
vector<int> cur;
backTracking(ret, cur, nums, 0);
return ret;
}
//回溯三步:
//1、确定递归函数参数及返回值
void backTracking(vector<vector<int>>& ret, vector<int>& cur, vector<int>& nums, int index) {
ret.push_back(cur);
//2、确定终止条件
if (index >= nums.size()) return;
//3、单次递归逻辑
for (int i = index; i < nums.size(); ++i) {
cur.push_back(nums[i]);
backTracking(ret, cur, nums, i + 1);
cur.pop_back();//回溯
}
}
};
2、90.子集II
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<vector<int>> ret;
if (nums.size() == 0) return ret;
sort(nums.begin(), nums.end());
vector<int> cur;
backTracking(ret, cur, nums, 0);
return ret;
}
//回溯三步:
//1、确定递归函数参数及返回值
void backTracking(vector<vector<int>>& ret, vector<int>& cur, vector<int>& nums, int index) {
ret.push_back(cur);
//2、确定终止条件
if (index >= nums.size()) return;
//3、单次递归逻辑
for (int i = index; i < nums.size(); ++i) {
if (i > index && nums[i] == nums[i - 1]) continue;
cur.push_back(nums[i]);
backTracking(ret, cur, nums, i + 1);
cur.pop_back();
}
}
};
小总结:子集问题同样不难,直接照着模板写。
1.2.4 排列
1、46.全排列
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> ret;
if (nums.size() == 0) return ret;
vector<int> cur;
vector<bool> used(nums.size(), false);
backTracking(ret, cur, nums, used);
return ret;
}
//回溯三步
//1、确定递归函数参数及返回值
void backTracking(vector<vector<int>>& ret, vector<int>& cur, vector<int>& nums, vector<bool>& used) {
//2、确定终止条件
if (cur.size() == nums.size()) {
ret.push_back(cur);
}
//3、单次递归逻辑
for (int i = 0; i < nums.size(); ++i) {
if (used[i] == true) continue;
used[i] = true;
cur.push_back(nums[i]);
backTracking(ret, cur, nums, used);
cur.pop_back();
used[i] = false;
}
}
};
2、47.全排列II
class Solution {
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<vector<int>> ret;
if (nums.size() == 0) return ret;
sort(nums.begin(), nums.end());
vector<int> cur;
vector<bool> used(nums.size(), false);
backTracking(ret, cur, nums, used);
return ret;
}
//回溯三步
//1、确定递归函数参数及返回值
void backTracking(vector<vector<int>>& ret, vector<int>& cur, vector<int>& nums, vector<bool>& used) {
//2、确定终止条件
if (cur.size() == nums.size()) {
ret.push_back(cur);
return;
}
//3、单次递归逻辑
for (int i = 0; i < nums.size(); ++i) {
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) continue;
if (used[i] == false) {
used[i] = true;
cur.push_back(nums[i]);
backTracking(ret, cur, nums, used);
cur.pop_back();
used[i] = false;
}
}
}
};
3、剑指offer 38
class Solution {
public:
vector<string> permutation(string s) {
set<string> ret;
vector<string> vec;
if (s.size() == 0) return vec;
vector<bool> used(s.size(), false);
string str = "";
backTracking(ret, str, used, s);
for (auto temp : ret) vec.push_back(temp);
return vec;
}
//回溯三步
//1、确定递归函数参数及返回值
void backTracking(set<string>& ret, string& str, vector<bool>& used, string& s) {
//2、确定终止条件
if (str.size() == s.size()) {
ret.insert(str);
return;
}
//3、确定单词递归逻辑
for (int i = 0; i < s.size(); ++i) {
if (used[i] == true) continue;
used[i] = true;
str.push_back(s[i]);
backTracking(ret, str, used, s);
str.pop_back();
used[i] = false;
}
}
};
1.2.5 棋盘问题
1、51.N皇后
//这应该是回溯中最经典的一道题了
//其实回溯部分并不难,但要考虑清楚放下一个皇后之后要对棋盘中的哪些位置进行跟新
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> ret;
if (n == 0) return ret;
vector<string> queen(n, string(n, '.'));
vector<vector<int>> attack(n, vector<int> (n, 0));
backTracking(ret, queen, attack, 0, n);
return ret;
}
void putQueens(vector<vector<int>>& attack, int x, int y) {
static const int dx[] = {-1, -1, -1, 0, 0, 1, 1, 1};
static const int dy[] = {-1, 0, 1, -1, 1, -1, 0, 1};
attack[x][y] = 1;
for (int i = 1; i < attack.size(); ++i) {
for (int j = 0; j < 8; ++j) {
int newX = x + i * dx[j];
int newY = y + i * dy[j];
if (newX >= 0 && newX < attack.size() && newY >= 0 && newY < attack.size()) {
attack[newX][newY] = 1;
}
}
}
}
void backTracking(vector<vector<string>>& ret, vector<string>& queen, vector<vector<int>>& attack, int m, int n) {
if (m == n) {
ret.push_back(queen);
return;
}
for (int j = 0; j < n; ++j) {
if (attack[m][j] == 0) {
vector<vector<int>> temp = attack;
queen[m][j] = 'Q';
putQueens(attack, m, j);
backTracking(ret, queen, attack, m + 1, n);
queen[m][j] = '.';
attack = temp;
}
}
}
};
2、52.N皇后II
//与上一道题类似
class Solution {
public:
int totalNQueens(int n) {
if (n == 0) return 0;
int ret = 0;
vector<vector<int>> attack(n, vector<int> (n, 0));
backTracking(ret, attack, 0, n);
return ret;
}
void putQueens(vector<vector<int>>& attack, int x, int y) {
static const int dx[] = {-1, -1, -1, 0, 0, 1, 1, 1};
static const int dy[] = {-1, 0, 1, -1, 1, -1, 0, 1};
attack[x][y] = 1;
for (int i = 1; i < attack.size(); ++i) {
for (int j = 0; j < 8; ++j) {
int newX = x + i * dx[j];
int newY = y + i * dy[j];
if (newX >= 0 && newX < attack.size() && newY >= 0 && newY < attack.size()) {
attack[newX][newY] = 1;
}
}
}
}
void backTracking(int& ret, vector<vector<int>>& attack, int m, int n) {
if (m == n) {
++ret;
return;
}
for (int j = 0; j < n; ++j) {
if (attack[m][j] == 0) {
vector<vector<int>> temp = attack;
putQueens(attack, m, j);
backTracking(ret, attack, m + 1, n);
attack = temp;
}
}
}
};
3、37.解数独
class Solution {
public:
void solveSudoku(vector<vector<char>>& board) {
backTracking(board);
}
bool isValid(int row, int col, char val, vector<vector<char>>& board) {
//检查行
for (int i = 0; i < 9; ++i) {
if (board[row][i] == val) return false;
}
//检查列
for (int j = 0; j < 9; ++j) {
if (board[j][col] == val) return false;
}
//检查9个方格
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;
}
//这里返回值是bool,找到一组解,就返回
bool backTracking(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;
//依次放入1~9
for (char k = '1'; k <= '9'; ++k) {
//如果可以放入
if (isValid(i, j, k, board)) {
board[i][j] = k;
//如果找到了一组合适的解,返回
if (backTracking(board)) return true;
//回溯
board[i][j] = '.';
}
}
//1~9都试完了还没有合适的解,返回false
return false;
}
}
//遍历完没有返回false,返回true
return true;
}
};
1.2.6 其它
1、491.递增子序列
class Solution {
public:
vector<vector<int>> findSubsequences(vector<int>& nums) {
vector<vector<int>> ret;
if (nums.size() == 0) return ret;
vector<int> cur;
backTracking(ret, cur, nums, 0);
return ret;
}
//回溯三步
//1、确定递归函数参数及返回值
void backTracking(vector<vector<int>>& ret, vector<int>& cur, vector<int>& nums, int index) {
//2、确定终止条件
if (cur.size() > 1) {
ret.push_back(cur);
}
if (index >= nums.size()) {
return;
}
//3、单次递归逻辑
unordered_set<int> tempSet;//用set对本层元素进行去重
for (int i = index; i < nums.size(); ++i) {
if ((!cur.empty() && nums[i] < cur.back()) || tempSet.find(nums[i]) != tempSet.end()) continue;
cur.push_back(nums[i]);
tempSet.insert(nums[i]);
backTracking(ret, cur, nums, i + 1);
cur.pop_back();
}
}
};
1.3 回溯总结
整理一下回溯的模板
void backTracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (遍历) {
处理节点;
backTracking(参数); //递归
回溯;
}
}
回溯类的问题基本上照着模板写就行,无非是有些难题需要在每次的搜索过程中多去增加一些判断条件。