回溯
回溯的理解
引入
先通过一个实例来引入这个概念
假设我们此时有两个容积为3箱子和三个编号分别为1,2,3的球,需要将这三个球装到这两个箱子里面去
当然我们可以一眼看出1和2装一个箱子,3装在一个箱子里面,那对于计算机来说要怎么实现这个过程呢?
解决方案
一种可行的方案是,我们对球进行遍历,每次遍历的时候都会有两种选择:放到a箱子or放到b箱子
遍历的 终止条件 \textcolor{red}{终止条件} 终止条件是index==3
总共我门会最多得到8种答案,然后我们对这8种答案逐一判断即可
//bucket为箱子数组大小为2,nums为球数组,大小为3,index为当前遍历到第几层
backtrack(vetcor<int>&bucket,vector<int>&nums,int index){
//终止条件
if(index==3){
for(int i=0;i<2;i++){
if(bucket[i]!=3){//3为目的每个箱子中放的容积
return false;
}
}
return true;
}
for(int i=0;i<2;i++){
bucket[i]+=nums[index];//将当前球加到这个箱子中
if(backtrack(bucket,nums,index+1)){
return true;
}
bucket[i]-=nums[index];//将当前球取出来,为了下一次加入别的箱子
}
return false;
}
上述做法实际就是将所有情况挨个去尝试,所以 回溯实际上就是一种暴力解法 \textcolor{red}{回溯实际上就是一种暴力解法} 回溯实际上就是一种暴力解法,那我们当然不会满足于一种暴力解法来实现我们的需求,所以需要对回溯进行优化, 回溯的尽头是剪枝 \textcolor{red}{回溯的尽头是剪枝} 回溯的尽头是剪枝,所谓剪枝就是将一些重复或者我们通过合理的判断得到这种情况是不可能得到正确答案的,我们可以将这些过程删去,从而达到降低时间复杂度的目的。对于上面的例子,我们可以进行这样的剪枝操作,
1.在每次给bucket[i]装入球时,进行一次判断,如果加入球之后,数量超过了其容积,我们直接continue,因为按这种情况发展下去,一定是错误的.
2.在这样的基础之上,我们还可以在最终的判断当中直接返回true,因为能够一直遍历到最后一个球,则说明他们肯定在两个箱子当中数量均为3 ( 可以用反证法,如果不为 3 ,则肯定有一个超过了 3 ,则在第一次剪枝时已经排除过了 \textcolor{blue}{可以用反证法,如果不为3,则肯定有一个超过了3,则在第一次剪枝时已经排除过了} 可以用反证法,如果不为3,则肯定有一个超过了3,则在第一次剪枝时已经排除过了)
3.可以先对球按照大小排序,将大球先加入进去,这样可以尽快凑满一个箱子
4.如果当前箱子的已有容积和前一个箱子的已有容积相等,则也是可以
经过上述三步优化,我们可以得到最终代码
static bool cmp(const int a,const int b){
return a>b;
}
//bucket为箱子数组大小为2,nums为球数组,大小为3,index为当前遍历到第几层
backtrack(vetcor<int>&bucket,vector<int>&nums,int index){
//终止条件
if(index==3){
return true;
}
for(int i=0;i<2;i++){
if(bucket[i]+nums[index]>3){
continue;
}
if(i>0&&bucket[i]==bucket[i-1]){
continue;
}
bucket[i]+=nums[index];//将当前球加到这个箱子中
if(backtrack(bucket,nums,index+1)){
return true;
}
bucket[i]-=nums[index];//将当前球取出来,为了下一次加入别的箱子
}
return false;
}
sor(nums.begin(),nums.end(),cmp);
实列
题目描述
给定一个整数数组 nums
和一个正整数 k
,找出是否有可能把这个数组分成 k
个非空子集,其总和都相等。
分析
这个题和上面的例子极为相似,我们可以将nums数组当中的元素当作球,k当作箱子的个数来处理
思路
先对问题简单处理,如果nums数组的和除以k,不能除尽,则可以直接返回false
若可以除尽,定义target=ans/k;
target就是最终每个箱子里面所的容积
终止条件:index==nums.size();
按照上面的思路往下做就行
代码
class Solution {
public:
static bool cmp(const int a,const int b){
return a>b;
}
bool backtrack(vector<int>&nums,vector<int>&bucket,int target,int k,int index){
//终止条件
if(index==nums.size()){
return true;
}
for(int i=0;i<k;i++){
if(bucket[i]+nums[index]>target){
continue;
}
if(i>0&&bucket[i]==bucket[i-1]){
continue;
}
bucket[i]+=nums[index];
if(backtrack(nums,bucket,target,k,index+1)){
return true;
}
bucket[i]-=nums[index];
}
return false;
}
bool canPartitionKSubsets(vector<int>& nums, int k) {
int ans=0;
int n=nums.size();
//预判断
for(int a: nums){
ans+=a;
}
if(ans%k!=0){
return false;
}
//定义箱子数组
vector<int> bucket(n);
int target=ans/k;
sort(nums.begin(),nums.end(),cmp);
return backtrack(nums,bucket,target,k,0);
}
};
回溯的作用
当我们在解决一些需要 不断尝试 \textcolor{red}{不断尝试} 不断尝试才可以解决的问题上,回溯是一种良好的思想,不过必须要对剪枝有明确的判断,每一次的剪枝都对时间复杂度有很重要的影响, 回溯的尽头是剪枝 \textcolor{red}{回溯的尽头是剪枝} 回溯的尽头是剪枝.
回溯的一般步骤
1.预处理
2.确定回溯时的终止条件
3. 确定这个过程的内在逻辑 \textcolor{red}{确定这个过程的内在逻辑} 确定这个过程的内在逻辑
4.确定回溯时每一次的变化是什么,而且记得要在当此回溯结束时,给人家回复过去
5.剪枝
注意 \textcolor{red}{注意} 注意 5并不是必须进行的,因为有些在有些问题下,必须要将所有的晴空统统遍历一遍,这个时候一般他给的时间限制时够的,如果还是超时,那么就要考虑另外的方案了,毕竟,回溯本质上是一种 暴力 \textcolor{red}{暴力} 暴力.,而且在进行内在逻辑分析和预处理的时候,有时候就相当于进行了剪枝操作
回溯的用途
一般回溯有如下几个经典使用场景
第一个就是上文所说的球放箱子
2.括号生成
题目描述
给定一个数字n,表示生成括号的对数,输出所以可能的有效括号组合
思路
1.预分析
由于要实现n对有效括号,所以我们最终得到的字符串数组的每个字符串大小都是2*n,记录当前字符串为cur
2.确定回溯终止的条件
index==2*n
3.内在逻辑
左括号的数目一定为n,记录当前已经放置了左括号的数目为left
右括号的数目一定为n,记录当前已经放置了右括号的数目为right
可以放左括号的情况一定为left<n
可以放右括号的情况一定为left>right
4.确定回溯时每一次的变化是什么
在能放左括号的时候给cur加入一个元素’(',结束后弹出
在能放右括号的时候给cur加入一个元素’)',结束后弹出
5.剪枝
世纪上在内在逻辑分析的时候,就已经进行了剪枝操作,不然我们需要凑齐所以情况,然后逐一判断他们的正确性,但是我们按照正确逻辑去加入括号,就相当于直接把错误的情况排除在外了
代码
class Solution {
public:
string cur;
vector<string> res;
void backtrack(int left,int right,int index,int n){
if(2*n==index){
res.push_back(cur);
return;
}
if(left<n){
cur.push_back('(');
backtrack(left+1,right,index+1,n);
cur.pop_back();
}
if(left>right){
cur.push_back(')');
backtrack(left,right+1,index+1,n);
cur.pop_back();
}
return;
}
vector<string> generateParenthesis(int n) {
backtrack(0,0,0,n);
return res;
}
};
3.N皇后2
题目描述
皇后可以吃掉处在同一行和的同一列和同一斜线的棋子,N皇后研究的问题是如何将n个皇后放在n*n的棋盘上,使他们不能相互攻击,返回最终的方案数
思路
1.预处理
我们根据行去回溯
设置两个哈希表 row,b1,b2
row目的是表示当前列已经有皇后了
b1,b2目的是表示当前直线已经有皇后了
row都很容易理解
对于当前直线来说,根据公式
y
=
k
x
+
b
y=kx+b
y=kx+b
斜率k固定为-1和1,所以决定
直线的唯一性
\textcolor{red}{直线的唯一性}
直线的唯一性就取决于b
2.确定回溯的终止条件
cur==n
3.内在逻辑
因为是根据行去遍历
所以维护row和index的唯一性,可以确保皇后的存放位置一定正确
4.确定每一次回溯时,什么东西变了
每一次回溯前,都要把col和index的值设为1
代码
class Solution {
public:
int res;
void backtrack(unordered_map<int>&row,unordered_map<int,int>&b1,unordered_map<int,int>&b2,int cur,int n){
if(cur==n){
res++;
return;
}
for(int i=0;i<n;i++){
if(row[i]==0&&b1[i+cur]==0&&b2[i-cur]==0){
row[i]=1;
b1[i+cur]=1;
b2[i-cur]=1;
backtrack(row,b1,b2,cur+1,n);
row[i]=0;
b1[i+cur]=0;
b2[i-cur]=0;
}
}
return;
}
int totalNQueens(int n) {
unordered_map<int,int> row,b1,b2;
res=0;
backtrack(row,index,0,n);
return res;
}
};
4.字母大小写全排列
题目描述
给定一个字符串s,将s中的字母大小写进行转换,求所有可能的结果
思路
依次去遍历每一个字符,遇到一个字符,我们可以选择将他转变,也可以选择不转变
终止条件
index==n
代码
class Solution {
public:
vector<string> res;
void backtrack(string& s,int index,int n){
if(n==index){
res.push_back(s);
return;
}
if(s[index]>='a'&&s[index]<='z'){
backtrack(s,index+1,n);
s[index]-=32;
backtrack(s,index+1,n);
s[index]+=32;
}else if(s[index]>='A'&&s[index]<='Z'){
backtrack(s,index+1,n);
s[index]+=32;
backtrack(s,index+1,n);
s[index]-=32;
}else{
backtrack(s,index+1,n);
}
return;
}
vector<string> letterCasePermutation(string s) {
int n=s.size();
backtrack(s,0,n);
return res;
}
};
细节
32=‘a’-‘A’;
5.组合总和
题目描述
给定一个正整数的数组candidates,和一个整数target
求在只使用candidates数组里面元素一次或者0次的情况下,求出他们的和是target的组合
思路
预处理
对candidiate排序
2.确定回溯的终止条件
cur== taregt或者index==n
3.内在逻辑
当cur+candidate[i]>target
直接continue;
4.确定每一次回溯时,什么东西变了
cur的大小需要加上candidate[i]的值
代码
class Solution {
public:
vector<int> r;
vector<vector<int>> res;
int n;
void backtrack(vector<int>& candidates, int target,int cur,int index){
if(cur>target){
return;
}
if(cur==target){
res.push_back(r);
return;
}
vector<int> dp(50,0);
for(int i=index;i>=0;i--){
if(dp[candidates[i]]==0){
if(cur+candidates[i]>target){
continue;
}
dp[candidates[i]]=1;
r.push_back(candidates[i]);
backtrack(candidates,target,cur+candidates[i],i-1);
r.pop_back();
}
}
return;
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
n=candidates.size();
sort(candidates.begin(),candidates.end());
for(int i=n-1;i>=0;i--){
if(candidates[i]<=target){
backtrack(candidates,target,0,n-1);
break;
}
}
return res;
}
};
细节
用一个数组dp来保证数据的唯一性