点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
1.找出所有子集的异或总和再求和
题目链接:1863. 找出所有子集的异或总和再求和
题目描述:
先找出所有子集,然后把每个子集异或的和加起来返回去。
算法原理:
这道题和我们上一道思路完全是一模一样,
- 先画出决策树
- 设计代码
全局变量
递归函数
细节:回溯、剪枝、递归出口
因为我们有上一道题的基础,我们直接就画出决策树
这里我们需要两个全局变量,一个path记录到沿途每个子集的异或,然后sum负责每个字节的异或和加起来。dfs函数,还是需要一个pos记录当前异或的位置,dfs(nums,pos),回溯利用异或的规则,两个相同的数异或为0。这里也没有剪枝, 递归出口 循环结束就是递归出口。
class Solution {
int sum=0;
int path=0;
public:
int subsetXORSum(vector<int>& nums) {
dfs(nums,0);
return sum;
}
void dfs(vector<int>& nums,int pos)
{
sum+=path;
for(int i=pos;i<nums.size();++i)
{
path^=nums[i];
dfs(nums,i+1);
path^=nums[i]; // 恢复现场
}
}
};
2.全排列 II
题目链接:47. 全排列 II
题目分析:
重复的数全排列后会有重复的结果,这道题就是要求去掉重乎之后的全排列
算法原理:
这道题几乎和全排列1 一模一样,我们就不在细说那些决策树怎么画,代码应该怎么写等等。这里主要就是剪枝的问题。
下面我们边画决策树变分析问题,把全排列所有不重复不漏的情况画出来,越详细越好。 我们只用关心四个位置,每个位置每次从数组中4个数选择一个树放到一个位置上就行了。只用选四次就行了。
第一次可以选第一个1、第二个1、第三个1、2,但是注意这里就存在剪枝的问题了,如果第一个位置还把第二个1和第三个1选上,此时就会存在重复问题!因为后面三个位置是从112中选的。
此时就出现了第一种剪枝情况
同一个节点的所有分支中,相同的元素只能选择一次
然后我们再往下走,第二个位置也可以从数组中4个数字中选任意一个。但是第一个1我们要把它剪掉,因为第一个位置已经把第一个1选过了,只能选一次。此时就有了第二种剪枝情况,这个是和全排列1一模一样的。
同一个位置的数,只能使用一次 ,
还是用bool类型的check数组可以实现,check[i] = true 表明已经使用过了,
check[i] = false 说明还没有使用过。
但是这里还会出现剪枝情况,第一个1不能用,那我可以把第二个1放在第二个位置,但是第三个1不能出现了,因为同一个节点分支相同的数只能出现一次!
接下来我们就不画了,我们就可以写代码了。代码逻辑和全排列1几乎一模一样,这里我们主要分析,剪枝应该怎么写。剪枝可以从两个角度写。其实就是两个if判定语句。
1.只关心 “不合法” 的分支。 不合法的直接不让递归下去
2.只关心 “合法” 的分支 合法的就递归
虽然是两个角度,但是最终得到结果都是一样的。
只关心 “不合法” 的分支
- 当有一个位置已经选了这个数了下一个位置就不能在选这个位置,check[i] == true
- 属于同一个分支节点,前面的数和当前的数相等 就不能选当前的数了。nums[i] == nums[i-1],但是这里有一个问题,我们这个数组里面的数字是有序的,可以这样写,如果无序就不能这样写,因此最开始先对数组进行排序。但是还是有问题,目前nums[i] == nums[i-1]这个条件太广泛了,注意看它目前只适用于第一个位置中不选择相同数,但是在第二个位置中又出现第二个1可以选第三个1不选的情况。因此还要再加条件,注意我们是从第一个位置递归下去到第二个位置然后出现相同的位置不选,但是当我们递归返回的时候这个数又可以选了。这个check[i]==true是上一层的和当前属于第二层无关!我们关注的是同一层相同元素只能选一次。因此这个条件是
nums[i] == nums[i-1] && check[i-1] == false,但是还不够,可能会有越界的风险,因此这个条件最终是 i != 0 && nums[i] == nums[i-1] && check[i-1] == false
把上面两个条件组合在一起就得到只关心 “不合法” 的分支
只关心 “合法” 的分支
- 当一个数没人选的时候可以选这个数 check[i] == false,
- 但注意到同一层可能有相同的数,第一个相同的数没人选因为是第一次出现确实是可以选的,但是如果当前的数和前面的数相同即使当前数没人选也是不能选的,因此 还要满足 nums[i] != num[i-1] 只要这个数不和前面相同说明就是第一次出现了绝对可以选!。但是还有一种情况,如果有相同的数字,也就是 nums[i] != num[i-1] 不满足的情况,那就是满足 nums[i] == num[i-1] 前面数和后面数相同的情况,如果上一个位置选了前面的数,那走下到下一个位置的时候,因为这个数已经选过了check[i] == true,其实也就说明这个数是上一个位置的数,和我本次第二个位置选数无关,即使是相同的数子也没有关系,我也是能够选择这个后面相同的数。也就是要满足 check[i-1] == true 前面的相同数被上一个位置选了。还有最后一个情况,刚开始i==0肯定是第一次出现的数并且这个数字没人选的时候,可以选。
class Solution {
vector<vector<int>> ret;
vector<int> path;
bool check[9];
public:
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)
{
// 剪枝
// 1.只关心不合法
// if(check[i] == true ||(i != 0 && nums[i] == nums[i-1] && check[i-1] == false))
// continue;
// path.push_back(nums[i]);
// check[i]=true;
// dfs(nums);
// check[i]=false;
// path.pop_back();//恢复现场
// 2.只关心合法
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);
check[i]=false;
path.pop_back();//恢复现场
}
}
}
};
3.电话号码的字母组合
题目链接:17. 电话号码的字母组合
题目分析:
就是数字对应的字符串进行排列组合。对于这种搜索啊、暴搜啊,我们已经知道要用到递归,回溯、剪枝了。
算法原理:
有了前面的基础,这个题我们就不写那么步骤了,画出决策树,我们直接写出对应需要的东西。不过在此之前我们需要先将数字与字符串映射关系搞和,我们可以用哈希表映射,或者其他方法,这里最简单的就是弄一个字符串数组把数字和字符串映射一下。
接下来就是画出决策树然后分析一下,首先需要两个全局变量 ret记录结果,path记录每条路径到叶子节点的组合,递归函数 给我一个digits和数字然后递归函数把组合后的结果返回出来,相信它一定能完成任务。dfs(digits,pos),然后回溯 记得恢复现场,递归出口 到叶子节点,这道题没有剪枝。
class Solution {
public:
string numberletter[10]={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
vector<string> ret;
string path;
vector<string> letterCombinations(string digits) {
if(digits.empty()) return ret;
dfs(digits,0);
return ret;
}
void dfs(string& digits,int pos)
{
if(pos == digits.size())
{
ret.push_back(path);
return;
}
string str=numberletter[digits[pos]-'0'];
for(int i=0;i<str.size();++i)
{
path+=str[i];
dfs(digits,pos+1);
path.pop_back();
}
}
};
4.括号生成
题目链接:22. 括号生成
题目分析:
给几对括号,然后把括号组合一下形成 有效括号
算法原理:
首先我们要知道什么是 有效括号的组合
1.左括号的数量 = 右括号的数量
2.从头开始的任意一个子串,左括号数量 >= 右括号数量
如下面第二种情况,虽然满足条件1但是并不满足条件2,画线字串右括号数量 > 左括号数量
对于这样暴力枚举的所有情况的问题,我们还是画一颗决策树,把所有情况不重不漏的情况都画出来,然后根据这棵树我们写代码。
每个位置都有两种选择,但是注意到刚开始就有剪枝的情况,刚开始不能选右括号,因为不满足条件2,所以右括号我们要分情况剪枝。当右括号数量大于等于左括号的数量时此时不能添加右括号 right >= left。还有当左括号的数量大于等于n时此时就没有左括号可以选了,left >= n。后面情况都是这样分析的,因此我们就可以做写代码的准备了。
全局变量,需要一个 left 记录左括号的数量,right 记录右括号的数量,还有一个n记录有几对括号要组合。还需要一个ret记录结果,path记录每条路径的结果。递归函数 每个位置都有两种选择。因为上面都是用的全局变量,因此递归函数参数什么都不用传了。dfs()。回溯 当把path放到ret里,返回后要恢复现场。pop掉path最后一个位置元素。剪枝 前面已经分析了。递归出口 当right==n的时候说明括号组合完了。
class Solution {
public:
int left=0,right=0;
vector<string> ret;
string path;
vector<string> generateParenthesis(int n)
{
dfs(n);
return ret;
}
void dfs(int& n)
{
if(right == n)
{
ret.push_back(path);
return;
}
if(left < n) //添加左括号
{
path+='(';++left;
dfs(n);
path.pop_back();--left;//恢复现场
}
if(right<left)//添加右括号
{
path+=')';++right;
dfs(n);
path.pop_back();--right;//恢复现场
}
}
};