
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《算法题讲解指南》--优选算法
《算法题讲解指南》--递归、搜索与回溯算法
《算法题讲解指南》--动态规划算法
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
14.找出所有子集的异或总和再求和
题目链接:
题目描述:

题目示例:

解法(递归):
算法思路:
所有子集可以解释为:每个元素选择在或不在一个集合中(因此,子集有2"个)。本题我们需要求出所有子集,将它们的异或和相加。因为异或操作满足交换律,所以我们可以定义一个变量,直接记录当前状态的异或和。使用递归保存当前集合的状态(异或和),选择将当前元素添加至当前状态与否,并依次递归数组中下一个元素。当递归到空元素时,表示所有元素都被考虑到,记录当前状态(将当前状态的异或和添加至答案中)。
例如集合中的元素为[1,2],则它的子集状态选择过程如下:
/ \
\] \[1\] //第一个元素选择与否 / \\ / \\ \[ \] \[2\] \[1\] \[1, 2\] //第二个元素选择与否,每个状态到这⼀层时需要记录异或和
递归函数设计:
void dfs(int val,int idx, vector<int>& nums)
参数:val(当前状态的异或和),idx(当前需要处理的元素下标,处理过程:选择将其添加至当前状态或不进行操作);
返回值:无;
函数作用:选择对元素进行添加与否处理。
递归流程:
1.递归结束条件:当前下标与数组长度相等,即已经越界,表示已经考虑到所有元素;
a.将当前异或和添加至答案中,并返回;
2.考虑将当前元素添加至当前状态,当前状态更新为与当前元素值的异或和,然后递归下一个元素;
3.考虑不选择当前元素,当前状态不变,直接递归下一个元素;
C++算法代码:
cpp
class Solution {
public:
int ret = 0;
int sum = 0;
int subsetXORSum(vector<int>& nums)
{
dfs(nums, 0);
return ret;
}
void dfs(vector<int>& nums, int index)
{
ret += sum;
for(int i = index; i < nums.size(); i++)
{
sum ^= nums[i];
dfs(nums, i + 1);
//回溯->恢复现场(nums[i]^nums[i]=0)
sum ^= nums[i];
}
}
};
算法总结及流程解析:

15.全排列Ⅱ
题目链接:
题目描述:

题目示例:

解法:
算法思路:
因为题目不要求返回的排列顺序,因此我们可以对初始状态排序,将所有相同的元素放在各自相邻的位置,方便之后操作。因为重复元素的存在,我们在选择元素进行全排列时,可能会存在重复排列,例如:[1,2,1],所有的下标排列为:
012
021
102
120
201
210
按照以上对应的下标进行排列的结果为:
121
112
211
211
112
121
可以看到,有效排列只有三种[1,1,2],[1,2,1],[2,1,1],其中每个排列都出现两次。因此,我们需要对相同元素定义一种规则,使得其组成的排列不会形成重复的情况:
1.我们可以将相同的元素按照排序后的下标顺序出现在排列中,通俗来讲,若元素s出现x次,则排序后的第2个元素s一定出现在第1个元素s后面,排序后的第3个元素s一定出现在第2个元素s后面,以此类推,此时的全排列一定不会出现重复结果。
2.例如:a1=1,a2=1,a3=2,排列结果为[1,1,2]的情况只有一次,即a1在a2 前面,因为 a2 不会出现在a1前面从而避免了重复排列。
3.我们在每一个位置上考虑所有的可能情况并且不出现重复;
4.*注意*:若当前元素的前一个相同元素未出现在当前状态中,则当前元素也不能直接放入当前状态的数组,此做法可以保证相同元素的排列顺序与排序后的相同元素的顺序相同,即避免了重复排列出现。
5.通过深度优先搜索的方式,不断地枚举每个数在当前位置的可能性,并在递归结束时回溯到上一个状态,直到枚举完所有可能性,得到正确的结果。
递归函数设计:
void backtrack(vector<int>& nums, int idx)
参数:idx(当前需要填入的位置);
返回值:无;
函数作用:查找所有合理的排列并存储在答案列表中。
递归流程如下:
1.定义一个二维数组ans用来存放所有可能的排列,一个一维数组 perm 用来存放每个状态的排列,一个一维数组visited 标记元素,然后从第一个位置开始进行递归;
2.在每个递归的状态中,我们维护一个步数idx,表示当前已经处理了几个数字;
3.递归结束条件:当idx等于nums数组的长度时,说明我们已经处理完了所有数字,将当前数组存入结果中;
4.在每个递归状态中,枚举所有下标i,若这个下标未被标记,并且在它之前的相同元素被标记过,则使用 nums数组中当前下标的元素:
a.将visited[i]标记为 1;
b.将 nums[i]添加至 perm 数组末尾;
c.对第step+1个位置进行递归;
d.将visited[i]重新赋值为 0,并删除perm末尾元素表示回溯;
5.最后,返回ans。
C++算法代码:
cpp
class Solution {
public:
vector<vector<int>> ret;
bool check[9];
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(i >= 1 && check[i - 1] == false && nums[i -1] == nums[i])
{
continue;
}
if(check[i] == false)
{
path.push_back(nums[i]);
check[i] = true;
dfs(nums);
//回溯->恢复现场
path.pop_back();
check[i] = false;
}
}
}
};
算法总结及流程解析:

16.电话号码的字母组合
题目链接:
题目描述:

题目示例:

解法:
算法思路:
每个位置可选择的字符与其他位置并不冲突,因此不需要标记已经出现的字符,只需要将每个数字对应的字符依次填入字符串中进行递归,在回溯是撤销填入操作即可。
在递归之前我们需要定义一个字典 phoneMap,记录2~9各自对应的字符。
递归函数设计:
void backtrack(unordered_map<char, string>& phoneMap, string& digits, int index)
参数:index(已经处理的元素个数),ans (字符串当前状态),res (所有成立的字符串);
返回值:无
函数作用:查找所有合理的字母组合并存储在答案列表中。
递归函数流程如下:
1.递归结束条件:当index等于digits 的长度时,将 ans 加入到 res 中并返回;
2.取出当前处理的数字digit,根据 phoneMap取出对应的字母列表letters;
3.遍历字母列表letters,将当前字母加入到组合字符串ans的末尾,然后递归处理下一个数字(传入index+ 1,表示处理下一个数字);
4.递归处理结束后,将加入的字母从ans的末尾删除,表示回溯。
5.最终返回 res即可。
C++算法代码:
cpp
class Solution {
public:
vector<string> ret;
string path;
unordered_map<char, string> hash;
vector<string> letterCombinations(string digits)
{
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, digits[0], 0);
dfs(digits, 0);
return ret;
}
// void dfs(string digits, char num, int index)
// {
// if(path.size() == digits.size())
// {
// ret.push_back(path);
// return;
// }
// for(int i = 0; i < hash[num].size(); i++)
// {
// path.push_back(hash[num][i]);
// dfs(digits, digits[index + 1], index + 1);
// //回溯->恢复现场
// path.pop_back();
// }
// }
//优化参数
void dfs(string digits, int index)
{
if(path.size() == digits.size())
{
ret.push_back(path);
return;
}
for(int i = 0; i < hash[digits[index]].size(); i++)
{
path.push_back(hash[digits[index]][i]);
dfs(digits, index + 1);
//回溯->恢复现场
path.pop_back();
}
}
};
算法总结及流程解析:

17.括号生成
题目链接:
题目描述:

题目示例:

解法:
算法思路:
从左往右进行递归,在每个位置判断放置左右括号的可能性,若此时放置左括号合理,则放置左括号继续进行递归,右括号同理。
一种判断括号是否合法的方法:从左往右遍历,左括号的数量始终大于等于右括号的数量,并且左括号的总数量与右括号的总数量相等。因此我们在递归时需要进行以下判断:
1.放入左括号时需判断此时左括号数量是否小于字符串总长度的一半(若左括号的数量大于等于字符串长度的一半时继续放置左括号,则左括号的总数量一定大于右括号的总数量);
2.放入右括号时需判断此时右括号数量是否小于左括号数量。
递归函数设计:
void dfs(int step, int left)
参数:step(当前需要填入的位置),left(当前状态的字符串中的左括号数量);
返回值:无;
函数作用:查找所有合理的括号序列并存储在答案列表中。
递归函数参数设置为当前状态的字符串长度以及当前状态的左括号数量,递归流程如下:
1.递归结束条件:当前状态字符串长度与2*n相等,记录当前状态并返回;
2.若此时左括号数量小于字符串总长度的一半,则在当前状态的字符串末尾添加左括号并继续递归,递归结束撤销添加操作;
3.若此时右括号数量小于左括号数量(右括号数量可以由当前状态的字符串长度减去左括号数量求得),则在当前状态的字符串末尾添加右括号并递归,递归结束撤销添加操作;
C++算法代码:
cpp
class Solution {
public:
int left = 0;
int right = 0;
vector<string> ret;
string path;
vector<string> generateParenthesis(int n)
{
dfs(n);
return ret;
}
void dfs(int n)
{
//有效括号的组合条件:
//(1)左括号数量==右括号数量;
//(2)以开头为起始位置的任意子串->左括号数量>=右括号数量
if(path.size() == 2 * n)
{
if(left == n && right == n)
{
ret.push_back(path);
}
return;
}
path.push_back('(');
left++;
//左括号剪枝
if(left <= n)
{
dfs(n);
}
//回溯->恢复现场
path.pop_back();
left--;
path.push_back(')');
right++;
//右括号剪枝
if(left >= right)
{
dfs(n);
}
//回溯->恢复现场
path.pop_back();
right--;
}
};
算法总结及流程解析:

结束语
到此,14.找出所有子集的异或总和再求和,15.全排列Ⅱ,16.电话号码的字母组合,17.括号生成 这四道算法题就讲解完了。**子集异或总和:通过递归枚举元素选/不选两种状态,计算所有子集异或和。全排列II:在排序基础上通过剪枝避免重复排列,使用标记数组记录已选元素。电话号码字母组合: 递归处理数字映射,构建所有可能的字母组合。括号生成:通过左右括号数量约束确保合法性,递归构建有效括号组合。**希望大家能有所收获!