本文为代码随想录的学习笔记捏,可自行搜索公众号学习
目录
131. 分割回文串 - 力扣(LeetCode)https://leetcode.cn/problems/palindrome-partitioning/
78. 子集 - 力扣(LeetCode)https://leetcode.cn/problems/subsets/
回溯是一种暴力搜索的算法,可解决以下问题:
为什么是暴力搜索呢,因为这个方法的思想就是列举全部可能的结果,从中取出符合题目要求的结果。
所以说,回溯不高效,并不高效但不代表没用,对于有的问题我们也只能使用回溯解决。
下面的问题就是例子:
- 组合问题:N个数里面按照一定的规则找出k个数的集合
- 分割问:一个字符串按照一定规则有几种切割方式
- 子集:一个N个数的集合中有多少符合条件的子集
- 排列:N个数按照一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独
- 其他
回溯是递归的副产品,只要有递归就会有回溯,回溯的本质为穷举,
集合的大小决定了回溯树的宽度,递归的深度决定了回溯树的深度
做题公式如下
void backtracking(参数)
{
if(终止条件){
存放结果;
return;
}
for(选择:本层集合中的元素){
处理节点;
backtracking(参数)
回溯撤销结果
}
}
组合问题
77. 组合 - 力扣(LeetCode)https://leetcode.cn/problems/combinations/
class Solution {
public:
vector<vector<int>> res;//用于存放结果
vector<int> path;//用来存放符合条件结果
vector<vector<int>> combine(int n, int k) {
backtracking(n,k,1);
return res;
}
void backtracking(int n,int k,int index){
if(path.size()==k){
res.push_back(path);
return;
}
for(int i=index;i<=n;i++){
path.push_back(i);//处理节点
backtracking(n,k,i+1);//递归
path.pop_back();//回溯
}
}
};
剪枝优化:
path.size()为已经选定的数据
k-path.size()为还需要的选择的数据
那么索引开始的位置至多为n-(k-path.size())+1
+1是为了左边闭合,因为我们的索引是从一开始
修改结果:
for(int i=index;i<n-(k-path.size())+1;i++){
}
递归树:
减去红色位置的遍历的路径
216. 组合总和 III - 力扣(LeetCode)https://leetcode.cn/problems/combination-sum-iii/
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k,n,1);
return res;
}
void backtracking(int k,int sum,int index){
if(sum==0&&path.size()==k){
res.push_back(path);
return;
}
if(sum<0||path.size()>k){
return;
}
for(int i=index;i<=9;i++){
//对于节点的处理
path.push_back(i);
sum-=i;
backtracking(k,sum,i+1);
sum+=i;
path.pop_back();
}
}
};
39. 组合总和 - 力扣(LeetCode)https://leetcode.cn/problems/combination-sum/ 本问题和之前问题的主要区别就于:元素是可以重复选取的,并且没有个数的限制。
思考回溯函数的参数:
- 数组
- 目标值
- 开始的索引startindex
- 路径中数的总和(可以直接通过target得出)
终止条件:
if(sum==target){ res.push_back(path); return;}
if(sum>target) return;
ac的代码:
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates,target,0,0);
return res;
}
void backtracking(vector<int>& candidates,int target,int sum,int startindex){
if(sum==target){ res.push_back(path); return;}
if(sum>target) return;
for(int i=startindex;i<candidates.size();i++){
//对于本节点的处理
sum+=candidates[i];
path.push_back(candidates[i]);
//递归
backtracking(candidates,target,sum,i);
//回溯
sum-=candidates[i];
path.pop_back();
}
}
};
剪枝优化:
如果 sum + candidates[i] 的值也就是下一轮的sum大于target的情况下,没有必要进入下一轮的递归,在for循环中添加条件sum + candidates[i] <= target
40. 组合总和 II - 力扣(LeetCode)https://leetcode.cn/problems/combination-sum-ii/ 40题和上题比较相似,但是存在细微的差别
- 给定的集合中元素可以重复
- 最终的结果中组合不能重复
针对这两个问题,我们需要进行结果的去重,去重的是在本层递归中已经使用过的相同元素。
那么我们使用一个数组来标识数组中的元素是否被使用过,使用和给定数组同样大小的标识数组,创建used数组标识是否使用访问过某个元素。
参数思考:
- 题目函数给定的参数
- sum代表当前的元素的总和
- startindex代表当前需要开始递归的位置
- used标识元素是否使用过的数组
结束条件和上题一样。
ac的代码如下
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size());
sort(candidates.begin(), candidates.end());
backtracking(candidates,target,0,0,used);
return res;
}
void backtracking(vector<int>& candidates,int target,int sum,int startindex, vector<bool>& used){
if(sum==target){ res.push_back(path); return;}
if(sum>target) return;
for(int i=startindex;i<candidates.size() && sum+candidates[i]<=target;i++){
//相对于同层之前已经使用过的不能再次使用
//对于本节点的处理
sum+=candidates[i];
path.push_back(candidates[i]);
used[i]=true;
//递归
backtracking(candidates,target,sum,i+1,used);
//回溯
used[i]=false;
sum-=candidates[i];
path.pop_back();
}
}
};
if(i>0 && candidates[i]==candidates[i-1] && used[i-1]==false) continue;
意思是现在这个i位置的元素和i-1位置元素是一样的,但是i位置的元素(false)已经用过了,有那条路径,我们进行剪枝,防止结果重复。
used[i - 1] == true ,说明同⼀树⽀ candidates[i - 1] 使⽤过used[i - 1] == false ,说明同⼀树层 candidates[i - 1] 使⽤过
电话号码字母组合
17. 电话号码的字母组合 - 力扣(LeetCode)https://leetcode.cn/problems/letter-combinations-of-a-phone-number/思考需要传输哪些参数:
- path.size()确定好代表字母的按键个数
- 按键位置index
- 按键总个数n
参考了一下:
发现参数n可以不用传参,但都可以的没影响。
class Solution {
public:
vector<string> res;
vector<char> path;
vector<string> letterCombinations(string digits) {
if(digits.size()==0) return res;
backtracking(digits.size(),0,digits);
return res;
}
void backtracking(int n,int index,string digits){
//终止条件
if(n==path.size()){
string s="";
for(auto i:path){
s+=i;
}
res.push_back(s);
return ;
}
int sum=0;
int flag=digits[index]-'0';
if(flag==9 || flag==7) sum=4;
else sum=3;
for(int i=0;i<sum;i++){
//特殊处理
int offset=flag>7?1:0;
char c=(flag-2)*3+offset+i+'a';
path.push_back(c);
backtracking(n,index+1,digits);
path.pop_back();
}
}
};
分割字符串
131. 分割回文串 - 力扣(LeetCode)https://leetcode.cn/problems/palindrome-partitioning/
本题需要将分割为回文字符串的各种方式添加到最终的结果数组中,以分割位置为索引和组合问题比较相似。
思考传参:
- 字符串s
- 切割的索引index
思考终止条件:
如果切割的位置到了字符串的最后一个位置,那么跳出循环,添加路径上的数到结果数组。
ac代码如下:
class Solution {
public:
vector<vector<string>> result;
vector<string> path;
vector<vector<string>> partition(string s) {
backtracking(s,0);
return result;
}
bool ishuiwen(const string& s,int start,int end){
for(int i=start,j=end;i<j;i++,j--){
if(s[i]==s[j]){
continue;
}else{
return false;
}
}
return true;
}
void backtracking(const string& s,int index){
if(index>=s.length()){
result.push_back(path);
return;
}
for(int i=index;i<s.length();i++){
if(ishuiwen(s,index,i)){
string str=s.substr(index,i-index+1);
path.push_back(str);
}
else{
continue;
}
backtracking(s,i+1);
path.pop_back();
}
}
};
IP地址分割
93. 复原 IP 地址 - 力扣(LeetCode)https://leetcode.cn/problems/restore-ip-addresses/与上一道题分割字符串相似,本题也可使用回溯,以分割地址为索引,添加点号,回溯时删掉。
思考参数
void backtracking(string& s,int pointNum,int startindex);
s:字符串
pointNum:已经添加的点号的数量,本体为3时符合返回的条件
startindex:下次分割位置开始选择的索引
终止返回条件
- 添加了三个点
- 第四段数据符合要求
添加isvalid函数用于判断切割的字串:
start-end之间的字串是否符合要求
class Solution {
public:
vector<string> res;
vector<string> restoreIpAddresses(string s) {
res.clear();
if(s.size()<4 || s.size()>12) return res;
backtracking(s,0,0);
return res;
}
bool isvalid(const string& s,int start,int end){
if (start > end) {
return false;
}
if (s[start] == '0' && start != end) { // 0开头的数字不合法
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');
if (num > 255) { // 如果⼤于255了不合法
return false;
}
}
return true;
}
void backtracking(string& s,int sum,int startindex){
if(sum==3){
if(isvalid(s,startindex,s.size()-1)){
res.push_back(s);
}
return;
}
for(int i=startindex;i<s.size();i++){
if(isvalid(s,startindex,i)){
s.insert(s.begin()+i+1,'.');
backtracking(s,sum+1,i+2);
s.erase(s.begin() + i + 1);
}else break;
}
}
};
求子集问题
子集
78. 子集 - 力扣(LeetCode)https://leetcode.cn/problems/subsets/
分割和组合是求回溯树的叶子节点的值,而子集问题是收集树中的所有的节点。
for的遍历从startindex开始,到数组的最后
class Solution {
public:
vector<vector<int>> res;
vector<int> vec;
vector<vector<int>> subsets(vector<int>& nums) {
backtracking(nums,0);
return res;
}
void backtracking(vector<int>& nums,int index){
res.push_back(vec);
for(int i=index;i<nums.size();i++){
vec.push_back(nums[i]);
backtracking(nums,i+1);
vec.pop_back();
}
}
};
递增子序列
491. 递增子序列 - 力扣(LeetCode)https://leetcode.cn/problems/non-decreasing-subsequences/这道题也是求子序列,但是需要的是递增的子序列,因此需要进行判断,同时不允许重复,因此对结果进行去重,添加set,对于每层的元素进行去重
思考参数
void backtracking(vector<int>& nums,int index);
index为遍历到的索引位置
终止条件
题目要求两个元素及以上,重复的两个元素作为特殊的序列
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> findSubsequences(vector<int>& nums) {
res.clear();
path.clear();
backtracking(nums, 0);
return res;
}
void backtracking(vector<int>& nums,int index){
if(path.size()>1){
res.push_back(path);
}
//对每层的结果进行去重
unordered_set<int> uset;
for(int i=index;i<nums.size();i++){
if ((!path.empty() && nums[i] < path.back())
|| uset.find(nums[i]) != uset.end()) {
continue;
}
uset.insert(nums[i]);
path.push_back(nums[i]);
backtracking(nums,i+1);
path.pop_back();
}
}
};
重新安排⾏程
332. 重新安排行程 - 力扣(LeetCode)https://leetcode.cn/problems/reconstruct-itinerary/
本题的标签是深度搜索,但深度搜索中是包含回溯的思想的。
思考本题:
我们首先需要将给出的对应的关系映射成类似邻接表的形式,将他们之间的可到达的关系存储起来
需要输出字母序排序在前面的结果
在回溯时,最终的返回条件(终止条件)的设置
第一个问题,映射可到达的关系,一个机场可以到达多个机场,因此既可以是一对一,也可以是一对多,因此使用unordered_map<string,multiset<string>> 可以存储相关的信息 ,使用map也可以,map维持存储数据的顺序,同时我们也可以用map<string,unordered_map<string,int>> map来进行数据的存储
因为map会对能到达的机场进行排序,因此可以保证每次遍历的时候都是先遍历排序在前的机场。
思考结束的条件,一个机场,可以是出发的地方也是到达的地方
那么需要的机场数为票数加一
if(ticketnum+1=result.size());
class Solution {
public:
vector<string> result;
unordered_map<string,map<string,int>> targets;
vector<string> findItinerary(vector<vector<string>>& tickets) {
targets.clear();
for(const vector<string>& vec:tickets){
targets[vec[0]][vec[1]]++;
}
result.push_back("JFK");
backtracking(tickets.size());
return result;
}
bool backtracking(int ticketNum){
if(ticketNum+1==result.size()){
return true;
}
//对现在到达的机场可到达的机场进行遍历
for(pair<const string,int>& target:targets[result[result.size()-1]]){
if(target.second>0){
result.push_back(target.first);
target.second--;
if(backtracking(ticketNum)) return true;//如果找到了一条路线,就是答案,立即返回
target.second++;
result.pop_back();
}
}
return false;
}
};
n 皇后问题
51. N 皇后 - 力扣(LeetCode)https://leetcode.cn/problems/n-queens/
n皇后问题要求我们给出所有的可能的排列方式,在二维数组上进行回溯,如下面的回溯树。
回溯的宽度为每列的长度,深度为行数,
构建回溯的逻辑:
for(int col=0;col<n;col++){处理节点
back();递归
回溯,撤销处理结果
}
思考终止条件,如果遍历到叶子节点,就是最后一行的时候,添加结果返回
if(row==n){
result.push_back(chessboard);
return;
}
我们思考什么时候,一个位置是可以进行插入棋子的,
在之前的行的棋子,不在将要插入位置的同一列,不在左上的斜线上和右上的斜线上
bool isvalid(int row,int col,vector<string>& chessboard,int n){
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<n;i--,j++){
if(chessboard[i][j]=='Q') return false;
}return true;
}
class Solution {
public:
vector<vector<string>> result;
void backtracking(int n,int row,vector<string>& chessboard){
if(row==n){
result.push_back(chessboard);
return;
}
for(int i=0;i<n;i++){
if(isvalid(row,i,chessboard,n)){
chessboard[row][i] = 'Q';
backtracking(n,row+1,chessboard);
chessboard[row][i] = '.';
}
}
}
bool isvalid(int row,int col,vector<string>& chessboard,int n){
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<n;i--,j++){
if(chessboard[i][j]=='Q') return false;
}
return true;
}
vector<vector<string>> solveNQueens(int n) {
vector<string> vec(n,string(n, '.'));
backtracking(n,0,vec);
return result;
}
};