目录
前言
本文章将从回溯算法的适用场景,使用方式展开,然后落实到使用回溯算法的经典力扣题目中,希望能对大家有所帮助
一、回溯算法相关知识
回溯法可抽象为n叉树的结构,宽度为集合的大小,纵方向为递归处理(只要有递归,就会有回溯),其本身属于纯暴力搜索
1.1 回溯算法用于解决的问题
组合问题(组合为无序,【1,2】和【2,1】意义相同),可以将回溯算法抽象为树,组合的结果就是树中的树叶,而回溯算法的路径就构成了整个树的结构
切割问题(字符串切割的方式),子集问题,这类问题,就需要对回溯算法中的单层搜索逻辑进行限制,使得满足条件
排列问题,这属于对组合问题的一个延伸,可见的是会对剪枝操作进行一定的减少限制
棋盘问题 ,这是对回溯算法的直观演绎,因为需要对每一个棋子进行判断回溯
1.2 回溯算法使用模板
模板:终止条件+单层搜索逻辑
终止条件:(在递归回溯函数的最上面),对传入的元素进行首先判断,一般与return搭配使用
单层搜索逻辑:这里一般是遍历形成,结构是循环中,判断成立条件,设置数组存入元素后,进行递归回溯,然后再从数组弹出元素。可以概括为,收集结果(集合),集合元素,处理节点。
二、回溯算法经典力扣题目
组合 77
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
class Solution {
private:
vector<vector<int>> result;
vector<int> single;
void GetResult(int n, int k,int startindex) {
//终止条件
if(single.size() == k){
result.push_back(single);
return;
}
//递归--从startindex开始
for(int i = startindex;i<=n;i++){
single.push_back(i);
GetResult(n,k,i+1);
single.pop_back();//回溯,去掉之前遍历的节点
}
}
public:
vector<vector<int>> combine(int n, int k) {
GetResult(n,k,1);
return result;
}
};
题解:
本题用回溯算法解题,设置回溯函数的关键分别为终止条件和递归算法,在本题中,使用一个二维数组保存另一个递归获得的一维数组的思路,当递归中的数组长度满足要求,就向上回溯,清空当前存储的值,重新以上节点向下递归遍历。依次遍历,最终返回存储了这些数组的二维数组
组合总和三 216
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7 输出: [[1,2,4]] 解释: 1 + 2 + 4 = 7 没有其他符合的组合了。
class Solution {
private:
vector<vector<int>> result;//保存的数组
vector<int> single;
void GetResult(int targerSum,int k,int sum,int currentIndex){
//终止条件
if(single.size()==k){
if(sum == targerSum){
//满足条件则返回
result.push_back(single);
}
return;
}
//递归过程
for(int i = currentIndex;i<=9;i++){
sum += i;
single.push_back(i);
GetResult(targerSum,k,sum,i+1);//组合
//回溯
single.pop_back();
sum -= i;
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
GetResult(n,k,0,1);
return result;
}
};
题解:
这一题采用的思路和解法类似于组合这道题,思路上仍然是二维数组依次保存一维数组,结构上是先设置终止条件,后设置递归过程,跟组合那道题不同的是,终止条件还需要满足sum和目标sum相同才会存储进入二维数组,递归过程要sum加上,回溯需要sum减去对应的值。通过以上过程,最终得到包含所有组合的二维数组。
电话号码的字母组合 17
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
class Solution {
private:
const string letters[10]={
"",//索引为0
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz",
};
vector<string> result;
void GetResult(const string& digits,int index,const string& s){
if(digits.size()==index){
result.push_back(s);//存入结果数组
return;
}
int currentindex = digits[index]-'0';//获得当前第一个数字
string currentss = letters[currentindex];//获得当前的string
//从当前的string向下遍历
for(int i=0;i<currentss.size();i++){
//遍历过程
GetResult(digits,index+1,s+currentss[i]);
}
}
public:
vector<string> letterCombinations(string digits) {
result.clear();
if(digits.size()==0){
return result;
}
GetResult(digits,0,"");
return result;
}
};
题解:
首先确定本题思路为回溯算法,需要通过最初字符串的每一个字符,按顺序从第一个字符开始,获得它对应的字符串,然后从字符串的每一个字符向下递归。具体实现如下:
终止条件是当前index(保存递归次数)和最初字符串长度相同,如果相同就将之前传进来的字符串s存入。在接下来的递归过程中,每次传入最初字符串、index+1(标志着向下递归)和需要保存入最终string<vector>容器的字符串(该字符串为当前得到的字符串加上当前遍历的字符)。如此不断递归,回溯上来之后,遍历继续向下进行(脑图上回溯呈波浪式)
组合总和 39
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
示例 1:
输入:candidates =[2,3,6,7],
target =7
输出:[[2,2,3],[7]] 解释: 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7 。 仅有这两种组合。
class Solution {
private:
vector<vector<int>> result;
vector<int> single;
void GetResult(vector<int>& candidates,int target,int startIndex,int sum){
//终止条件
if(sum > target){
return;//不满足也要回溯上去
}
if(sum == target){
result.push_back(single);
return;
}
//单层搜索逻辑
for(int i =startIndex;i<candidates.size();i++){
sum += candidates[i];
single.push_back(candidates[i]);//存储和刷新sum和
GetResult(candidates,target,i,sum);
//回溯的刷新
sum -= candidates[i];
single.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
//传入数组和目标值
GetResult(candidates,target,0,0);
return result;
}
};
题解:
本题是组合问题的一种形式,区别于上面几道组合题,这道组合题不限制单个组合的数量,这就导致转化成树无法计算树的高度,需要优化"单层搜索的逻辑",递归的时候,每次传入的startindex就是当前的数的索引(因为可以无限选取),还需要优化"终止条件",对不满足的和满足的进行判断,并且返回,注意如果当前的和已经大于了目标值,就没必要向下继续讨论
组合总和二
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates =[10,1,2,7,6,1,5]
, target =8
, 输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]
class Solution {
private:
vector<vector<int>> result;
vector<int> comb;
void GetResult(vector<int>& candidates,int target,int sum,int startindex,vector<bool>& used){
if(sum >target){
return;
}
if(sum == target){
result.push_back(comb);
return;
}
//单层搜索逻辑
for(int i = startindex;i<candidates.size();i++){
if(i>0 && candidates[i]==candidates[i-1] && used[i-1] == false){
//used判定的作用这个时候就体现
//used如果为负,作为树枝的根节点就使用过了
continue;//当前情况说明前一个根节点使用过了,避免重合,则跳过当前准备使用的根节点
}
sum += candidates[i];
used[i] = true;//作为树枝的根节点还没有被使用
comb.push_back(candidates[i]);
GetResult(candidates,target,sum,i+1,used);//向下递归
comb.pop_back();
used[i] = false;//作为树枝的根节点就使用过了
sum -= candidates[i];
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(),false);//定义标识数组
result.clear();
comb.clear();
sort(candidates.begin(),candidates.end());
GetResult(candidates,target,0,0,used);
return result;
}
};
题解:
本题主要是考虑到如何去重,其他的逻辑和之前组合问题相似,也是在递归函数中,设置终止条件和单层搜索逻辑,不同的是,搜索逻辑上需要上一层锁,为了防止重复,设置used布尔数组,在每次传入前设置当前索引对应值为真,代表还没有作为根节点使用过,当回溯上来后,再设置当前索引对应值为假,这样就可以在一开始判断,如果前一个被使用过,并且值相同,就不再对这个节点进行逻辑操作(因为会重复)。其他地方和一般组合问题相同,索引加1传入递归函数,向下递归
分割字符串 131
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是 回文串 。返回 s
所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
输入:s = "aab" 输出:[["a","a","b"],["aa","b"]]
class Solution {
private:
vector<vector<string>> result;
vector<string> single;//小组合
vector<vector<bool>> isOFM;//回文串判断数组
void GetRusult(const string& s,int startIndex){
//终止条件
if(startIndex >= s.size()){
result.push_back(single);//当前的single已经是一个结果集合了
return;//已经没有可遍历的了,就终止并返回
}
//单层搜索逻辑-->从第一个数字起一个个向后讨论
for(int i=startIndex;i<s.size();i++){
if(isOFM[startIndex][i]){
//回文串
string str = s.substr(startIndex,i-startIndex+1);//切割出从startindex开始并且长度是到i的字符串
single.push_back(str);
}else{
continue;//否则继续遍历寻找
}
GetRusult(s,i+1);//如果当前已经满足,则分割,向下递归
single.pop_back();
}
}
void JudgeISOFM(const string& s){
isOFM.resize(s.size(),vector<bool>(s.size(),false));//将isofm的每个元素(一维数组)赋值为大小为size,值为false的一维数组
for(int i = s.size()-1;i>=0;i--){
for(int j = i;j<s.size();j++){
//从后往前的遍历方式,为了回文串判断与定义的方便
if(j == i){
isOFM[i][j] = true;//重合一定自身为回文串
}else if(j-i==1){
isOFM[i][j] = (s[i]==s[j]);//挨着的时候,一定要彼此相等
}else{
isOFM[i][j] = (s[i]==s[j] && isOFM[i+1][j-1]);//不挨着的时候,因为j在逐渐像size靠近,只要缩小区间还能相等,那么内部一定完全对称--回文串
}
}
}
}
public:
vector<vector<string>> partition(string s) {
result.clear();
single.clear();
JudgeISOFM(s);
GetRusult(s,0);
return result;
}
};
题解:
本题不同于一般的组合问题,首先是递归思路上面,需要讨论所有的字符串的切割可能,所以思路是从0开始切割,直到满足回文串后,切割成为一个切割集合中的一个元素,并且从下一个元素开始递归,终止条件也要改变为判断到最后一个元素才返回,区别是在于本题要求切割集合的集合,所以才有了以上的改变。在判断回文串方面,使用倒序讨论,分三种情况-重合、挨着、有距离,分别进行对称相同判断,同时在“有距离”的判断中,也使用到了类似递归的一个思维,就是如果缩短范围仍然满足回文串,那么才能说明当前范围内是满足回文串的。
复原ip地址 93
有效 IP 地址 正好由四个整数(每个整数位于 0
到 255
之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
- 例如:
"0.1.2.201"
和"192.168.1.1"
是 有效 IP 地址,但是"0.011.255.245"
、"192.168.1.312"
和"192.168@1.1"
是 无效 IP 地址。
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s
中插入 '.'
来形成。你 不能 重新排序或删除 s
中的任何数字。你可以按 任何 顺序返回答案。
示例 1:
输入:s = "25525511135" 输出:["255.255.11.135","255.255.111.35"]
class Solution {
private:
vector<string> result;//字符串集合
void GetResult(string& s,int startIndex,int pointcount){
if(pointcount == 3){
//已经打上了3个点
if(isVaild(s,startIndex,s.size()-1)){
result.push_back(s);//保存入结果
}
return;
}
for(int i = startIndex;i<s.size();i++){
if(isVaild(s,startIndex,i)){
//startindex始终作为前一个切割点
pointcount++;
s.insert(s.begin()+i+1,'.');//在指定位置之前插入一个.
GetResult(s,i+2,pointcount);//从插入的位置的下个位置开始切割
pointcount--;
s.erase(s.begin()+i+1);//回溯删除所有的
}else{
break;
//不满足就直接退出循环
}
}
}
bool isVaild(string& s,int start,int end){
if(start > end){
return false;
}
if(s[start] == '0' && start != end){
//该数字以零开头,自然不合理
return false;
}
int sum = 0;
for(int i = start;i<=end;i++){
if(s[i] < '0' ||s[i] > '9'){
return false;//确保为正数字
}
sum = sum*10 + (s[i] - '0');//遍历使得sum为当前的数字总和
if(sum > 255){
return false;
}
}
return true;
}
public:
vector<string> restoreIpAddresses(string s) {
result.clear();
if(s.size()<4 || s.size()>12){
return result;//剪枝操作
}
GetResult(s,0,0);
return result;
}
};
题解:
这道题类似于上一题切割回文串,这里也是要切割但是不同的是,这一题的切割是切割完了之后,就直接加上一个点,然后切割的逻辑和回文一致,都是切割到底,直到满足后存入vector容器,再向上回溯。本题也需要设置一个判断的条件,判断字符串是否有效,涉及到ip地址的相关知识,不能大于225并且为一个正整数,并且数字组合不可以以0开头(重要)。
子集 78
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
class Solution {
private:
vector<vector<int>> result;
vector<int> single;
void GetResult(vector<int>& nums,int startindex){
result.push_back(single);//每次递归的时候,需要把之前的数组立刻存储进入结果集-->同时包括了空的子集
if(startindex >= nums.size()){
return;
}
for(int i =startindex;i<nums.size();i++){
single.push_back(nums[i]);
GetResult(nums,i+1);//组合问题从下一个开始遍历
single.pop_back();
}
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
result.clear();
single.clear();
GetResult(nums,0);
return result;
}
};
题解-注意点:
本题用到了回溯算法,需要将各种组合放入集合中,将整道题的思路变为树型结构后不难发现,每个节点都是题目需要的集合元素。需要注意的一点是,需要将每次的裁剪过的元素存入数组中,且没有限制,一个是因为要考虑空集也是子集,还有一个是子集也可以是真子集。在遍历条件方面也和之前的一样,而且比较直观,就是先存入vector容器中,然后递归,回溯上来再弹出存入的元素即可。
子集2:
给你一个整数数组 nums
,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2] 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
class Solution {
private:
vector<vector<int>> result;
vector<int> single;
void GetResult(vector<int>& nums,int startindex,vector<bool>& numUsed){
result.push_back(single);//每个节点都是子集
if(startindex >= nums.size()){
return;
}
for(int i = startindex;i< nums.size();i++){
if(i>0 && nums[i] == nums[i-1] && numUsed[i-1] == false){
continue;//前一个节点不是当前节点的树枝父节点还相等,则跳过该节点
}
single.push_back(nums[i]);
numUsed[i] = true;//树枝已经取用了该点
GetResult(nums,i+1,numUsed);//向下遍历,防止重复
numUsed[i] = false;//该节点不再作为树枝使用
single.pop_back();
}
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
result.clear();
single.clear();
vector<bool> numUsed(nums.size(),false);//false说明该节点和节点平行,位于同一树枝下
sort(nums.begin(),nums.end());//排序,从小到大
GetResult(nums,0,numUsed);
return result;
}
};
题解:
这题和子集1相比加入了一次去重的逻辑,在这里使用vector布尔数组进行去重的保存,在每次遍历之前将当前数组元素,设置为真,用来标记改元素为树枝元素,所以当向下递归的时候,前一个元素与当前元素相同的时候,不要急着去舍弃掉本次递归,要确定好是不是同一层的节点(在同一个树枝下),所以如果前一个元素的vector布尔值为真,说明它是之前的树枝,而本元素是其单独的树叶,这个时候是不需要去重的。相反如果是同一层的,那么前一个节点一定被使用过了,所以这个节点就不需要再向下递归,此时需要去重。最终在执行开始逻辑时,记得对目标数组进行排序,这样才能有序去重
递增子序列 491
给你一个整数数组 nums
,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7] 输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
class Solution {
private:
vector<vector<int>> result;
vector<int> single;
void GetResult(vector<int>& nums,int startindex){
if(single.size()>1){
result.push_back(single);
//不需要return因为接下来还可以继续存储
}
unordered_set<int> used;
for(int i = startindex;i< nums.size();i++){
//去重
if(!single.empty() && nums[i] < single.back() || used.find(nums[i])!= used.end()){
continue;
//分为两种情况 ,一种如果当前元素比之前最后的元素要小,就不必存了,因为已经不为递增
//第二种是当前层(已经默认是当前的层,因为每次要新创建used)如果之前存在过该元素的值,去重
}
used.insert(nums[i]);
single.push_back(nums[i]);
GetResult(nums,i+1);
single.pop_back();
}
}
public:
vector<vector<int>> findSubsequences(vector<int>& nums) {
result.clear();
single.clear();
GetResult(nums,0);
return result;
}
};
题解:
本题是组合问题的一个变形,需要注意几点。一个要注意不能重复元素(去重),一个要保证元素递增。但是本题的前置条件已经简化了这些,题目要求是子序列,说明顺序要按照目标数组来,这样就还是按照顺序遍历处理即可。限制条件中,需要保证是2个元素以上,则需要在一开始的“终止条件”中设置存储限制,同样本题需要所以子序列,所以不用立刻去存储。在去重方面,当前层(已经默认是当前的层,因为每次要新创建used)如果之前存在过该元素的值,则去重该元素。在元素递增方面,当前元素比之前存储的子序列数组最后的元素要小,就不必存了,因为已经不为递增。综上进行递归回溯,则完成本题
全排列 46
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
class Solution {
private:
vector<vector<int>> result;
vector<int> single;
void GetResult(vector<int>& nums,vector<bool>& used){
if(single.size() == nums.size()){
result.push_back(single);
return;//直到大小满足了,才说明当前的single满足要求
}
for(int i =0; i<nums.size();i++){
//每一个元素都是需要必要遍历的
if(used[i] == true){
continue;//说明之前-树枝-已经出现过了该节点
}
used[i] = true;
single.push_back(nums[i]);
GetResult(nums,used);
single.pop_back();
used[i] = false;
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
result.clear();
single.clear();
vector<bool> used(nums.size(),false);
GetResult(nums,used);
return result;
}
};
题解:
本题实际上是组合的引申,有很多组合问题的影子。首先不含重复元素,就可以用到used布尔数组,保存之前树枝的真值,如果为真,则本树叶结点不需要再存入排列中。其次是所有可能的全排列,则需要更改单层处理逻辑为,从0开始遍历,并且在“终止条件”中,当排列数组的大小和目标数组一致的时候,则满足了条件,存入结果集并且回溯
全排列 二
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]
class Solution {
private:
vector<vector<int>> result;
vector<int> single;
void GetResult(vector<int>& nums,vector<bool>& used){
if(single.size() == nums.size()){
result.push_back(single);
return;
}
for(int i=0;i<nums.size();i++){
//全排列去重,需要保证之前遍历过的不能再次遍历-->为true说明为树枝的子节点,所以不用去重
if(i > 0 && nums[i] == nums[i-1] && used[i-1] == false){
continue;//同一层级继续遍历
}
if(used[i] == false){
single.push_back(nums[i]);
used[i] = true;
GetResult(nums,used);
used[i] = false;
single.pop_back();
}//没有了startindex,则用used作为时刻判断条件
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> used(nums.size(),false);
sort(nums.begin(),nums.end());
GetResult(nums,used);
return result;
}
};
题解:
在本题中,注意到全排列,也就是要保证每种顺序都是一种解,所以不存在startindex。但是本题要求全排列不重复且可包含重复数字。如果不含重复数字,只要限制树枝出现过的节点,子节点不再出现即可。但是可包含的情况下,就不能简单的限制出现了,为了保证全排列不重复,那就需要保证树枝的出现,一定是不和前面的树枝重复的,只要树枝相同重复了,那么向下的解一定会重复,同时要确定当前的节点,是没有使用过的,不然如果树枝和树叶重复(这里不仅仅是指数值,而是整体的索引,为了不重复,不可以重复利用索引的,这一点类似组合),这样也是不满足题目要求的,会溢出解。这样检查过之后,才可以存入并进行下面的递归回溯
N皇后 51
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
示例 1:
输入:n = 4 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] 解释:如上图所示,4 皇后问题存在两个不同的解法。
class Solution {
private:
vector<vector<string>> result;
void GetResult(vector<string>& single,int n,int row){
if(row == n){
//注意是row到达需求,因为single大小是一开始定好的
result.push_back(single);
return;
}
//对每一行进行遍历
for(int i = 0;i<n;i++){
if(isvalid(row,i,n,single)){
single[row][i] = 'Q';//字符‘’引用
GetResult(single,n,row+1);
single[row][i] = '.';//回溯返回重新开始一套解
}
}
}
bool isvalid(int row,int col,int n,vector<string>& single)
{
//检查列--上检查--回溯
for(int i = 0;i< row ;i++){
if(single[i][col] == 'Q'){
return false;//说明已经存在
}
}
//检查行--左检查--回溯
for(int i = 0;i<col;i++){
if(single[row][i] == 'Q'){
return false;//说明已经存在
}
}
//检查斜线--45度--向左上--回溯
for(int i = row -1, j = col -1;i>=0 && j>=0;i--,j--){
//每个变量的声明和初始化应该用逗号分割,并且不需要再加上声明类型
if(single[i][j] == 'Q'){
return false;
}
}
//检查斜线--135度--向右上--回溯
for(int i = row -1, j = col +1;i>=0 && j<n ;i--,j++){
if(single[i][j] == 'Q'){
return false;
}
}
return true;
}
public:
vector<vector<string>> solveNQueens(int n) {
result.clear();
vector<string> single(n,string(n,'.'));//创建内部由固定元素string字符串组成的数组
GetResult(single,n,0);
return result;
}
};
题解:
本题N皇后,完美的应用了回溯算法的知识。仍然使用树形结构进行思维,可知每种结果,就是一个树叶节点,从棋盘上的第一行第一列开始遍历,不断向下遍历。遍历到结尾的,也就是树的深度等于目标n,则存入结果而二维数组。每种结果的保存方式也比较不同,创建一个string数组,string为字符串所以其每一个字符都是一个元素,综上这个string数组其实可以看做是一个二维数组。在最重要的单层判断逻辑上,需要从当前的棋字(元素)向上向之前去遍历,具体表现为向上遍历(到本节点前),向左遍历,向左上遍历,向右上遍历,一旦发现元素为'Q',则说明该节点不能成为皇后,则切断该树枝,不再向下递归回溯。
最终通过创建数组,进行递归,剪枝,进行回溯的方式,返回得到目标数组
解数独 37
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
- 数字
1-9
在每一行只能出现一次。 - 数字
1-9
在每一列只能出现一次。 - 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.'
表示。
示例 1:
输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]] 输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]] 解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
class Solution {
private:
bool GetResult(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] == '.'){
for(char k = '1';k<='9';k++){
//字符遍历
if(isVaild(board,i,j,k)){
board[i][j] = k;
if(GetResult(board)){
return true;//在当前的节点赋值下,如果能找到解决方案,就返回真
}
board[i][j] = '.';//没有找到方案,就继续向下遍历
}
}
return false;//遍历后仍然到了这里,说明没有找到可以放置的节点,没有方案
}
}
}
return true;//到这一步同样返回真
}
bool isVaild(vector<vector<char>>& board,int row,int col,int k){
//检查同一行-有没有和目标值一样的元素
for(int i = 0 ;i<board[0].size();i++){
if(board[row][i] == k){
return false;//出现了重复值
}
}
//检查同一列
for(int j = 0;j<board.size();j++){
if(board[j][col] == k){
return false;
}
}
//定位到对应规则化的九宫格内,判断有没有出现元素
int realrow = (row/3)*3;
int realcol = (col/3)*3;
for(int i = realrow;i<realrow+3;i++){
for(int j = realcol;j<realcol+3;j++){
if(board[i][j] == k){
return false;
}
}
}
return true;
}
public:
void solveSudoku(vector<vector<char>>& board) {
GetResult(board);
}
};
题解:
这道题也是使用的回溯算法解决,和一般的组合问题不同的是,本题不用单独列出终止条件,而是直接在函数中return真假即可,这也是和之前组合问题回溯形式不同的地方。整体上,遍历每一处节点,并且遍历当前节点是否可以存储1-9间的任意值,可以存储则继续向下递归回溯,直到最终得到一套解决方案,则会回溯上来真假的返回值,为真则该节点形成的解决方案完全成立,返回真即可(接下来不用操作),反之如果返回假,则当前节点形成的解决方案有问题,则回溯上来,继续按顺序赋值并且遍历,如果遍历到最后都没有成立,则返回假(这个逻辑其实主要是为了递归回溯方便,因为最后读取是直接读board的),在这个过程中,board值被直接改变。
在判断节点是否成立的时候,行和列的逻辑相似,注意临界点行列的不同即可。重要的是需要定位到对应规则化的九宫格内,判断有没有出现元素,这里需要转换坐标为九宫格启动位置。
最终在主函数中调用定义好的递归回溯函数,然后需要的board就会随之被赋值好了。