本文为代码随想录的学习笔记捏,可自行搜索公众号学习
目录
组合问题
电话号码字母组合
分割字符串
131. 分割回文串 - 力扣(LeetCode)https://leetcode.cn/problems/palindrome-partitioning/
IP地址分割
求子集问题
子集
78. 子集 - 力扣(LeetCode)https://leetcode.cn/problems/subsets/
递增子序列
重新安排⾏程
n 皇后问题
回溯是一种暴力搜索的算法,可解决以下问题:
为什么是暴力搜索呢,因为这个方法的思想就是列举全部可能的结果,从中取出符合题目要求的结果。
所以说,回溯不高效,并不高效但不代表没用,对于有的问题我们也只能使用回溯解决。
下面的问题就是例子:
回溯是递归的副产品,只要有递归就会有回溯,回溯的本质为穷举,
集合的大小决定了回溯树的宽度,递归的深度决定了回溯树的深度
做题公式如下
void backtracking(参数)
{
if(终止条件){
存放结果;
return;
}
for(选择:本层集合中的元素){
处理节点;
backtracking(参数)
回溯撤销结果
}
}
77. 组合 - 力扣(LeetCode)https://leetcode.cn/problems/combinations/
class Solution {
public:
vector> res;//用于存放结果
vector path;//用来存放符合条件结果
vector> 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
}
减去红色位置的遍历的路径
216. 组合总和 III - 力扣(LeetCode)https://leetcode.cn/problems/combination-sum-iii/
class Solution {
public:
vector> res;
vector path;
vector> 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> res;
vector path;
vector> combinationSum(vector& candidates, int target) {
backtracking(candidates,target,0,0);
return res;
}
void backtracking(vector& candidates,int target,int sum,int startindex){
if(sum==target){ res.push_back(path); return;}
if(sum>target) return;
for(int i=startindex;i
剪枝优化:
如果 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> res;
vector path;
vector> combinationSum2(vector& candidates, int target) {
vector used(candidates.size());
sort(candidates.begin(), candidates.end());
backtracking(candidates,target,0,0,used);
return res;
}
void backtracking(vector& candidates,int target,int sum,int startindex, vector& used){
if(sum==target){ res.push_back(path); return;}
if(sum>target) return;
for(int i=startindex;i
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 res;
vector path;
vector 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;i7?1:0;
char c=(flag-2)*3+offset+i+'a';
path.push_back(c);
backtracking(n,index+1,digits);
path.pop_back();
}
}
};
本题需要将分割为回文字符串的各种方式添加到最终的结果数组中,以分割位置为索引和组合问题比较相似。
思考传参:
- 字符串s
- 切割的索引index
思考终止条件:
如果切割的位置到了字符串的最后一个位置,那么跳出循环,添加路径上的数到结果数组。
ac代码如下:
class Solution {
public:
vector> result;
vector path;
vector> partition(string s) {
backtracking(s,0);
return result;
}
bool ishuiwen(const string& s,int start,int end){
for(int i=start,j=end;i=s.length()){
result.push_back(path);
return;
}
for(int i=index;i
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 res;
vector 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
分割和组合是求回溯树的叶子节点的值,而子集问题是收集树中的所有的节点。
for的遍历从startindex开始,到数组的最后
class Solution {
public:
vector> res;
vector vec;
vector> subsets(vector& nums) {
backtracking(nums,0);
return res;
}
void backtracking(vector& nums,int index){
res.push_back(vec);
for(int i=index;i
491. 递增子序列 - 力扣(LeetCode)https://leetcode.cn/problems/non-decreasing-subsequences/这道题也是求子序列,但是需要的是递增的子序列,因此需要进行判断,同时不允许重复,因此对结果进行去重,添加set,对于每层的元素进行去重
思考参数
void backtracking(vector
& nums,int index); index为遍历到的索引位置
终止条件
题目要求两个元素及以上,重复的两个元素作为特殊的序列
class Solution {
public:
vector> res;
vector path;
vector> findSubsequences(vector& nums) {
res.clear();
path.clear();
backtracking(nums, 0);
return res;
}
void backtracking(vector& nums,int index){
if(path.size()>1){
res.push_back(path);
}
//对每层的结果进行去重
unordered_set uset;
for(int i=index;i
332. 重新安排行程 - 力扣(LeetCode)https://leetcode.cn/problems/reconstruct-itinerary/
本题的标签是深度搜索,但深度搜索中是包含回溯的思想的。
思考本题:
我们首先需要将给出的对应的关系映射成类似邻接表的形式,将他们之间的可到达的关系存储起来
需要输出字母序排序在前面的结果
在回溯时,最终的返回条件(终止条件)的设置
第一个问题,映射可到达的关系,一个机场可以到达多个机场,因此既可以是一对一,也可以是一对多,因此使用unordered_map
> 可以存储相关的信息 ,使用map也可以,map维持存储数据的顺序,同时我们也可以用map > map来进行数据的存储 因为map会对能到达的机场进行排序,因此可以保证每次遍历的时候都是先遍历排序在前的机场。
思考结束的条件,一个机场,可以是出发的地方也是到达的地方
那么需要的机场数为票数加一
if(ticketnum+1=result.size());
class Solution {
public:
vector result;
unordered_map> targets;
vector findItinerary(vector>& tickets) {
targets.clear();
for(const vector& 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& 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;
}
};
51. N 皇后 - 力扣(LeetCode)https://leetcode.cn/problems/n-queens/
n皇后问题要求我们给出所有的可能的排列方式,在二维数组上进行回溯,如下面的回溯树。
回溯的宽度为每列的长度,深度为行数,
构建回溯的逻辑:
for(int col=0;col处理节点
back();递归
回溯,撤销处理结果
}
思考终止条件,如果遍历到叶子节点,就是最后一行的时候,添加结果返回
if(row==n){
result.push_back(chessboard);
return;
}
我们思考什么时候,一个位置是可以进行插入棋子的,
在之前的行的棋子,不在将要插入位置的同一列,不在左上的斜线上和右上的斜线上
bool isvalid(int row,int col,vector
& chessboard,int n){
for(int i=0;iif(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
if(chessboard[i][j]=='Q') return false;
}return true;
}
class Solution {
public:
vector> result;
void backtracking(int n,int row,vector& chessboard){
if(row==n){
result.push_back(chessboard);
return;
}
for(int i=0;i& chessboard,int n){
for(int i=0;i=0&&j>=0;i--,j--){
if(chessboard[i][j]=='Q') return false;
}
for(int i=row-1,j=col+1;i>=0&&j> solveNQueens(int n) {
vector vec(n,string(n, '.'));
backtracking(n,0,vec);
return result;
}
};