目录
SUMMARY
1、什么时候需要sort?
- 给定一个可包含重复数字的序列
nums时需要sort
- 给定一个无重复元素 的整数数组不需要sort
2、什么时候需要startIndex?
- 组合问题需要startIndex
- 不需要startIndex:全排列问题;多个集合取组合,各个集合之间相互不影响(电话号码的字母组合)
3、对于需要startIndex的情况,什么时候iter(nums, i+1),什么时候iter(nums, i)?
- iter(nums, i+1):同枝不包括自己,组合问题
- iter(nums, i):同枝包括自己,某些组合总和问题
4、什么时候需要used数组?
- 同层去重:if(i>0 && used[i] == used[i-1] && used[i-1]==false)continue;(给定一个可包含重复数字的序列
nums时需要同层去重
)- 同枝去重:if(used[i] == true) continue;(对于排列问题,同枝一个元素只能用一次)
5、什么时候需要sum参数?
- 组合总和问题
6、什么时候需要在for中做判断,判断通过才存?
- 分割问题(分割回文串、复原IP地址)
- N皇后
7、return的条件
- 可以考虑path的个数,startIndex的大小,sum==target
理论基础
其实在讲解二叉树的时候,就给大家介绍过回溯,这次正式开启回溯算法,大家可以先看视频,对回溯算法有一个整体的了解。
题目链接/文章讲解:代码随想录
视频讲解:带你学透回溯算法(理论篇)| 回溯法精讲!_哔哩哔哩_bilibili
组合
77. 组合
对着 在 回溯算法理论基础 给出的 代码模板,来做本题组合问题,大家就会发现 写回溯算法套路。本题关于剪枝操作是大家要理解的重点,因为后面很多回溯算法解决的题目,都是这个剪枝套路。
题目链接/文章讲解:代码随想录
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combine(int n, int k) {
iter(n, k, 1);
return res;
}
void iter(int n, int k, int startIndex){
if(k == path.size()){
res.push_back(path);
return ;
}
for(int i = startIndex; i <= n - (k - path.size()) + 1; i++){// 至多起始位置
path.push_back(i);
iter(n, k, i + 1);
path.pop_back();
}
}
};
16.组合总和III
如果把 组合问题理解了,本题就容易一些了。
题目链接/文章讲解:代码随想录
视频讲解:和组合问题有啥区别?回溯算法如何剪枝?| LeetCode:216.组合总和III_哔哩哔哩_bilibili
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
int sumpath = 0;
vector<vector<int>> combinationSum3(int k, int n) {
iter(k, n, 0, 1);
return res;
}
void iter(int k, int n, int sumpath, int startIndex){
if(path.size() == k){
if(sumpath == n){
res.push_back(path);
}
//sumpath = 0;
return ;
}
for(int i = startIndex; i <= 9; i++){
sumpath += i;
path.push_back(i);
iter(k, n, sumpath, i + 1);
path.pop_back();
sumpath -=i;
}
}
};
17.电话号码的字母组合
本题大家刚开始做会有点难度,先自己思考20min,没思路就直接看题解。
这道题更能体现出横向遍历和纵向遍历的含义,这里横向遍历遍历的每个键所代表的字符,纵向遍历遍历的是每个键之间的组合,所以要遍历的是用户输入的数字字符。
什么时候需要startIndex?
- 如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window),216.组合总和III (opens new window)。
- 如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合
string.push_back(单个字符)
题目链接/文章讲解:代码随想录
class Solution {
public:
vector<string> res;
string path;
unordered_map<int,string> map={{2,"abc"},{3,"def"},{4,"ghi"},{5,"jkl"},{6,"mno"},{7,"pqrs"},{8,"tuv"},{9,"wxyz"}};
vector<string> letterCombinations(string digits) {
if(digits == "") return res;
iter(digits, 0);
return res;
}
// index: 用户输入的第index个数字字符
void iter(string digits, int index){
if(path.size() == digits.size()){
res.push_back(path);
return;
}
int digit = digits[index] - '0';
for(int i = 0; i < map[digit].size(); i++){
path.push_back(map[digit][i]);
iter(digits, index + 1);
path.pop_back();
}
}
};
39. 组合总和
本题是 集合里元素可以用无数次,那么和组合问题的差别 其实仅在于 startIndex上的控制
iter(candidates, target, i, sum);//i包括自己;i+1不包括自己
题目链接/文章讲解:代码随想录
视频讲解:带你学透回溯算法-组合总和(对应「leetcode」力扣题目:39.组合总和)| 回溯法精讲!_哔哩哔哩_bilibili
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
iter(candidates, target, 0, 0);
return res;
}
void iter(vector<int>& candidates, int target, int startIndex, int sum){
if(sum > target) return;// 一旦超过target了一定不对了return但不报错
if(sum == target){//找到目标就返回
res.push_back(path);
return ;
}
for(int i = startIndex; i < candidates.size(); i++){
path.push_back(candidates[i]);
sum += candidates[i];
iter(candidates, target, i, sum);//i包括自己;i+1不包括自己
path.pop_back();
sum -= candidates[i];
}
}
};
40.组合总和II
本题开始涉及到一个问题了:去重。
注意题目中给我们 集合是有重复元素的,那么求出来的 组合有可能重复,但题目要求不能有重复组合。
这道题目和39.组合总和 (opens new window)如下区别:
- 本题candidates 中的每个数字在每个组合中只能使用一次。
- 本题数组candidates的元素是有重复的,而39.组合总和 (opens new window)是无重复元素的数组candidates
最后本题和39.组合总和 (opens new window)要求一样,解集不能包含重复的组合。
本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合。
题目链接/文章讲解:代码随想录
视频讲解:回溯算法中的去重,树层去重树枝去重,你弄清楚了没?| LeetCode:40.组合总和II_哔哩哔哩_bilibili
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
vector<bool> used(candidates.size(), false);
iter(candidates, target, 0, 0, used);
return res;
}
void iter(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used){
//if(sum > target) return;
if(sum == target){
res.push_back(path);
return;
}
for(int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i ++){
if(i > 0 && candidates[i - 1] == candidates[i] && used[i - 1] == false){
continue;
}//因为涉及到i-1了所以还得加个i>0的条件
path.push_back(candidates[i]);
used[i] = true;
sum += candidates[i];
iter(candidates, target, sum, i + 1, used);
path.pop_back();
used[i] = false;
sum -= candidates[i];
}
}
};
分割问题
131.分割回文串
本题较难,大家先看视频来理解 分割问题,明天还会有一道分割问题,先打打基础。
str.substr(index, legth)//而不是startIndex,endIndex
class Solution {
public:
vector<vector<string>> res;
vector<string> path;
vector<vector<string>> partition(string s) {
iter(s, 0);
return res;
}
void iter(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)){
path.push_back(s.substr(startIndex, i - startIndex + 1));
}else continue;
iter(s, i + 1);
path.pop_back();
}
}
bool isPalindrome(string s, int startIndex, int endIndex){
for(int i = startIndex, j = endIndex; i < j; i ++,j --){
if(s[i] != s[j]) return false;
}
return true;
}
};
93.复原IP地址
本期本来是很有难度的,不过 大家做完 分割回文串 之后,本题就容易很多了
注意:单独的0是ok的;为了不让stoll超过限制可以先限制str的长度。
题目链接/文章讲解:代码随想录
视频讲解:回溯算法如何分割字符串并判断是合法IP?| LeetCode:93.复原IP地址_哔哩哔哩_bilibili
我的:
class Solution {
public:
vector<string> res;
vector<string> path;
vector<string> restoreIpAddresses(string s) {
iter(s, 0);
return res;
}
void iter(string s, int startIndex){
if(startIndex >= s.size() && path.size() == 4){
string str ="";
int i;
for( i = 0; i < path.size() - 1; i++){
str += path[i];
str +='.';
}
str += path[i];
res.push_back(str);
return;
}
for(int i = startIndex; i < s.size(); i++){
string str = s.substr(startIndex, i - startIndex + 1);
if(right(str)){
path.push_back(str);
}else continue;
iter(s, i + 1);
path.pop_back();
}
}
bool right(string str){
if(str.size() == 1) return true;
if(str.size() >= 4) return false;
if(str[0] == '0') return false;
long long data = stoll(str);
if(data < 0 || data > 255) return false;
return true;
}
};
子集
78.子集
子集问题,就是收集树形结构中,每一个节点的结果。 整体代码其实和 回溯模板都是差不多的。
题目链接/文章讲解:代码随想录
视频讲解:回溯算法解决子集问题,树上节点都是目标集和! | LeetCode:78.子集_哔哩哔哩_bilibili
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
vector<vector<int>> subsets(vector<int>& nums) {
iter(nums, 0);
return res;
}
void iter(vector<int>& nums, int startIndex){
res.push_back(path);
if(startIndex >= nums.size()){ //不加这个条件也ok
return ;
}
for(int i = startIndex; i < nums.size(); i++){
path.push_back(nums[i]);
iter(nums, i + 1);
path.pop_back();
}
}
};
90.子集II
大家之前做了 40.组合总和II 和 78.子集 ,本题就是这两道题目的结合,建议自己独立做一做,本题涉及的知识,之前都讲过,没有新内容。
题目链接/文章讲解:代码随想录
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<bool> used(nums.size(), false);
iter(nums, 0, used);
return res;
}
void iter(vector<int>& nums, int startIndex, vector<bool>& used){
res.push_back(path);
if(startIndex >= nums.size()){
return;
}
for(int i = startIndex; i < nums.size(); i++){
if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;//不执行同层重复
path.push_back(nums[i]);
used[i] = true;
iter(nums, i + 1, used);
path.pop_back();
used[i] = false;
}
}
};
491.递增子序列【难】
本题和大家刚做过的 90.子集II 非常像,但又很不一样,很容易掉坑里。
难点在于不能排序还得去重
视频讲解:回溯算法精讲,树层去重与树枝去重 | LeetCode:491.递增子序列_哔哩哔哩_bilibili
排序
46.全排列
本题重点感受一下,排列问题 与 组合问题,组合总和,子集问题的区别。 为什么排列问题不用 startIndex
视频讲解:组合与排列的区别,回溯算法求解的时候,有何不同?| LeetCode:46.全排列_哔哩哔哩_bilibili
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(), false);
iter(nums, used);
return res;
}
void iter(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] == true) continue;// 一个排列里一个元素只能使用一次
used[i] = true;
path.push_back(nums[i]);
iter(nums, used);
path.pop_back();
used[i] = false;
}
}
};
47.全排列 II
本题 就是我们讲过的 40.组合总和II 去重逻辑 和 46.全排列 的结合,可以先自己做一下,然后重点看一下 文章中 我讲的拓展内容。 used[i - 1] == true 也行,used[i - 1] == false 也行
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<bool> used(nums.size(), false);
iter(nums, used);
return res;
}
void iter(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] == true) continue;
path.push_back(nums[i]);
used[i] = true;
iter(nums, used);
path.pop_back();
used[i] = false;
}
}
};
今天这三道题都非常难,那么这么难的题,为啥一天做三道?
因为 一刷 也不求大家能把这么难的问题解决,所以 大家一刷的时候,就了解一下题目的要求,了解一下解题思路,不求能直接写出代码,先大概熟悉一下这些题,二刷的时候,随着对回溯算法的深入理解,再去解决如下三题。
大家今天的任务,其实是 对回溯算法章节做一个总结就行。
重点是看 回溯算法总结篇:
332.重新安排行程(可跳过)
棋盘
51. N皇后(可跳过)
视频讲解:这就是传说中的N皇后? 回溯算法安排!| LeetCode:51.N皇后_哔哩哔哩_bilibili
class Solution {
public:
vector<vector<string>> res;
vector<vector<string>> solveNQueens(int n) {
vector<string> chess(n,string(n,'.'));
iter(n, 0, chess);
return res;
}
void iter(int n, int row, vector<string>& chess){
if(row == n){
res.push_back(chess);
return;
}
for(int col = 0; col < n; col++){
if(isVaild(row, col, chess)){
chess[row][col] = 'Q';
iter(n, row+1, chess);
chess[row][col] ='.';
}
}
}
bool isVaild(int row, int col, vector<string>& chess){
// 同行无需判断
// 同列
for(int i = 0 ; i < chess.size(); i++){
if(chess[i][col] == 'Q') return false;
}
//45°对角线
for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--){
if(chess[i][j] == 'Q') return false;
}
// 135°对角线
for(int i = row - 1, j = col + 1; i >= 0 && j < chess.size(); i--, j++){
if(chess[i][j] == 'Q') return false;
}
return true;
}
};