算法沉淀——穷举、暴搜、深搜、回溯、剪枝综合练习一
- 01.全排列
- 02.子集
- 03.找出所有子集的异或总和再求和
- 04.全排列 II
- 05.电话号码的字母组合
01.全排列
题目链接:https://leetcode.cn/problems/permutations/
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums
中的所有整数 互不相同
思路
这是一个典型的回溯问题,需要在每个位置上考虑所有可能情况,并确保不出现重复。通过深度优先搜索的方式,不断枚举每个数在当前位置的可能性,然后回溯到上一个状态,直到枚举完所有可能性得到正确的结果。
在这个问题中,可以通过一个递归函数 backtrack
和一个标记数组 visited
来实现全排列。递归函数的核心思想是在当前位置尝试放置每个数字,然后递归到下一层。同时,使用 visited
数组来判断某个数字是否已经在之前的位置出现过,以确保全排列的唯一性。
代码
class Solution {
vector<vector<int>> ret;
vector<int> path;
bool cheak[7];
public:
vector<vector<int>> permute(vector<int>& nums) {
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(!cheak[i]){
path.push_back(nums[i]);
cheak[i]=true;
dfs(nums);
path.pop_back();
cheak[i]=false;
}
}
}
};
02.子集
题目链接:https://leetcode.cn/problems/subsets/
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums
中的所有元素 互不相同
思路
这里我们我们可以有下面两种写法,我们结合代码解释
代码一
class Solution {
vector<vector<int>> ret;
vector<int> path;
public:
vector<vector<int>> subsets(vector<int>& nums) {
dfs(nums, 0);
return ret;
}
void dfs(vector<int>& nums, int pos) {
ret.push_back(path);
for (int i = pos; i < nums.size(); ++i) {
path.push_back(nums[i]);
dfs(nums, i + 1);
path.pop_back();
}
}
};
这段代码使用深度优先搜索(DFS)的思想。主要过程如下:
- 在
dfs
函数的一开始,将当前子集path
加入结果集ret
。 - 然后使用循环,从当前位置
pos
开始遍历数组nums
。 - 对于每个元素,将其加入当前子集
path
,然后递归调用dfs
函数,继续生成包含当前元素的子集。 - 递归完成后,回溯,将刚刚加入的元素从当前子集
path
移出,继续循环遍历下一个元素。
这样,通过深度优先搜索,代码逐步生成了包含不同数量元素的所有子集。
代码二
class Solution {
vector<vector<int>> ret;
vector<int> path;
public:
vector<vector<int>> subsets(vector<int>& nums) {
dfs(nums, 0);
return ret;
}
void dfs(vector<int>& nums, int pos) {
if (pos == nums.size()) {
ret.push_back(path);
return;
}
path.push_back(nums[pos]);
dfs(nums, pos + 1);
path.pop_back();
dfs(nums, pos + 1);
}
};
这段代码同样使用深度优先搜索的思想。主要过程如下:
- 在
dfs
函数的一开始,检查当前位置pos
是否等于数组nums
的大小。如果相等,表示已经处理完所有元素,将当前子集path
加入结果集ret
,然后返回结束当前 DFS 分支。 - 如果当前位置
pos
不等于数组大小,将当前元素加入子集path
,然后递归调用dfs
函数,继续生成包含当前元素的子集。 - 递归完成后,回溯,将刚刚加入的元素从当前子集
path
移出。 - 继续递归调用
dfs
函数,生成不包含当前元素的子集。
这样,同样通过深度优先搜索,代码逐步生成了包含不同数量元素的所有子集。这种实现方式在递归的不同阶段进行了两次递归调用,一次是包含当前元素的情况,一次是不包含当前元素的情况。
03.找出所有子集的异或总和再求和
题目链接:https://leetcode.cn/problems/sum-of-all-subset-xor-totals/
一个数组的 异或总和 定义为数组中所有元素按位 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 。
提示:
1 <= nums.length <= 12
1 <= nums[i] <= 20
思路
这一题实际上还是和上一题类似的子集问题,只不过我们将存下的数组变为了一个值,进数组变为子集累计异或的值,出临时数组变为异或相同值,也就是删除异或过的数。
代码
class Solution {
int sum=0,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];
}
}
};
int sum = 0, path = 0;
: 这两个类成员变量用于追踪异或和的总和 (sum
) 和当前正在生成的子集异或和 (path
)。int subsetXORSum(vector<int>& nums)
: 这是主函数,用于调用 DFS 函数,并返回最终的异或和结果。void dfs(vector<int>& nums, int pos)
: 这是深度优先搜索(DFS)函数,用于递归生成所有子集的异或和。sum += path;
: 在每一层递归中,将当前子集的异或和加到总和中。for(int i = pos; i < nums.size(); ++i) {
: 循环从当前位置pos
开始遍历数组nums
。path ^= nums[i];
: 将当前元素nums[i]
异或到当前子集异或和中。dfs(nums, i + 1);
: 递归调用 DFS 函数,以i + 1
为新的起点,生成包含当前元素的子集异或和。path ^= nums[i];
: 回溯,将刚刚异或的元素从当前子集异或和中移出,继续循环遍历下一个元素。
- 最终,
sum
存储了所有子集的异或和总和,该值作为结果返回。
04.全排列 II
题目链接:https://leetcode.cn/problems/permutations-ii/
定一个可包含重复数字的序列 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
思路
这里要考虑全排列,在同一节点的所有组合中,相同的元素只能选择一次,所以这里我们要有一个标记元素是否使用的数组,在后面的主逻辑递归中,有两种写法,第一种是只关心“不合法”分支,第二种是只关心“合法”分支。
代码一
class Solution {
vector<vector<int>> ret;
vector<int> path;
bool cheak[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(cheak[i]||(i!=0&&nums[i]==nums[i-1]&&cheak[i-1]==false))
continue;
path.push_back(nums[i]);
cheak[i]=true;
dfs(nums,pos+1);
path.pop_back();
cheak[i]=false;
}
}
};
-
vector<vector<int>> ret;
: 用于存储所有的全排列结果。 -
vector<int> path;
: 用于存储当前正在生成的排列。 -
bool cheak[9];
: 一个布尔数组,用于标记在当前生成排列的过程中哪些元素已经被选择。 -
vector<vector<int>> permuteUnique(vector<int>& nums)
: 主函数,用于调用 DFS 函数,并返回最终的全排列结果。 -
sort(nums.begin(), nums.end());
: 在开始生成排列之前,先将输入数组排序,以便处理重复元素。 -
void dfs(vector<int>& nums, int pos)
: 深度优先搜索函数,递归生成全排列。if(pos == nums.size())
: 如果当前位置pos
等于数组的大小,表示已经处理完所有元素,将当前排列加入结果集。ret.push_back(path);
: 将当前排列加入结果集。return;
: 返回结束当前 DFS 分支。
for(int i = 0; i < nums.size(); ++i)
: 遍历数组中的每个元素。if(cheak[i] || (i != 0 && nums[i] == nums[i-1] && cheak[i-1] == false))
: 如果当前元素已经被选择(cheak[i]
为 true)或者当前元素与前一个元素相同,且前一个元素未被选择,则跳过当前循环。这是为了处理重复元素的情况,确保不会生成重复的排列。continue;
: 跳过当前循环,进入下一个循环。
path.push_back(nums[i]);
: 将当前元素加入排列。cheak[i] = true;
: 将当前元素标记为已选择。dfs(nums, pos + 1);
: 递归调用 DFS 函数,继续生成下一个元素的排列。path.pop_back();
: 回溯,将刚刚加入的元素移出排列。cheak[i] = false;
: 回溯,将刚刚标记为已选择的元素重新标记为未选择。
代码二
class Solution { vector<vector<int>> ret; vector<int> path; bool cheak[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(!cheak[i]&&(i==0||nums[i]!=nums[i-1]||cheak[i-1]==true)) { path.push_back(nums[i]); cheak[i]=true; dfs(nums,pos+1); path.pop_back(); cheak[i]=false; } } } };
vector<vector<int>> ret;
: 用于存储所有的全排列结果。vector<int> path;
: 用于存储当前正在生成的排列。bool cheak[9];
: 一个布尔数组,用于标记在当前生成排列的过程中哪些元素已经被选择。vector<vector<int>> permuteUnique(vector<int>& nums)
: 主函数,用于调用 DFS 函数,并返回最终的全排列结果。sort(nums.begin(), nums.end());
: 在开始生成排列之前,先将输入数组排序,以便处理重复元素。void dfs(vector<int>& nums, int pos)
: 深度优先搜索函数,递归生成全排列。if(pos == nums.size())
: 如果当前位置pos
等于数组的大小,表示已经处理完所有元素,将当前排列加入结果集。ret.push_back(path);
: 将当前排列加入结果集。return;
: 返回结束当前 DFS 分支。
for(int i = 0; i < nums.size(); ++i)
: 遍历数组中的每个元素。if(!cheak[i] && (i == 0 || nums[i] != nums[i-1] || cheak[i-1] == true))
: 如果当前元素未被选择且(是第一个元素或者与前一个元素不相同或者前一个元素已被选择),则进入循环。path.push_back(nums[i]);
: 将当前元素加入排列。cheak[i] = true;
: 将当前元素标记为已选择。dfs(nums, pos + 1);
: 递归调用 DFS 函数,继续生成下一个元素的排列。path.pop_back();
: 回溯,将刚刚加入的元素移出排列。cheak[i] = false;
: 回溯,将刚刚标记为已选择的元素重新标记为未选择。
05.电话号码的字母组合
题目链接:https://leetcode.cn/problems/letter-combinations-of-a-phone-number/
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
提示:
0 <= digits.length <= 4
digits[i]
是范围['2', '9']
的一个数字。
思路
这里比前面的排列组合更为简单,只不过在这里我们需要额外创建一个哈希结构来建立数字和字母的映射关系。
代码
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(!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+=ch;
dfs(digits,pos+1);
path.pop_back();
}
}
};
string hash[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs","tuv", "wxyz"};
: 数字与对应的字母组合的映射关系,hash[2]
对应 “abc”,hash[3]
对应 “def”,以此类推。string path;
: 用于存储当前正在生成的字母组合。vector<string> ret;
: 用于存储所有的字母组合结果。vector<string> letterCombinations(string digits)
: 主函数,用于调用 DFS 函数,并返回最终的字母组合结果。if(!digits.size()) return ret;
: 如果输入的数字序列为空,则直接返回空的结果集。void dfs(string& digits, int pos)
: 深度优先搜索函数,递归生成数字序列对应的所有字母组合。if(pos == digits.size())
: 如果当前位置pos
等于数字序列的大小,表示已经处理完所有数字,将当前字母组合加入结果集。ret.push_back(path);
: 将当前字母组合加入结果集。return;
: 返回结束当前 DFS 分支。
for(auto ch : hash[digits[pos] - '0'])
: 遍历当前数字对应的所有字母。path += ch;
: 将当前字母加入字母组合。dfs(digits, pos + 1);
: 递归调用 DFS 函数,继续生成下一个数字对应的字母组合。
)`: 主函数,用于调用 DFS 函数,并返回最终的字母组合结果。
if(!digits.size()) return ret;
: 如果输入的数字序列为空,则直接返回空的结果集。void dfs(string& digits, int pos)
: 深度优先搜索函数,递归生成数字序列对应的所有字母组合。if(pos == digits.size())
: 如果当前位置pos
等于数字序列的大小,表示已经处理完所有数字,将当前字母组合加入结果集。ret.push_back(path);
: 将当前字母组合加入结果集。return;
: 返回结束当前 DFS 分支。
for(auto ch : hash[digits[pos] - '0'])
: 遍历当前数字对应的所有字母。path += ch;
: 将当前字母加入字母组合。dfs(digits, pos + 1);
: 递归调用 DFS 函数,继续生成下一个数字对应的字母组合。path.pop_back();
: 回溯,将刚刚加入的字母移出字母组合。