目录
1. 找出所有⼦集的异或总和再求和(easy)
解析:
方法一:
解法二:
总结:
2. 全排列 Ⅱ(medium)
解析:
解法一:只关心“不合法”的分支
解法二:只关心“合法”的分支
总结:
3. 电话号码的字⺟组合(medium)
解析:
1.然后就开始考虑函数头:
2.考虑函数体:
3.出口条件:
总结:
4. 括号⽣成(medium)
解析:
1.函数头:画出决策树
2.函数体:函数体就是来判断当选择'(' 或者 ‘)’ 的边界条件:
3.出口条件
总结:
5. 组合(medium)
解析:
1,函数头:
2.函数体:
3.出口条件:
总结:
6. ⽬标和(medium)
解析:
解法一:设置全局变量
解法二:优化,使用局部变量,不用恢复现场
总结:
7. 组合总和(medium)
解析:
函数头:这里要递归遍历所有的子集,从第0层传入
函数体:
出口条件:
总结:
8.组合总和II
解析:
总结:
9.组合总和III
解析:
总结:
10.字母大小写全排列
解析:
总结:
11. 优美的排列(medium)
解析:
画决策树
出口条件:
函数头:
函数体:
总结:
从这里开始就要进入二维数组的递归回溯+剪枝了:
12. N 皇后(hard)
解析:
画决策树,很重要很重要!!!编辑
全局变量:
函数头:设置n*n大小的棋盘传入dfs,i来设置当前是第几行也就是层级
函数体:
出口条件:只要记录层级i到达n层就说明已经可以放了n个皇后在棋盘上,然后这个时候只需要将棋盘添加到ret内即可。
总结:
13. 有效的数独(medium)
解析:
画图!画图!画图!编辑
总结:
14. 解数独(hard)
决策树!!!
解析:
总结:
从这题开始就要进入矩阵搜索的板块了!:有点洪水灌溉的意思了;
15. 单词搜索(medium)
解析:
这题切入点就是在二维矩阵里找到跟word完全相同的字符串,要保证它每个字符都相连,那么我们就应该在主函数内去寻找word[0],然后不断遍历所有的word[0],直到有一个能返回true,否者最后就返回false。
这里就需要定义dx,dy数组,用向量的方式来方便我们进行前后左右进行查找
然后定义x,y分别就是当前位置的前后左右的值的下标,就开始判断这个下标是否会越界,是否是满足条件的字符,是否是被访问过等一系列问题。
总结:
16. ⻩⾦矿⼯(medium)
解析:
写多了,也就是属于自己的模板题了,要学会多总结。
总结:
17. 不同路径 Ⅲ(hard)
解析:
出口条件:
在就是函数体:
总结:
看到这里终于结束了,为了写这篇文章的总结,真是花了4天时间,每天写一点,每天写一点,因为学校有课的原因平时还要继续学校C++,Linux,HTLM5,CSS3等等一大堆不同的语法内容,可能不能做到太频繁的更新,但是我绝对在保证质量的前提下不停的写博客,绝对不会断更,最后关于暴搜,深搜,回溯小总结:
下期介绍:floodfill(洪水灌溉算法) 算法简介
这一章是递归回溯算法专题的综合练习,一起有十几个题目,已经足够能够完成对递归回溯的清晰认识了,废话不多说,直接上例题:
1. 找出所有⼦集的异或总和再求和(easy)
解析:
方法一:
我第一次写的时候,就是想到很无脑的办法,把所有子集全部存入数组里,然后,最后再把数组里面的所有子集进行^异或运算进行相加。
那么依然是要画决策树:
我想这着第一步就是要进行全局变量的构建:dp[][] ,one[]
递归函数体dfs() 那么在每次进入dfs()的时候都要进行填入dp[],这样才能遍历到每一个子集。
并且要用i来记录是第k层,每次传入都要传入i+1层,这样才能让函数结束,不然就会陷入死循环
函数头dfs(nums,i);
class Solution {
public:
int ret = 0;
vector<vector<int>> dp;
vector<int> one;
void dfs(vector<int>& nums,int k)
{
dp.push_back(one);
for (int i = k; i < nums.size(); i++)
{
one.push_back(nums[i]);
dfs(nums,i+1);
one.pop_back();
}
}
int subsetXORSum(vector<int>& nums) {
dfs(nums,0);
for (int i = 0; i < dp.size(); i++)
{
for (int j = 0; j < dp[i].size(); j++)
{
cout << dp[i][j] << " ";
}
cout << endl;
}
for (int i = 0; i < dp.size(); i++)
{
if (dp[i].size() == 0) ret += 0;
else if (dp[i].size() == 1) ret += dp[i][0];
else
{
int num = dp[i][0];
for (int j = 1; j < dp[i].size(); j++)
{
num ^= dp[i][j];
}
ret += num;
}
}
cout << ret << endl;
return ret;
}
};
解法二:
解法一真的有点过于冗余,虽然思想都是一样的就是求出所有的子集,但是明明可以不用浪费数组空间,只需要用变量path来求出每一个子集的异或结果,然后相加到sum中即可。
其中path都只需要在for循环内进行^=异或运算,包括恢复现场都可以利用^异或运算的消消乐原理。
class Solution {
public:
int sum=0,path=0;
int subsetXORSum(vector<int>& nums) {
dfs(nums,0);
return sum;
}
void dfs(vector<int>& nums,int k)
{
sum+=path;
for(int i=k;i<nums.size();i++)
{
path^=nums[i];
dfs(nums,i+1);
path^=nums[i];
}
}
};
总结:
这一题相对来说还是十分简单的,就是上一个专题的最后一题求子集,只要递归函数体能写对,那怎么写这题都能过。
2. 全排列 Ⅱ(medium)
解析:
在这题里面剪枝策略十分重要,因为要关心去掉所有重复的元素构成的相同子集,那么就有两种剪枝策略:
1.只关心“不合法”的分支
2.只关心“合法”的分支
那么在接下来的两种剪枝讨论里需要考虑的是nums[i-1]==nums[i] 这件事,那么就说明数组可能出现[1,2,1,1] 也可能出先[1,1,1,2]两种情况,那么我们就要最开始就要对数组进行排序,这样才能讲相同数字产生的相同效果全部剪掉。
sort(nums.begin(),nums.end());
解法一:只关心“不合法”的分支
那么如果能看到这里,关于全排列的递归实现不用多解释,这题全排列思路跟上一个专题的全排列一模一样,唯一不同的就是剪枝策略。
那么我们看上面的图,考虑到剪枝只考虑“不合法”的情况,就是说明在这种不合法的时候就跳过这个条件,防止进入下一层。
1.不合法,第一步就是说明check[i]==true,证明这个数字已经在上面的某一层被用过了,不能再重复使用。
2. 或者是再选择一个元素后,前面存在相同的元素,那么就要考虑前面这个相同的元素跟我的关系,来确定我是否能被使用。如nums[i-1]==nums[i]此时前一个元素等于我此时的元素,并且前一个元素跟我属于同一层,仍然没有被使用过,就证明出现了很多相同的元素,产生了相同的效果,这样就要把此时的数字给剪掉,跳过当前的元素,依次类推,知道后面不满足这个不合法的条件。再就是为了判断数组是否越界,就要考虑了下标i不能等于0的情况否则nums[i-1]会越界。
class Solution {
public:
vector<vector<int>> ret;
bool check[8];
vector<int> path;
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(),nums.end());
dfs(nums);
return ret;
}
void dfs(vector<int>& nums)
{
if(path.size()==nums.size())
{
ret.push_back(path);
return;
}
for(int i=0;i<nums.size();i++)
{
if(check[i]||(i!=0&&nums[i]==nums[i-1]&&check[i-1])) continue;
else
{
path.push_back(nums[i]);
check[i]=true;
dfs(nums);
path.pop_back();
check[i]=false;
}
}
}
};
解法二:只关心“合法”的分支
那么只关心合法的分支就是要保证再满足合法的条件下才能进入添加数字到path数组内;
那么考虑合法的条件:
1.当check[i]==false 说明当前数字没有被使用过,可以添加到数组path内,但是这不能作为唯一的标准。
2.并且包括如果i==0 说明当前元素是nums第一个元素,不会出现越界和与后面元素相等冲突的情况,可以直接添加后进入下一层。
nums[i]!=nums[i-1] 说明前一个元素和我当前的元素并不相等,我当前的元素就跟i=0一个性质,能够直接加入到path内。
如果前面条件都不满足了,就说明一定nums[i]==nums[i-1] 一定成立,这是一个隐含条件,因为再上一个已经判断过了。就证明上一个元素跟我当前的元素是相等的。那么想要再这种条件下进入下一层,就必须要让上一个元素nums[i-1]再上面几层就已经被使用过,不会对我产生影响,就要保证check[i-1]==true;
class Solution {
public:
vector<vector<int>> ret;
bool check[8];
vector<int> path;
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(),nums.end());
dfs(nums);
return ret;
}
void dfs(vector<int>& nums)
{
if(path.size()==nums.size())
{
ret.push_back(path);
return;
}
for(int i=0;i<nums.size();i++)
{
if(check[i]==false&&(i==0||nums[i]!=nums[i-1]||check[i-1]==true))
{
path.push_back(nums[i]);
check[i]=true;
dfs(nums);
path.pop_back();
check[i]=false;
}
}
}
};
总结:
全排列II跟全排列I一样,思路是一模一样,就只是再剪枝的策略上有所不同,只需要画清楚决策树,就可以完美的解决剪枝的策略。
3. 电话号码的字⺟组合(medium)
解析:
一眼就知道,这题肯定用hash表存起来,你可以选自数组当哈希,也可以创建哈希,都是一样的。
string hash[10]={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
1.然后就开始考虑函数头:
因为要知道每一层的数字,就要传入字符串digits,还要知道当前是第几层,这样会避免死循环,传入层数k
dfs(digits,k);
2.考虑函数体:
此时由于记录数字的字符串digits我们就要一层for()来访问数字中hash所记录的字符串,然后再通过hash里面的字符串来访问字符,就又是一层for(),此时要单独拿string s来记录该字符串,后面就是最硬的规则,添加字符到path() ,然后进行递归添加,回来后恢复现场,进行删除。
这里注意:
s.erase() 的话会全部删除所有字符,除非往里面添加迭代器,指定删除。
这里就可以用s.pop_back()来指定删除。
3.出口条件:
就当字符串path长度等于叶子节点的时候,就是digits添加完所有可以添加的字符后就可以进行push_back()到ret内了。
class Solution {
public:
vector<string> ret;
unordered_map<int,string> hash;
string path;
vector<string> letterCombinations(string digits) {
if(digits=="") return ret;
hash[2]="abc",hash[3]="def",hash[4]="ghi",hash[5]="jkl",hash[6]="mno",hash[7]="pqrs"
,hash[8]="tuv",hash[9]="wxyz";
dfs(digits,0);
return ret;
}
void dfs(string digits,int k)
{
if(path.size()==digits.size())
{
ret.push_back(path);
return;
}
for(int i=k;i<digits.size();i++)
{
for(int j=0;j<hash[digits[i]-'0'].size();j++)
{
string s=hash[digits[i]-'0'];
path+=s[j];
dfs(digits,i+1);
path.pop_back();
}
}
}
};
总结:
依然是很常规的递归回溯算法,只要画好决策树,确实没有什么很难很难理解的。
4. 括号⽣成(medium)
解析:
依然是简单的递归回溯问题:
1.函数头:画出决策树
那么就是从开始就是考虑同一个问题,是选 '(' 还是选择 ')' ,那么带着这个相同的子问题,设计函数头,n是定义的函数头,要他来判断函数结束的位置, k就是定义我现在递归到了第几层,记录层数。
2.函数体:函数体就是来判断当选择'(' 或者 ‘)’ 的边界条件:
1).选择'(' ,要考虑的相对来说就比较少,只需要考虑递归到最深的深度后,'('个数不要超过n个即可,那么此时要用变量left来记录'('的个数,添加到path字符串中。
if(left < n)
2).选择')' 要看考虑如果第一个是')'怎么办的情况,或者')'比'('个数多怎么办,这些都是非法的问题。那么这两个问题都可以归结到一个问题上,只要左括号个数大于右括号,那么现在的右括号就可以进行添加 ,用right来记录右括号的个数
if(left > right)
3.出口条件
出口条件还是比较简单,子需要到达第k==n*2层 就说明path已经添加了所有的括号,直接ret.push_back()即可。
class Solution {
public:
vector<string> ret;
string path;
int left=0,right=0;
vector<string> generateParenthesis(int n) {
dfs(n,0);
return ret;
}
void dfs(int n,int k)
{
if(k==n*2)
{
ret.push_back(path);
return;
}
cout<<path<<endl;
//选(
if(left<n)
{
path+='(';
left++;
dfs(n,k+1);
path.pop_back();
left--;
}
//选)
if(left>right)
{
right++;
path+=')';
dfs(n,k+1);
right--;
path.pop_back();
}
}
};
总结:
这题还是比较简单的,只是简单的递归回溯加剪枝,减去那些不必要进入的层级,只要加一个判断条件即可。
5. 组合(medium)
题目意思很简单,就是【1-n】,然后有数字k,保证不含相同子集的数字个数为k。
解析:
这题就是求子集问题,真的很简单,前面已经练习很多遍了。
1,函数头:
因为要记录我当前递归的层数m 和 当前可以加入path的值pos,所以
dfs(m,pos);
2.函数体:
就是简单记录我当前要加入path的值pos 然后依旧老规矩,添加path 进入递归(添加层数m+1,当前数字+1)保证下一层是从我当前数字的下一个位置开始的。
3.出口条件:
就是当我递归的层数到了k层,就说明path已经添加完了,直接ret.push_back()就行。
class Solution {
public:
vector<vector<int>> ret;
vector<int> path;
int n,k;
vector<vector<int>> combine(int _n, int _k) {
n=_n,k=_k;
dfs(0,1);
return ret;
}
void dfs(int m,int pos)
{
if(m==k)
{
ret.push_back(path);
return;
}
for(int i=pos;i<=n;i++)
{
path.push_back(i);
dfs(m+1,i+1);
path.pop_back();
}
}
};
总结:
这题跟前面题目一模一样,真的很简单,可以自己练练手。
6. ⽬标和(medium)
解析:
解法一:设置全局变量
任然是跟前面题目大差不差,就是利用递归的方法,利用全局变量,进行递归,考虑当前数字是+还是-,然后进行递归dfs,然后恢复现场。但是这种时间复杂度特别高,时间感人,还可以继续考虑优化。
class Solution {
public:
int n;
int ret=0,target,num=0;
int findTargetSumWays(vector<int>& nums, int _target) {
n=nums.size();
target=_target;
dfs(nums,0);
return ret;
}
void dfs(vector<int>& nums,int k)
{
if(k==nums.size())
{
if(num==target) ret++;
return;
}
//加法
num+=nums[k];
dfs(nums,k+1);
num-=nums[k];
//减法
num-=nums[k];
dfs(nums,k+1);
num+=nums[k];
}
};
解法二:优化,使用局部变量,不用恢复现场
class Solution {
public:
int n;
int ret=0,target,num=0;
int findTargetSumWays(vector<int>& nums, int _target) {
n=nums.size();
target=_target;
dfs(nums,0,0);
return ret;
}
void dfs(vector<int>& nums,int k,int pos)
{
if(k==nums.size())
{
if(pos==target) ret++;
return;
}
//加法
dfs(nums,k+1,pos+nums[k]);
//减法
dfs(nums,k+1,pos-nums[k]);
}
};
总结:
再写这种类似的递归回溯题目时候,可以先尝试考虑使用全局变量,如果行不通,在考虑局部变量进行优化。
7. 组合总和(medium)
解析:
重要的事说一遍!!!画决策树!
题目意思说一个数字可以重复使用,但是问题就是不能出现相同的子集。
那就说明再选取数字2后,得到2的所有情况,那后面所有数字的情况都不能包含2,即后面的所有数字都不能包含前面的数字。那么递归的时候,就要保证此时还能遍历到当前的数字,而不包括前面的数字,那么就是传入到下一层的时候,此时for里面的i还应该等于上一层的i,所以传入的函数dfs(candidates,i)这里的i不用++
函数头:这里要递归遍历所有的子集,从第0层传入
dfs(canditates,k);
函数体:
从当前层的第i个数字开始往后遍历所有的数字的所有相加的情况,直到满足出口条件了,就结束递归因为要从当前值开始遍历相加后面所有值的情况,这里就要用到for循环,再for内进行递归dfs(i),这里条件值传入i,是因为为了保证下一层相加的值仍然能从当前只开始,不会跳过相加到最后全是当前值的情况。
出口条件:
当遇到比目标值大或者等于的时候就不用再递归下去,直接进行返回上一层。
class Solution {
public:
int n;
vector<vector<int>> ret;
vector<int> path;
int target;
int sum=0;
vector<vector<int>> combinationSum(vector<int>& candidates, int _target) {
n=candidates.size();
target=_target;
dfs(candidates,0);
return ret;
}
void dfs(vector<int>& candidates,int k)
{
if(sum>=target)
{
if(sum==target) ret.push_back(path);
return;
}
for(int i=k;i<n;i++)
{
path.push_back(candidates[i]);
sum+=candidates[i];
dfs(candidates,i);
path.pop_back();
sum-=candidates[i];
}
}
};
总结:
写到这里来说,像这种题,递归回溯剪枝,他就没有固定的模板,但是每题的思路又大差不差,所以还是很值得深思,只要会求出所有子集问题,就大概能解决这些题目。~
8.组合总和II
题目意思很简单,跟组合总和I不同的就是这里面存在多个相同的数字,但是每个数字只能使用一次,并且要设计不能含有相同数字的子集。
解析:
这题画决策树会发现,跟之前做的一道题非常类似
本题由于存在多个相同的数字再同一个数组内,但是如果深一点考虑,就会发现如果当前一个数字递归完所有结果后,到第二个相同的数字时,就会出现完全相同的递归结果,那么就要想办法取消后面相同数字的递归结果,但是又不能直接取消,因为再第一个数字进行递归的时候还是需要的。
那么就要考虑剪枝的策略,剪掉第二个以上的相同的数字,但是再第一个数字的位置不能剪掉。
那么,这里考虑不合法的情况:
if(i!=0&&candidates[i-1]==candidates[i]&&check[i-1]==false) continue;
这里就是为了防止越界,i!=0,因为当i等于0的时候,此时一定是合法的,考虑到不合法的情况就是再前一个数字跟我当前的数字相等的时候,此时前一个数字还是false,就表明此时是从我当前的数字开始的第一层,前面相同的数字还没用过,但其实是再上一轮循环就已经被使用过了,这里就已经可以看出我是已经被重复的元素,如果上一个数字是true就表明,上一个相同的数字再上一层被使用过,还能证明他是第一个相同的元素,那么我当前的元素仍然可以进入循环。
class Solution {
public:
int n,target,sum=0;
vector<vector<int>> ret;
vector<int> path;
bool check[101];
vector<vector<int>> combinationSum2(vector<int>& candidates, int _target) {
sort(candidates.begin(),candidates.end());
n=candidates.size();
target=_target;
dfs(candidates,0);
return ret;
}
void dfs(vector<int>& candidates,int k)
{
if(sum>=target)
{
if(sum==target) ret.push_back(path);
return;
}
for(int i=k;i<n;i++)
{
if(i!=0&&candidates[i-1]==candidates[i]&&check[i-1]==false) continue;
else
{
check[i]=true;
path.push_back(candidates[i]);
sum+=candidates[i];
dfs(candidates,i+1);
path.pop_back();
sum-=candidates[i];
check[i]=false;
}
}
}
};
总结:
这些题目都大差不差,都是利用相同的递归回溯问题,要认真思考递归下去回来后的情况,想清楚每一个细节,绝对可以cv。
9.组合总和III
题目意思还是比较简单的,就是求出1-9的子集,每个子集要有k个数,和要等于目标值n。
解析:
要不要定义数组都行,反正也只开9个空间,如何依旧是简单的递归加回溯,这里唯一要注意的就是递归的层数跟当数字开始进行递归,后面要添加的数字个数k个要区分开,所以函数头要传入两个参数:
函数头:dfs(0,0) 一个代表递归的当前的层数,一个代表添加数字的个数,只有添加的数字个数达到了k个,才能证明可以进行返回上一层,或者sum>n就要进行返回
函数体:依旧是简单的递归回溯问题由于每个数字只能出现一次,那么遍历的数字只能往后走,不能往前看。传入(i+1,w+1);
出口条件:当添加w添加的数字到达k个或者sum总和大于n了就可以进行返回了。
class Solution {
public:
vector<vector<int>> ret;
vector<int> path;
int n,k;
int sum=0;
int a[9]={1,2,3,4,5,6,7,8,9};
vector<vector<int>> combinationSum3(int _k, int _n) {
k=_k,n=_n;
dfs(0,0);
return ret;
}
void dfs(int pos,int w)
{
if(w>=k||sum>=n)
{
if(w==k&&sum==n) ret.push_back(path);
return;
}
for(int i=pos;i<9;i++)
{
path.push_back(a[i]);
sum+=a[i];
dfs(i+1,w+1);
path.pop_back();
sum-=a[i];
}
}
};
总结:
依旧是简单的递归回溯问题,多练练就全会了。
10.字母大小写全排列
题目意思很简单就是遍历整个字符串,然后对每一个字符进行是否要修改两种选择。
解析:
唯一需要单独考虑的就是当前的字符是不是字母,然后进行是否要替换两种选择,如果要替换就单独+-32, 然后再考虑不用改变字符的情况。
class Solution {
public:
vector<string> ret;
int n;
string path;
string s;
vector<string> letterCasePermutation(string _s) {
if(_s=="") return ret;
s=_s;
n=s.size();
dfs(0);
return ret;
}
void dfs(int pos)
{
if(pos==n)
{
ret.push_back(path);
return;
}
//变
if(s[pos]>='a'&&s[pos]<='z')
{
path+=s[pos]-32;
dfs(pos+1);
path.pop_back();
//不变
path.push_back(s[pos]);
dfs(pos+1);
path.pop_back();
}
else if(s[pos]>='A'&&s[pos]<='Z')
{
path+=s[pos]+32;
dfs(pos+1);
path.pop_back();
//不变
path.push_back(s[pos]);
dfs(pos+1);
path.pop_back();
}
else
{
path+=s[pos];
dfs(pos+1);
path.pop_back();
}
}
};
总结:
这题又跟上面求子集不同,只需要遍历整个字符串,然后进行是否选择当前字符进行修改即可。
11. 优美的排列(medium)
解析:
画决策树
因为下标是从1开始,这里就是一个坑点,如果不注意的话,能一直死再这里。那么pos传入的时候就可以设置成1,来表示传入现在第几层。
出口条件:
当传入1-n所有元素后,就说名此时已经有n层,但是pos是从1开始的,那么就是再pos==n+1层结束。
函数头:
dfs(1); 只要记录当前层数,传入pos即可。
函数体:
下标从1开始那么就要判断剪枝条件,当前数字是false才能进入,并且要满足两个条件里面的一个才行,所以这里用bool check[]数组来标记当前的数组是否被使用过。
这里for(i) i就充当的是数组[1-n],pos就相当于添加到优美数组的下标,也是层级。
if(check[i]==false&&(i%pos==0||pos%i==0))
class Solution {
public:
int ret,n;
bool check[16];
int countArrangement(int _n) {
n=_n;
dfs(1);
return ret;
}
void dfs(int pos)
{
if(pos==n+1)
{
ret++;
return;
}
for(int i=1;i<=n;i++)
{
if(check[i]==false&&(i%pos==0||pos%i==0))
{
check[i]=true;
dfs(pos+1);
check[i]=false;
}
}
}
};
总结:
这题又跟上面决策树又不一样,要遍历整个数组进行剪枝,所以要用到check数组来判断当前位置的数组是否合法。
从这里开始就要进入二维数组的递归回溯+剪枝了:
12. N 皇后(hard)
解析:
画决策树,很重要很重要!!!
比如我们从3*3的棋盘大小出发,分别考虑行号和列号,对于行号,应该考虑的是,在每一行都进行遍历它的列号,那么对于行号应该只是在函数头进行传递,从第0行开始进行传递,直到最后一行结束,那么就在每一层即每一行内进行循环判断当前的列是否满足能够被按行皇后。
全局变量:
col[] //判断当前列是否存在皇后
dig1[] //判断当前位置的对角线是否存在皇后
dig2[] //判断当前位置的斜对角线是否存在皇后
函数头:设置n*n大小的棋盘传入dfs,i来设置当前是第几行也就是层级
dfs(path,i)
函数体:
我觉得这题虽然有点难,但是练习了上面那么多道题目,这个函数体还是非常简单的。
主要就是剪枝问题,只要考虑道能够满足合法的条件就能够进入dfs进入下一层:
if(col[j]==false&&dig1[i-j+n]==false&&dig2[j+i+n]==false)
当当前列,当前对角线,斜对角线都是满足false的时候就能够放入皇后,那么就可以进入下一层,那么这里就要注意改变col,dig1,dig2后返回到该层的时候就要恢复现场
path[i][j]="Q";
col[j]=true;
dig1[i-j+n]=true;
dig2[j+i+n]=true;
dfs(path,i+1);
path[i][j]=".";
col[j]=false;
dig1[i-j+n]=false;
dig2[j+i+n]=false;
出口条件:只要记录层级i到达n层就说明已经可以放了n个皇后在棋盘上,然后这个时候只需要将棋盘添加到ret内即可。
class Solution {
public:
bool col[10];
bool dig1[30];
bool dig2[30];
vector<vector<string>> ret;
int n;
vector<vector<string>> solveNQueens(int _n) {
n=_n;
vector<vector<string>> path(n,vector<string>(n,"."));
dfs(path,0);
return ret;
}
void dfs(vector<vector<string>>& path,int i)
{
if(i==n)
{
vector<string> nums;
for(int i=0;i<n;i++)
{
string s;
for(int j=0;j<n;j++)
{
s+=path[i][j];
}
nums.push_back(s);
}
ret.push_back(nums);
return;
}
for(int j=0;j<n;j++)
{
if(col[j]==false&&dig1[i-j+n]==false&&dig2[j+i+n]==false)
{
path[i][j]="Q";
col[j]=true;
dig1[i-j+n]=true;
dig2[j+i+n]=true;
dfs(path,i+1);
path[i][j]=".";
col[j]=false;
dig1[i-j+n]=false;
dig2[j+i+n]=false;
}
}
}
};
总结:
N皇后问题虽然难,但是只要有前面题目的铺垫,现在在看这题,真的能发现自己的进步。只要画好决策树,真的就是信手拈来。
13. 有效的数独(medium)
解析:
画图!画图!画图!
此题不是说非要太满整个数独盘,只需要判断当前的数独盘是不是已经出现了重复的元素即可。
那么此题就是判断各行和各列是否出现了相同的数字,因为在这个数独盘上只有数字[1-9],那么就可以分别证明行,列上是否出现了相同的数字。
设置bool 数组来设置每个数字在当前行或者当前列或者这个3*3的方格中是否存在相同的数字,因为我们要时刻记录[1-9]这些数字内的每一个数字,都是否重复存在,就将每一个数字单独进行记录。
那么定义行
row[9][10];
cal[9][10];
grid[3][3][10];
用两层for循环进行遍历二维数组的每一个数字,分别用bool行 和 bool列 来进行观察这个数字之前是否存在过,即row[]第一个[]表示行或列下标 row[][]第二个[]表示当前的数字,如果出现过就为true,此时就直接返回false,否则继续判断
上面是对整行和整列的判断,那么对于3*3的各种又要单独进行考虑:
grid[3][3][10];
因为可以观察到这个9*9的格子在下标[0-9]中有0-2,3-5,6-8三种表示,然后分别除3就会完美的被分成3*3的格子从而将完美的将9*9的格子分成9个3*3的格子。所以就定义除一个三维数组
grid[3][3][10];分别表示:行数/3,列数/3,当前位置的数字,就可以判断这个数字是否在这个3*3的格子内出现过。
class Solution {
public:
bool row[9][10];
bool cal[9][10];
bool grid[3][3][10];
bool isValidSudoku(vector<vector<char>>& board) {
return dfs(board);
}
bool dfs(vector<vector<char>>& board)
{
for(int i=0;i<9;i++)
{
for(int j=0;j<9;j++)
{
int sum=0;
char _sum=board[i][j];
if(_sum=='.') continue;
else sum=_sum-'0';
if(row[i][sum]==false&&cal[j][sum]==false)
{
row[i][sum]=true;
cal[j][sum]=true;
}
else return false;
if(grid[i/3][j/3][sum]==false)
{
grid[i/3][j/3][sum]=true;
}
else return false;
}
}
return true;
}
};
总结:
观察后这题不用进行递归,只要进行两层for循环遍历完整个二维数组就可以得到最后的结果~,但也是为了后面的题目做铺垫。
14. 解数独(hard)
决策树!!!
解析:
通过决策树可以看到,如果我们开始就把这个数独已经存在的数字放入bool数组内,然后在开始填入就会方便很多。
这题通过决策树可以看到最主要的就是剪枝问题,因为每次在这个空格处填入的数字都是从1开始遍历填的,所以先来一次两层循环直到遇到了空格处,按照常规,进行第一步剪枝条件:
if(row[i][k]==false&&cal[j][k]==false&&grid[i/3][j/3][k]==false)
从[1-9]开始的每一个数字进行填入,遇到可以填入的数字,就把当前位置的bool数组修改为true,保证后面不会填入重复的数字,然后进行递归下去,此时就要重新进入dfs函数,重新进入循环就是开始寻找这一行的下一个空格,直到遇到上图我画的情况,在这一行的最后一个数还没有得到返回true的结果,那么就说明此时遍历完所有的[1-9]的情况就要直接返回false,说明当前这个空格已经失败了,不可取,说明上一层也要进行改变。
这题最主要的就是:在递归这里要判断下层返回回来的是不是false,如果是false,就证明下层遇到了重复的元素,并且不能进行填入,说明我当前层和上层都要进行改变;如果返回的树true,就证明可以告诉上层,当前填的值是正确的。
if(dfs(board)) return true; //重点理解
class Solution {
public:
bool row[9][10];
bool cal[9][10];
bool grid[3][3][10];
void solveSudoku(vector<vector<char>>& board) {
for(int i=0;i<9;i++)
{
for(int j=0;j<9;j++)
{
char _sum=board[i][j];
int sum=0;
if(_sum!='.')
{
sum=_sum-'0';
row[i][sum]=true;
cal[j][sum]=true;
grid[i/3][j/3][sum]=true;
}
}
}
dfs(board);
}
bool dfs(vector<vector<char>>& board)
{
for(int i=0;i<9;i++)
for(int j=0;j<9;j++)
{
char _sum=board[i][j];
int sum=0;
if(_sum=='.')
{
for(int k=1;k<=9;k++)
{
if(row[i][k]==false&&cal[j][k]==false&&grid[i/3][j/3][k]==false)
{
row[i][k]=true;
cal[j][k]=true;
grid[i/3][j/3][k]=true;
board[i][j]=k+'0';
if(dfs(board)) return true; //重点理解
//恢复现场
row[i][k]=false;
cal[j][k]=false;
grid[i/3][j/3][k]=false;
board[i][j]='.';
}
}
return false; //重点理解
}
}
return true; //重点理解
}
};
总结:
这一题是很值得深度思考的一题,很能帮助我们进行解决剪枝的问题,还是建议多思考,多判断这题进行递归的条件和返回值的位置,多判断在哪个位置进行递归,哪个位置进行返回,我相信会有巨大的收获。
从这题开始就要进入矩阵搜索的板块了!:有点洪水灌溉的意思了;
int dx[4]={0,-1,0,1};
int dy[4]={-1,0,1,0};for(int k=0;k<4;k++)
{
int x=i+dx[k];
int y=j+dy[k];
if(x>=0&&x<m&&y>=0&&y<n&&board[x][y]==word[pos]&&visit[x][y]==false)
{
visit[x][y]=true;
if(dfs(board,word,x,y,pos+1)) return true;
visit[x][y]=false;
}
}
return false;
15. 单词搜索(medium)
解析:
这题切入点就是在二维矩阵里找到跟word完全相同的字符串,要保证它每个字符都相连,那么我们就应该在主函数内去寻找word[0],然后不断遍历所有的word[0],直到有一个能返回true,否者最后就返回false。
这里最重要的一点就是,在主函数内进行准备递归之前,要将当前位置的字符的visit设置为true,这样才能让递归下去的字符串不出错。
visit[i][j]=true;
那么就从word[0]进入后,就要开始前后左右来寻找word的下一个字符,那么就是要在四个方位上去寻找,如果有相同的就进在进入下一层,那么记录查找到字符串的第几个字符pos 就进行+1,去寻找下一个字符。
int dx[4]={0,-1,0,1};
int dy[4]={-1,0,1,0};
这里就需要定义dx,dy数组,用向量的方式来方便我们进行前后左右进行查找
int x=i+dx[k];
int y=j+dy[k];
然后定义x,y分别就是当前位置的前后左右的值的下标,就开始判断这个下标是否会越界,是否是满足条件的字符,是否是被访问过等一系列问题。
if(x>=0&&x<m&&y>=0&&y<n&&board[x][y]==word[pos]&&visit[x][y]==false)
判断完成后,就开始进行递归,这里需要注意的就是visit数组要手动设置为true,然后递归到下一层寻找下一个字符,直到遍历完前后左右4个位置的字符都不满足的话,就返回false;若pos==word.size()就说明已经找到了所有的字符,就可以返回true。
class Solution {
public:
int n,m;
bool visit[16][16];
int dx[4]={0,-1,0,1};
int dy[4]={-1,0,1,0};
bool exist(vector<vector<char>>& board, string word) {
m=board.size(),n=board[0].size();
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
if(board[i][j]==word[0])
{
visit[i][j]=true;
bool ret=dfs(board,word,i,j,1);
if(ret) return true;
visit[i][j]=false;
}
}
}
return false;
}
bool dfs(vector<vector<char>>& board,string word,int i,int j,int pos)
{
if(pos==word.size()) return true;
for(int k=0;k<4;k++)
{
int x=i+dx[k];
int y=j+dy[k];
if(x>=0&&x<m&&y>=0&&y<n&&board[x][y]==word[pos]&&visit[x][y]==false)
{
visit[x][y]=true;
if(dfs(board,word,x,y,pos+1)) return true;
visit[x][y]=false;
}
}
return false;
}
};
总结:
这是相当于洪水灌溉类第一个题目吧,二维矩阵搜索,还是比较简单的,也有参考意义,适合大家多思考多总结~
16. ⻩⾦矿⼯(medium)
解析:
这题跟上题简直一模一样,不过多赘述,就是在主函数开始从每一个不为0的位置开始进行访问,然后利用洪水灌溉的模式加上每一个位置的值,进行递归式访问,+到sum上,一直都让sum跟ret取最大值,直到遍历完所有的结果返回最大的ret。
同样跟上题一模一样,就是设置向量,来控制前后左右的位置,依旧是判断当前位置的周围位置,即前后左右x,y是否会越界:
for(int k=0;k<4;k++)
{
int x=dx[k]+i;
int y=dy[k]+j;if(x>=0&&x<m&&y>=0&&y<n&&grid[x][y]!=0&&visit[x][y]==false)
{
sum+=grid[x][y];
ret=max(ret,sum);
visit[x][y]=true;
dfs(grid,x,y);
visit[x][y]=false;
sum-=grid[x][y];
}
}
写多了,也就是属于自己的模板题了,要学会多总结。
这里唯一需要注意的就是要在刚进入这个dfs的时候也要进行比较大小,因为这个时候的数字可能比前面所包含的sum和都要大,所以要单独进行比较一下。
class Solution {
public:
int ret=0,sum=0;
int dx[4]={0,-1,0,1};
int dy[4]={-1,0,1,0};
bool visit[16][16];
int n,m;
int getMaximumGold(vector<vector<int>>& grid) {
m=grid.size(),n=grid[0].size();
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
if(grid[i][j]!=0)
{
visit[i][j]=true;
sum+=grid[i][j];
ret=max(ret,sum);
dfs(grid,i,j);
sum-=grid[i][j];
visit[i][j]=false;
}
}
}
return ret;
}
void dfs(vector<vector<int>>& grid,int i,int j)
{
for(int k=0;k<4;k++)
{
int x=dx[k]+i;
int y=dy[k]+j;
if(x>=0&&x<m&&y>=0&&y<n&&grid[x][y]!=0&&visit[x][y]==false)
{
sum+=grid[x][y];
ret=max(ret,sum);
visit[x][y]=true;
dfs(grid,x,y);
visit[x][y]=false;
sum-=grid[x][y];
}
}
}
};
// [0, 0, 34,0,5, 0, 7,0,0, 0]
// [0, 0, 0, 0,21,0, 0,0,0, 0]
// [0, 18,0, 0,8, 0, 0,0,4, 0]
// [0, 0, 0, 0,0, 0, 0,0,0, 0]
// [15,0, 0, 0,0, 22,0,0,0, 21]
// [0, 0, 0, 0,0, 0, 0,0,0, 0]
// [0, 7, 0, 0,0, 0, 0,0,38,0]
总结:
题目不难,主要就是要学会自己总结模板,但也不要死记硬背,理解了也就自然敲的出来了。
17. 不同路径 Ⅲ(hard)
解析:
不要看他是一个困难题,如果这题用动态规划确实很难,但是用暴搜确实很暴力,但是也变得非常简单了。
画图可以知道,只要从1开始走,到2结束,那么记录中间的所有0的个数即可,那么我就先提前记录所有0的个数和1的位置,然后进行进入dfs。
出口条件:
这里的出口条件要单独拿出来说一下,这里的出口条件就是在从1进入后开始遍历所有0的位置,当sum中0的个数==count后并不能代表ret就可以++,而是要额外判断,结束的位置的周围是否有2的存在,如果有,就说明能够从2出去,没有就会失败!
在就是函数体:
感觉越界没什么好讲的,前面这么多题,函数体都是一模一样的,全部都是设置x,y即周围位置的值,可以让它进行查找0的存在,然后不停的进行递归,知道最后如果失败了就进行回溯。
for(int k=0;k<4;k++)
{
int x=i+dx[k];
int y=j+dy[k];
if(x>=0&&x<m&&y>=0&&y<n&&grid[x][y]==0&&visit[x][y]==false)
{
visit[x][y]=true;
sum++;
dfs(grid,x,y);
visit[x][y]=false;
sum--;
}
}
class Solution {
public:
int n,m;
int dx[4]={0,-1,0,1};
int dy[4]={-1,0,1,0};
bool visit[20][20];
int count=0,ret=0,sum=0;
int uniquePathsIII(vector<vector<int>>& grid) {
m=grid.size(),n=grid[0].size();
int xi,yi;
for(int i=0;i<m;i++)
for(int j=0;j<n;j++)
{
if(grid[i][j]==0) count++;
else if(grid[i][j]==1) xi=i,yi=j;
}
dfs(grid,xi,yi);
return ret;
}
void dfs(vector<vector<int>>& grid,int i,int j)
{
if(count==sum)
{
for(int k=0;k<4;k++)
{
int x=i+dx[k];
int y=j+dy[k];
if(x>=0&&x<m&&y>=0&&y<n&&grid[x][y]==2) ret++;
}
return;
}
for(int k=0;k<4;k++)
{
int x=i+dx[k];
int y=j+dy[k];
if(x>=0&&x<m&&y>=0&&y<n&&grid[x][y]==0&&visit[x][y]==false)
{
visit[x][y]=true;
sum++;
dfs(grid,x,y);
visit[x][y]=false;
sum--;
}
}
}
};
总结:
虽然这题是困难题,但是用洪水灌溉思想真的很简单,就是暴力搜索,在二维矩阵内查找所有0的个数,并且求出合法的路径个数。
看到这里终于结束了,为了写这篇文章的总结,真是花了4天时间,每天写一点,每天写一点,因为学校有课的原因平时还要继续学校C++,Linux,HTLM5,CSS3等等一大堆不同的语法内容,可能不能做到太频繁的更新,但是我绝对在保证质量的前提下不停的写博客,绝对不会断更,最后关于暴搜,深搜,回溯小总结:
算法原理并不难
考察的是:代码能力,思路转化为代码
下期介绍:floodfill(洪水灌溉算法) 算法简介
性质相同的一个连通块,斜对角不算联通,只有上下左右才算联通。就是不断的在每一个位置进行深度优先遍历,在每一个位置进行上下左右扫描,知道走不动了就进行回溯。
这一期对我的收获巨大,创作不易,希望能对你也能产生巨大帮助!!!~