目录
一、找出所有子集的异或总和再求和
1.题目链接:1863. 找出所有子集的异或总和再求和
2.题目描述:
一个数组的 异或总和 定义为数组中所有元素按位
XOR
的结果;如果数组为 空 ,则异或总和为0
。
- 例如,数组
[2,5,6]
的 异或总和 为2 XOR 5 XOR 6 = 1
。给你一个数组
nums
,请你求出nums
中每个 子集 的 异或总和 ,计算并返回这些值相加之 和 。注意:在本题中,元素 相同 的不同子集应 多次 计数。
数组
a
是数组b
的一个 子集 的前提条件是:从b
删除几个(也可能不删除)元素能够得到a
。示例 1:
输入:nums = [1,3] 输出:6 解释:[1,3] 共有 4 个子集: - 空子集的异或总和是 0 。 - [1] 的异或总和为 1 。 - [3] 的异或总和为 3 。 - [1,3] 的异或总和为 1 XOR 3 = 2 。 0 + 1 + 3 + 2 = 6示例 2:
输入:nums = [5,1,6] 输出:28 解释:[5,1,6] 共有 8 个子集: - 空子集的异或总和是 0 。 - [5] 的异或总和为 5 。 - [1] 的异或总和为 1 。 - [6] 的异或总和为 6 。 - [5,1] 的异或总和为 5 XOR 1 = 4 。 - [5,6] 的异或总和为 5 XOR 6 = 3 。 - [1,6] 的异或总和为 1 XOR 6 = 7 。 - [5,1,6] 的异或总和为 5 XOR 1 XOR 6 = 2 。 0 + 5 + 1 + 6 + 4 + 3 + 7 + 2 = 28示例 3:
输入:nums = [3,4,5,6,7,8] 输出:480 解释:每个子集的全部异或总和值之和为 480 。
3.解法(递归)
🌵算法思路:
所有子集可以解释为:每个元素选择在或不在一个集合中(因此,子集有 2^n 个)。本题我们需要求出所有子集,将它们的异或和相加。因为异或操作满足交换律,所以我们可以定义一个变量,直接记录当前状态的异或和。使用递归保存当前集合的状态(异或和),选择将当前元素添加至当前状态与否,并依次递归数组中下一个元素。当递归到空元素时,表示所有元素都被考虑到,记录当前状态(将当前状态的异或和添加至答案中)。
递归函数设计:void dfs(int val, int idx, vector<int>& nums)
- 参数:val(当前状态的异或和),idx(当前需要处理的元素下标,处理过程:选择将其添加至当前状态或不进行操作);
- 返回值:无;
- 函数作用:选择对元素进行添加与否处理。
递归流程:
1. 递归结束条件:当前下标与数组长度相等,即已经越界,表示已经考虑到所有元素;
- 将当前异或和添加至答案中,并返回;
2. 考虑将当前元素添加至当前状态,当前状态更新为与当前元素值的异或和,然后递归下一个元素;
3. 考虑不选择当前元素,当前状态不变,直接递归下一个元素。
🌵算法代码:
class Solution
{
int path;
int sum;
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];
}
}
};
二、全排列 II
1.题目链接:47. 全排列 II
2.题目描述:
给定一个可包含重复数字的序列
nums
,按任意顺序 返回所有不重复的全排列。示例 1:
输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]示例 2:
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10
3.解法
🌴算法思路:
因为题目不要求返回的排列顺序,因此我们可以对初始状态排序,将所有相同的元素放在各自相邻的位置,方便之后操作。因为重复元素的存在,我们在选择元素进行全排列时,可能会存在重复排列,例如:[1, 2, 1],所有的 下标排列 为:
123
132
213
231
312
321
按照以上下标进行排列的结果为:
121
112
211
211
112
121
可以看到,有效排列只有三种[1, 1, 2],[1, 2, 1],[2, 1, 1],其中每个排列都出现两次。因此,我们需要对相同元素定义⼀种规则,使得其组成的排列不会形成重复的情况:
- 我们可以将相同的元素按照排序后的下标顺序出现在排列中,通俗来讲,若元素 s 出现 x 次,则排序后的第 2 个元素 s ⼀定出现在第 1 个元素 s 后面,排序后的第 3 个元素 s 一定出现在第 2 个元素 s 后面,以此类推,此时的全排列⼀定不会出现重复结果。
- 例如:a1=1,a2=1,a3=2,排列结果为 [1, 1, 2] 的情况只有⼀次,即 a1 在 a2 前面,因为 a2 不会出现在 a1 前面从而避免了重复排列。
- 我们在每⼀个位置上考虑所有的可能情况并且不出现重复;
- *注意*:若当前元素的前⼀个相同元素未出现在当前状态中,则当前元素也不能直接放入当前状态的数组,此做法可以保证相同元素的排列顺序与排序后的相同元素的顺序相同,即避免了重复排列出现。
- 通过深度优先搜索的方式,不断地枚举每个数在当前位置的可能性,并在递归结束时回溯到上⼀个状态,直到枚举完所有可能性,得到正确的结果。
递归函数设计:void backtrack(vector<int>& nums, int idx)
- 参数:idx(当前需要填入的位置);
- 返回值:无;
- 函数作用:查找所有合理的排列并存储在答案列表中。
递归流程如下:
1. 定义⼀个二维数组 ans 用来存放所有可能的排列,⼀个⼀维数组 perm ⽤来存放每个状态的排列,⼀个⼀维数组 visited 标记元素,然后从第⼀个位置开始进行递归;
2. 在每个递归的状态中,我们维护⼀个步数 idx,表示当前已经处理了几个数字;
3. 递归结束条件:当 idx 等于 nums 数组的长度时,说明我们已经处理完了所有数字,将当前数组存入结果中;
4. 在每个递归状态中,枚举所有下标 i,若这个下标未被标记,并且在它之前的相同元素被标记过,则使用 nums 数组中当前下标的元素:
- 将 visited[i] 标记为 1;
- 将 nums[i] 添加至 perm 数组末尾;
- 对第 step+1 个位置进行递归;
- 将 visited[i] 重新赋值为 0,并删除 perm 末尾元素表示回溯;
5. 最后,返回 ans。
🌴算法代码:
class Solution
{
vector<int> path;
vector<vector<int>> ret;
bool check[9];
public:
vector<vector<int>> permuteUnique(vector<int>& nums)
{
sort(nums.begin(), nums.end());
dfs(nums, 0);
return ret;
}
void dfs(vector<int>& nums, int pos)
{
if (pos == 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] != false))
{
path.push_back(nums[i]);
check[i] = true;
dfs(nums, pos + 1);
path.pop_back(); // 恢复现场
check[i] = false;
}
}
}
};
三、电话号码的字母组合
1.题目链接:17. 电话号码的字母组合
2.题目描述:
给定一个仅包含数字
2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]示例 2:
输入:digits = "" 输出:[]示例 3:
输入:digits = "2" 输出:["a","b","c"]
3.解法
🍓算法思路:
每个位置可选择的字符与其他位置并不冲突,因此不需要标记已经出现的字符,只需要将每个数字对应的字符依次填入字符串中进行递归,在回溯时撤销填入操作即可。
- 在递归之前我们需要定义一个字典 phoneMap,记录 2~9 各自对应的字符。
递归函数设计:void backtrack(unordered_map<char, string>& phoneMap, string& digits, int index)
- 参数:index (已经处理的元素个数),ans (字符串当前状态),res (所有成立的字符串);
- 返回值:无。
- 函数作用:查找所有合理的字母组合并存储在答案列表中。
递归函数流程如下:
- 递归结束条件:当 index 等于 digits 的长度时,将 ans 加入到 res 中并返回;
- 取出当前处理的数字 digit,根据 phoneMap 取出对应的字母列表 letters;
- 遍历字母列表 letters,将当前字母加⼊到组合字符串 ans 的末尾,然后递归处理下一个数字(传入 index + 1,表示处理下一个数字);
- 递归处理结束后,将加入的字母从 ans 的末尾删除,表示回溯。
- 最终返回 res 即可。
🍓算法代码:
class Solution
{
string hash[10] = {" ", " ", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
string path;
vector<string> ret;
public:
vector<string> letterCombinations(string digits)
{
if (0 == digits.size()) return ret;
dfs(digits, 0);
return ret;
}
void dfs(string& digits, int pos)
{
if(pos == digits.size())
{
ret.push_back(path);
return;
}
for(auto ch : hash[digits[pos] - '0'])
{
path.push_back(ch);
dfs(digits, pos + 1);
path.pop_back();// 恢复现场
}
}
};
四、括号生成
1.题目链接:22. 括号生成
2.题目描述:
数字
n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。示例 1:
输入:n = 3 输出:["((()))","(()())","(())()","()(())","()()()"]示例 2:
输入:n = 1 输出:["()"]
3.解法
🍍算法思路:
从左往右进行递归,在每个位置判断放置左右括号的可能性,若此时放置左括号合理,则放置左括号继续进行递归,右括号同理。
一种判断括号是否合法的方法:从左往右遍历,左括号的数量始终大于等于右括号的数量,并且左括号的总数量与右括号的总数量相等。因此我们在递归时需要进行以下判断:
- 放入左括号时需判断此时左括号数量是否小于字符串总长度的一半(若左括号的数量大于等于字符串长度的一半时继续放置左括号,则左括号的总数量一定大于右括号的总数量);
- 放入右括号时需判断此时右括号数量是否小于左括号数量。
递归函数设计:void dfs(int step, int left)
- 参数:step(当前需要填入的位置),left(当前状态的字符串中的左括号数量);
- 返回值:无;
- 函数作用:查找所有合理的括号序列并存储在答案列表中。
递归函数参数设置为当前状态的字符串长度以及当前状态的左括号数量,递归流程如下:
- 递归结束条件:当前状态字符串长度与 2*n 相等,记录当前状态并返回;
- 若此时左括号数量小于字符串总长度的一半,则在当前状态的字符串末尾添加左括号并继续递归,递归结束撤销添加操作;
- 若此时右括号数量小于左括号数量(右括号数量可以由当前状态的字符串长度减去左括号数量求得),则在当前状态的字符串末尾添加右括号并递归,递归结束撤销添加操作;
🍍算法代码:
class Solution
{
int left, right, n;
string path;
vector<string> ret;
public:
vector<string> generateParenthesis(int _n)
{
n = _n;
dfs();
return ret;
}
void dfs()
{
if (right == n)
{
ret.push_back(path);
return;
}
// 添加左括号
if (left < n)
{
path.push_back('('); left++;
dfs();
path.pop_back(); left--;// 恢复现场
}
// 添加右括号
if (right < left)
{
path.push_back(')'); right++;
dfs();
path.pop_back(); right--;// 恢复现场
}
}
};