1 找出所有子集的异或总和再求和
1.1 题目链接
1.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 。
提示:
1 <= nums.length <= 121 <= nums[i] <= 20
1.3 解法(递归):
算法思路:
所有⼦集可以解释为:每个元素选择在或不在⼀个集合中(因此,⼦集有 2 n 个)。本题我们需要求出所有⼦集,将它们的异或和相加。因为异或操作满⾜交换律,所以我们可以定义⼀个变量,直接记录当前状态的异或和。使⽤递归保存当前集合的状态(异或和),选择将当前元素添加⾄当前状态与否,并依次递归数组中下⼀个元素。当递归到空元素时,表⽰所有元素都被考虑到,记录当前状态(将当前状态的异或和添加⾄答案中)。
例如集合中的元素为 [1, 2],则它的⼦集状态选择过程如下:
[]
/ \
[] [1] //第⼀个元素选择与否
/ \ / \
[] [1] [2] [1, 2] //第⼆个元素选择与否,每个状态到这⼀层时需要记录异或和
递归函数设计:
void dfs(int val, int idx, vector<int>& nums)
参数:val(当前状态的异或和),idx(当前需要处理的元素下标,处理过程:选择将其添加⾄当前状态或不进⾏操作);
返回值:⽆;
函数作⽤:选择对元素进⾏添加与否处理。
递归流程:
- 递归结束条件:当前下标与数组⻓度相等,即已经越界,表⽰已经考虑到所有元素;
- a. 将当前异或和添加⾄答案中,并返回;
- 考虑将当前元素添加⾄当前状态,当前状态更新为与当前元素值的异或和,然后递归下⼀个元素;
- 考虑不选择当前元素,当前状态不变,直接递归下⼀个元素;
1.4 C++算法代码:
class Solution {
int sum;
int path;
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
2.1 题目链接
2.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
2.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(当前需要填⼊的位置);
返回值:⽆;
函数作⽤:查找所有合理的排列并存储在答案列表中。
递归流程:
- 定义⼀个⼆维数组 ans ⽤来存放所有可能的排列,⼀个⼀维数组 perm ⽤来存放每个状态的排列,⼀个⼀维数组 visited 标记元素,然后从第⼀个位置开始进⾏递归;
- 在每个递归的状态中,我们维护⼀个步数 idx,表⽰当前已经处理了⼏个数字;
- 递归结束条件:当 idx 等于 nums 数组的⻓度时,说明我们已经处理完了所有数字,将当前数组存⼊结果中;
- 在每个递归状态中,枚举所有下标 i,若这个下标未被标记,并且在它之前的相同元素被标记过,则使⽤ nums 数组中当前下标的元素:
- a. 将 visited[i] 标记为 1;
- b. 将 nums[i] 添加⾄ perm 数组末尾;
- c. 对第 step+1 个位置进⾏递归;
- d. 将 visited[i] 重新赋值为 0,并删除 perm 末尾元素表⽰回溯;
- 最后,返回 ans。
2.4 C++算法代码:
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;
}
}
}
};
3 电话号码的字母组合
3.1 题目链接
3.2 题目描述
给定一个仅包含数字 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 <= 4digits[i]是范围['2', '9']的一个数字。
3.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 即可。
3.4 C++算法代码:
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() == 0) 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();
}
}
};
4 括号生成
4.1 题目链接
4.2 题目描述
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入: n = 3
输出: ["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入: n = 1
输出: ["()"]
提示:
1 <= n <= 8
4.3 解法
算法思路:
从左往右进⾏递归,在每个位置判断放置左右括号的可能性,若此时放置左括号合理,则放置左括号继续进⾏递归,右括号同理。
⼀种判断括号是否合法的⽅法:从左往右遍历,左括号的数量始终⼤于等于右括号的数量,并且左括号的总数量与右括号的总数量相等。因此我们在递归时需要进行以下判断:
- 放⼊左括号时需判断此时左括号数量是否⼩于字符串总⻓度的⼀半(若左括号的数量⼤于等于字符串⻓度的⼀半时继续放置左括号,则左括号的总数量⼀定⼤于右括号的总数量);
- 放⼊右括号时需判断此时右括号数量是否⼩于左括号数量。
递归函数设计:
void dfs(int step, int left)
参数:step(当前需要填⼊的位置),left(当前状态的字符串中的左括号数量);
返回值:⽆;
函数作⽤:查找所有合理的括号序列并存储在答案列表中。递归函数参数设置为当前状态的字符串⻓度以及当前状态的左括号数量.
递归流程:
- 递归结束条件:当前状态字符串⻓度与 2*n 相等,记录当前状态并返回;
- 若此时左括号数量⼩于字符串总⻓度的⼀半,则在当前状态的字符串末尾添加左括号并继续递归,递归结束撤销添加操作;
- 若此时右括号数量⼩于左括号数量(右括号数量可以由当前状态的字符串⻓度减去左括号数量求得),则在当前状态的字符串末尾添加右括号并递归,递归结束撤销添加操作;
4.4 C++算法代码:
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--;
}
}
};
5 组合
5.1 题目链接
5.2 题目描述
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入: n = 1, k = 1
输出: [[1]]
提示:
1 <= n <= 201 <= k <= n
5.3 解法(回溯):
算法思路:
题⽬要求我们从 1 到 n 中选择 k 个数的所有组合,其中不考虑顺序。也就是说,[1,2] 和 [2,1] 等价。我们需要找出所有的组合,但不能重复计算相同元素的不同顺序的组合。对于选择组合,我们需要进⾏
如下流程:
- 所有元素分别作为⾸位元素进⾏处理;
- 在之后的位置上同理,选择所有元素分别作为当前位置元素进⾏处理;
- 为避免计算重复组合,规定选择之后位置的元素时必须⽐前⼀个元素⼤,这样就不会有重复的组合([1,2] 和 [2,1] 中 [2,1] 不会出现)。
递归函数设计:
void dfs(vector<vector<int>>& ans, vector<int>& v, int step, int &n, int &k)
参数:step(当前需要进⾏处理的位置);
返回值:⽆;
函数作⽤:某个元素作为⾸位元素出现时,查找所有可能的组合。
递归流程:
- 定义⼀个⼆维数组和⼀维数组。⼆维数组⽤来记录所有组合,⼀维数组⽤来记录当前状态下的组合。
- 遍历 1 到 n-k+1,以当前数作为组合的⾸位元素进⾏递归(从 n-k+1 到 n 作为⾸位元素时,组合中⼀定不会存在 k 个元素)。
- 递归函数的参数为两个数组、当前步骤以及 n 和 k。递归流程如下:
- a. 结束条件:当前组合中已经有 k 个元素,将当前组合存进⼆维数组并返回。
- ▪ 剪枝:如果当前位置之后的所有元素放⼊组合也不能满⾜组合中存在 k 个元素,直接返回。
- b. 从当前位置的下⼀个元素开始遍历到 n,将元素赋值到当前位置,递归下⼀个位置。
- a. 结束条件:当前组合中已经有 k 个元素,将当前组合存进⼆维数组并返回。
5.4 C++算法代码:
class Solution {
vector<int> path;
vector<vector<int>> ret;
int n, k;
public:
vector<vector<int>> combine(int _n, int _k) {
n = _n; k = _k;
dfs(1);
return ret;
}
void dfs(int start)
{
if(path.size() == k)
{
ret.push_back(path);
return ;
}
for(int i = start; i <= n; i++)
{
path.push_back(i);
dfs(i + 1);
path.pop_back();
}
}
};
6 目标和
6.1 题目链接
6.2 题目描述
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1],可以在2之前添加'+',在1之前添加'-',然后串联起来得到表达式"+2-1"。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入: nums = [1,1,1,1,1], target = 3
输出: 5
解释: 一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入: nums = [1], target = 1
输出: 1
提示:
1 <= nums.length <= 200 <= nums[i] <= 10000 <= sum(nums[i]) <= 1000-1000 <= target <= 1000
6.3 解法(回溯):
算法思路:
对于每个数,可以选择加上或减去它,依次枚举每⼀个数字,在每个数都被选择时检查得到的和是否等于⽬标值。如果等于,则记录结果。
需要注意的是,为了优化时间复杂度,可以提前计算出数组中所有数字的和 sum,以及数组的⻓度 len。这样可以快速判断当前的和减去剩余的所有数是否已经超过了⽬标值 target ,或者当前的和加上剩下的数的和是否⼩于⽬标值 target,如果满⾜条件,则可以直接回溯。
递归流程:
- 递归结束条件:index 与数组⻓度相等,判断当前状态的 sum 是否与⽬标值相等,若是计数加⼀;
- 选择当前元素进⾏加操作,递归下⼀个位置,并更新参数 sum;
- 选择当前元素进⾏减操作,递归下⼀个位置,并更新参数 sum;
- 特别地,此问题可以转化为另⼀个问题:若所有元素初始状态均为减,选择其中⼏个元素将他们的状态修改为加,计算修改后的元素和与⽬标值相等的⽅案个数。
- 选择其中 x 个元素进⾏修改,并且这 x 个元素的和为 y;
- 检查使得 -sum+2*y=target(移项:y=(sum+target)/2)成⽴的⽅案个数,即选择 x 个元素和为(sum+target)/2 的⽅案个数;
- a. 若 sum+target 为奇数,则不存在这种⽅案;
- 递归流程:
- a. 传⼊参数:index(当前要处理的元素下标),sum(当前状态和),nums(元素数组),aim(⽬标值:(sum+target)/2);
- b. 递归结束条件:index 与数组⻓度相等,判断当前 sum 是否与⽬标值相等,若是返回 1,否则返回 0;
- c. 返回 递归选择当前元素 以及 递归不选择当前元素 函数值的和。
6.4 C++算法代码:
class Solution {
int ret, aim;
public:
int findTargetSumWays(vector<int>& nums, int target) {
aim = target;
dfs(nums, 0, 0);
return ret;
}
void dfs(vector<int> & nums, int pos, int path)
{
if(pos == nums.size())
{
if(path == aim) ret++;
return ;
}
// 加法
dfs(nums, pos + 1, path + nums[pos]);
// 减法
dfs(nums, pos + 1, path - nums[pos]);
}
};
7 组合总和
7.1 题目链接
7.2 题目描述
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 **不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入: candidates = [2,3,6,7], target = 7
输出: [[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
提示:
1 <= candidates.length <= 302 <= candidates[i] <= 40candidates的所有元素 互不相同1 <= target <= 40
7.3 解法
算法思路:
candidates 的所有元素 互不相同,因此我们在递归状态时只需要对每个元素进⾏如下判断:
- 跳过,对下⼀个元素进⾏判断;
- 将其添加⾄当前状态中,我们在选择添加当前元素时,之后仍可以继续选择当前元素(可以重复选择同⼀元素)。
- 因此,我们在选择当前元素并向下传递下标时,应该直接传递当前元素下标。
递归函数设计:
void dfs(vector<int>& candidates, int target, vector<vector<int>>& ans, vector<int>& combine, int idx)
参数:target(当前状态和与⽬标值的差),idx(当前需要处理的元素下标);
返回值:⽆;
函数作⽤:向下传递两个状态(跳过或者选择当前元素),找出所有组合使得元素和为⽬标值。
递归流程:
- 结束条件:
- a. 当前需要处理的元素下标越界;
- b. 当前状态的元素和已经与⽬标值相同;
- 跳过当前元素,当前状态不变,对下⼀个元素进⾏处理;
- 选择将当前元素添加⾄当前状态,并保留状态继续对当前元素进⾏处理,递归结束时撤销添加操作。
7.4 C++算法代码:
class Solution {
int aim;
vector<int> path;
vector<vector<int>> ret;
public:
vector<vector<int>> combinationSum(vector<int>& nums, int target) {
aim = target;
dfs(nums, 0, 0);
return ret;
}
void dfs(vector<int>& nums, int pos, int sum)
{
if(sum == aim)
{
ret.push_back(path);
return;
}
if(sum > aim || pos == nums.size()) return;
for(int i = pos; i < nums.size(); i++)
{
path.push_back(nums[i]);
dfs(nums, i, sum + nums[i]);
path.pop_back();
}
}
};
class Solution {
int aim;
vector<int> path;
vector<vector<int>> ret;
public:
vector<vector<int>> combinationSum(vector<int>& nums, int target) {
aim = target;
dfs(nums, 0, 0);
return ret;
}
void dfs(vector<int>& nums, int pos, int sum)
{
if(sum == aim)
{
ret.push_back(path);
return;
}
if(sum > aim || pos == nums.size()) return;
// 枚举个数
for(int k = 0; k * nums[pos] + sum <= aim; k++)
{
if(k) path.push_back(nums[pos]);
dfs(nums, pos + 1, sum + k * nums[pos]);
}
// 恢复现场
for(int k = 1; k * nums[pos] + sum <= aim; k++)
path.pop_back();
}
};
8 字母大小写全排列
8.1 题目链接
8.2 题目描述
给定一个字符串 s ,通过将字符串 s 中的每个字母转变大小写,我们可以获得一个新的字符串。
返回 所有可能得到的字符串集合 。以 任意顺序 返回输出。
示例 1:
输入: s = "a1b2"
输出: ["a1b2", "a1B2", "A1b2", "A1B2"]
示例 2:
输入: s = "3z4"
输出: ["3z4","3Z4"]
提示:
1 <= s.length <= 12s由小写英文字母、大写英文字母和数字组成
8.3 解法
算法思路:
只需要对英⽂字⺟进⾏处理,处理每个元素时存在三种情况:
- 不进⾏处理;
- 若当前字⺟是英⽂字⺟并且是⼤写,将其修改为⼩写;
- 若当前字⺟是英⽂字⺟并且是⼩写,将其修改为⼤写。
递归函数设计:
void dfs(int step)
参数:step(当前需要处理的位置);
返回值:⽆;
函数作⽤:查找所有可能的字符串集合,并将其记录在答案列表。
从前往后按序进⾏递归,递归流程:
- 递归结束条件:当前需要处理的元素下标越界,表⽰处理完毕,记录当前状态并返回;
- 对当前元素不进行任何处理,直接递归下⼀位元素;
- 判断当前元素是否为⼩写字⺟,若是,将其修改为⼤写字⺟并递归下⼀个元素,递归结束时撤销修改操作;
- 判断当前元素是否为⼤写字⺟,若是,将其修改为⼩写字⺟并递归下⼀个元素,递归结束时撤销修改操作;
8.4 C++算法代码:
class Solution {
string path;
vector<string> ret;
public:
vector<string> letterCasePermutation(string s) {
dfs(s, 0);
return ret;
}
void dfs(string& s, int pos)
{
if(pos == s.length())
{
ret.push_back(path);
return;
}
char ch = s[pos];
// 不改变
path.push_back(ch);
dfs(s, pos + 1);
path.pop_back();
// 改变
if(ch < '0' || ch > '9')
{
char tmp = change(ch);
path.push_back(tmp);
dfs(s, pos + 1);
path.pop_back();
}
}
char change(char ch)
{
if(ch >= 'a' && ch <= 'z') ch -=32;
else ch += 32;
return ch;
}
};
9 优美的排列
9.1 题目链接
9.2 题目描述
假设有从 1 到 n 的 n 个整数。用这些整数构造一个数组 perm(下标从 1 开始),只要满足下述条件 之一 ,该数组就是一个 优美的排列 :
perm[i]能够被i整除i能够被perm[i]整除
给你一个整数 n ,返回可以构造的 优美排列 的 数量 。
示例 1:
输入: n = 2
输出: 2
解释:
第 1 个优美的排列是 [1,2]:
- perm[1] = 1 能被 i = 1 整除
- perm[2] = 2 能被 i = 2 整除
第 2 个优美的排列是 [2,1]:
- perm[1] = 2 能被 i = 1 整除
- i = 2 能被 perm[2] = 1 整除
示例 2:
输入: n = 1
输出: 1
提示:
1 <= n <= 15
9.3 解法
算法思路:
我们需要在每⼀个位置上考虑所有的可能情况并且不能出现重复。通过深度优先搜索的⽅式,不断地枚举每个数在当前位置的可能性,并回溯到上⼀个状态,直到枚举完所有可能性,得到正确的结果。
我们需要定义⼀个变量 ⽤来记录所有可能的排列数量,⼀个⼀维数组 visited 标记元素,然后从第⼀个位置开始进⾏递归;
递归函数设计:
void backtrack(int index, int &n)
参数:index(当前需要处理的位置);
返回值:⽆;
函数作⽤:在当前位置填⼊⼀个合理的数字,查找所有满⾜条件的排列。
递归流程:
- 递归结束条件:当 index 等于 n 时,说明已经处理完了所有数字,将当前数组存⼊结果中;
- 在每个递归状态中,枚举所有下标 x,若这个下标未被标记,并且满⾜题⽬条件之⼀:
- a. 将 visited[x] 标记为 1;
- b. 对第 index+1 个位置进⾏递归;
- c. 将 visited[x] 重新赋值为 0,表⽰回溯;
9.4 C++算法代码:
class Solution {
bool check[16];
int ret;
public:
int countArrangement(int n) {
dfs(1, n);
return ret;
}
void dfs(int pos, int n)
{
if(pos == n + 1)
{
ret++;
return;
}
for(int i = 1; i <= n; i++)
{
if(!check[i] && (pos % i == 0 || i % pos == 0))
{
check[i] = true;
dfs(pos + 1, n);
check[i] = false;
}
}
}
};
10 N 皇后
10.1 题目链接
10.2 题目描述
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 **n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
示例 1:
输入: n = 4
输出: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释: 如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入: n = 1
输出: [["Q"]]
提示:
1 <= n <= 9
10.3 解法
算法思路:
⾸先,我们在第⼀⾏放置第⼀个皇后,然后遍历棋盘的第⼆⾏,在可⾏的位置放置第⼆个皇后,然后再遍历第三⾏,在可⾏的位置放置第三个皇后,以此类推,直到放置了 n 个皇后为⽌。
我们需要⽤⼀个数组来记录每⼀⾏放置的皇后的列数。在每⼀⾏中,我们尝试放置⼀个皇后,并检查是否会和前⾯已经放置的皇后冲突。如果没有冲突,我们就继续递归地放置下⼀⾏的皇后,直到所有的皇后都放置完毕,然后把这个⽅案记录下来。
在检查皇后是否冲突时,我们可以⽤⼀个数组来记录每⼀列是否已经放置了皇后,并检查当前要放置的皇后是否会和已经放置的皇后冲突。对于对⻆线,我们可以⽤两个数组来记录从左上⻆到右下⻆的每⼀条对⻆线上是否已经放置了皇后,以及从右上⻆到左下⻆的每⼀条对⻆线上是否已经放置了皇后。
- 对于对⻆线是否冲突的判断可以通过以下流程解决:
- 从左上到右下:相同对⻆线的⾏列之差相同;
- 从右上到左下:相同对⻆线的⾏列之和相同。
因此,我们需要创建⽤于存储解决⽅案的⼆维字符串数组 solutions ,⽤于存储每个皇后的位置的⼀维整数数组 queens ,以及⽤于记录每⼀列和对⻆线上是否已经有皇后的布尔型数组 columns 、 diagonals1 和 diagonals2 。
递归函数设计:
void dfs(vector<vector<string>> &solutions, vector<int> &queens, int &n, int row, vector<bool> &columns, vector<bool> &diagonals1, vector<bool> &diagonals2)
参数:row(当前需要处理的⾏数);
返回值:⽆;
函数作⽤:在当前⾏放⼊⼀个不发⽣冲突的皇后,查找所有可⾏的⽅案使得放置 n 个皇后后不发⽣冲突。
递归流程:
- 结束条件:如果 row 等于 n ,则表⽰已经找到⼀组解决⽅案,此时将每个皇后的位置存储到字符串数组 board 中,并将 board 存储到 solutions 数组中,然后返回;
- 枚举当前⾏的每⼀列,判断该列、两个对⻆线上是否已经有皇后:
- a. 如果有皇后,则继续枚举下⼀列;
- b. 否则,在该位置放置皇后,并将 columns 、 diagonals1 和 diagonals2 对应的位置设为 true ,表⽰该列和对⻆线上已经有皇后:
- i. 递归调⽤ dfs 函数,搜索下⼀⾏的皇后位置。如果该⽅案递归结束,则在回溯时需要将columns 、 diagonals1 和 diagonals2 对应的位置设为 false ,然后继续枚举下⼀列;
10.4 C++算法代码:
class Solution {
bool checkCol[10], checkDig1[20],checkDig2[20];
vector<vector<string>> ret;
vector<string> path;
int n;
public:
vector<vector<string>> solveNQueens(int _n) {
n = _n;
path.resize(n);
for(int i = 0; i < n; i++)
path[i].append(n, '.');
dfs(0);
return ret;
}
void dfs(int row)
{
if(row == n)
{
ret.push_back(path);
return;
}
for(int col = 0; col < n; col++) // 尝试在这一行放皇后
{
// 剪枝
if(!checkCol[col] && !checkDig1[row - col + n] && !checkDig2[row + col])
{
path[row][col] = 'Q';
checkCol[col] = checkDig1[row - col + n] = checkDig2[row + col] = true;
dfs(row + 1);
// 恢复现场
path[row][col] = '.';
checkCol[col] = checkDig1[row - col + n] = checkDig2[row + col] = false;
}
}
}
};
11 有效的数独
11.1 题目链接
11.2 题目描述
请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。
- 数字
1-9在每一行只能出现一次。 - 数字
1-9在每一列只能出现一次。 - 数字
1-9在每一个以粗实线分隔的3x3宫内只能出现一次。(请参考示例图)
注意:
- 一个有效的数独(部分已被填充)不一定是可解的。
- 只需要根据以上规则,验证已经填入的数字是否有效即可。
- 空白格用
'.'表示。
示例 1:
输入: board =
[["5","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]
输出: true
示例 2:
输入: board =
[["8","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]
输出: false
解释: 除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。 但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。
提示:
board.length == 9board[i].length == 9board[i][j]是一位数字(1-9)或者'.'
11.3 解法
算法思路:
创建三个数组标记⾏、列以及 3*3 ⼩⽅格中是否出现 1~9 之间的数字即可。
11.4 C++算法代码:
class Solution {
bool row[9][10];
bool col[9][10];
bool grid[3][3][10];
public:
bool isValidSudoku(vector<vector<char>>& board) {
for(int i = 0; i < 9; i++)
{
for(int j = 0; j < 9; j++)
{
if(board[i][j] != '.')
{
int num = board[i][j] - '0';
// 是否有效
if(row[i][num] || col[j][num] || grid[i / 3][j / 3][num])
return false;
row[i][num] = col[j][num] = grid[i / 3][j / 3][num] = true;
}
}
}
return true;
}
};
12 解数独
12.1 题目链接
12.2 题目描述
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
- 数字
1-9在每一行只能出现一次。 - 数字
1-9在每一列只能出现一次。 - 数字
1-9在每一个以粗实线分隔的3x3宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.' 表示。
示例 1:
输入: board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
输出: [["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]
解释: 输入的数独如上图所示,唯一有效的解决方案如下所示:
提示:
board.length == 9board[i].length == 9board[i][j]是一位数字或者'.'- 题目数据 保证 输入数独仅有一个解
12.3 解法
算法思路:
为了存储每个位置的元素,我们需要定义⼀个⼆维数组。⾸先,我们记录所有已知的数据,然后遍历所有需要处理的位置,并遍历数字 1~9。对于每个位置,我们检查该数字是否可以存放在该位置,同时检查⾏、列和九宫格是否唯⼀。
我们可以使⽤⼀个⼆维数组来记录每个数字在每⼀⾏中是否出现,⼀个⼆维数组来记录每个数字在每⼀列中是否出现。对于九宫格,我们可以以⾏和列除以 3 得到的商作为九宫格的坐标,并使⽤⼀个三维数组来记录每个数字在每⼀个九宫格中是否出现。在检查是否存在冲突时,只需检查⾏、列和九宫格⾥对应的数字是否已被标记。如果数字⾄少有⼀个位置(⾏、列、九宫格)被标记,则存在冲突,因此不能在该位置放置当前数字。
- 特别地,在本题中,我们需要直接修改给出的数组,因此在找到⼀种可⾏的⽅法时,应该停⽌递归,以防⽌正确的⽅法被覆盖。
初始化定义:
- 定义⾏、列、九宫格标记数组以及找到可⾏⽅法的标记变量,将它们初始化为 false。
- 定义⼀个数组来存储每个需要处理的位置。
- 将题⽬给出的所有元素的⾏、列以及九宫格坐标标记为 true。
- 将所有需要处理的位置存⼊数组。
递归函数设计:
void dfs(vector<vector<char>>& board, int pos)
参数:pos(当前需要处理的坐标);
返回值:⽆;
函数作⽤:在当前坐标填⼊合适数字,查找数独答案。
递归流程:
- 结束条件:已经处理完所有需要处理的元素。如果找到了可⾏的解决⽅案,则将标记变量更新为true 并返回。
- 获取当前需要处理的元素的⾏列值。
- 遍历数字 1~9。如果当前数字可以填⼊当前位置,并且标记变量未被赋值为 true,则将当前位置的⾏、列以及九宫格坐标标记为 true,将当前数字赋值给 board 数组中的相应位置元素,然后对下⼀个位置进⾏递归。
- 递归结束时,撤回标记。
12.4 C++算法代码:
class Solution {
bool row[9][10], col[9][10], grid[3][3][10];
public:
void solveSudoku(vector<vector<char>>& board) {
// 初始化
for(int i = 0; i < 9; i++)
{
for(int j = 0; j < 9; j++)
{
if(board[i][j] != '.')
{
int num = board[i][j] - '0';
row[i][num] = col[j][num] = grid[i / 3][j / 3][num] = true;
}
}
}
dfs(board);
}
bool dfs(vector<vector<char>>& board)
{
for(int i = 0; i < 9; i++)
{
for(int j = 0; j < 9; j++)
{
if(board[i][j] == '.')
{
// 填数
for(int num = 1; num <= 9; num++)
{
if(!row[i][num] && !col[j][num] && !grid[i/ 3][j / 3][num])
{
board[i][j] = num + '0';
row[i][num] = col[j][num] = grid[i/ 3][j / 3][num] = true;
if(dfs(board) == true) return true; // 重点理解
// 恢复现场
board[i][j] = '.';
row[i][num] = col[j][num] = grid[i/ 3][j / 3][num] = false;
}
}
return false; // 重点理解
}
}
}
return true; // 重点理解
}
};
13 单词搜索
13.1 题目链接
13.2 题目描述
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出: true
示例 2:
输入: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出: true
示例 3:
输入: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
输出: false
提示:
m == board.lengthn = board[i].length1 <= m, n <= 61 <= word.length <= 15board和word仅由大小写英文字母组成
13.3 解法
算法思路:
我们需要假设每个位置的元素作为第⼀个字⺟,然后向相邻的四个⽅向进⾏递归,并且不能出现重复使⽤同⼀个位置的元素。通过深度优先搜索的⽅式,不断地枚举相邻元素作为下⼀个字⺟出现的可能性,并在递归结束时回溯,直到枚举完所有可能性,得到正确的结果。
递归函数设计:
bool dfs(int x, int y, int step, vector<vector<char>>& board, string word, vector<vector<bool>>& vis, int &n, int &m, int &len)
参数:x(当前需要进⾏处理的元素横坐标),y(当前需要进⾏处理的元素横坐标),step(当前已经处理的元素个数),word(当前的字符串状态);
返回值:当前坐标元素作为字符串中下标 step 的元素出现是否可以找到成⽴的字符串。
函数作⽤:判断当前坐标的元素作为字符串中下标 step 的元素出现时,向四个⽅向传递,查找是否存在路径结果与字符串相同。
递归流程:
- 遍历每个位置,标记当前位置并将当前位置的字⺟作为⾸字⺟进⾏递归,并且在回溯时撤回标记。
- 在每个递归的状态中,我们维护⼀个步数 step,表⽰当前已经处理了⼏个字⺟。
- 若当前位置的字⺟与字符串中的第 step 个字⺟不相等,则返回 false。
- 若当前 step 的值与字符串⻓度相等,表⽰存在⼀种路径使得 word 成⽴,返回 true。
- 对当前位置的上下左右四个相邻位置进⾏递归,若递归结果为 true,则返回 true。
- 若相邻的四个位置的递归结果都为 false,则返回 false。
- 特别地,如果使⽤将当前遍历到的字符赋值为空格,并在回溯时恢复为原来的字⺟的⽅法,则在递归时不会重复遍历当前元素,可达到不使⽤标记数组的⽬的。
13.4 C++算法代码:
class Solution {
bool vis[7][7];
int m, n;
public:
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])
{
vis[i][j] = true;
if(dfs(board, i, j, word, 1)) return true;
vis[i][j] = false;
}
}
}
return false;
}
int dx[4] = {0, 0, -1, 1};
int dy[4] = {1, -1, 0, 0};
bool dfs(vector<vector<char>>& board, int i, int j, string& word, int pos)
{
if(pos == word.size()) return true;
// 向量的方式,定义上下左右四个位置
for(int k = 0; k < 4; k++)
{
int x = i + dx[k], y = j + dy[k];
if(x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] && board[x][y] == word[pos])
{
vis[i][j] = true;
if(dfs(board, x, y, word, pos + 1)) return true;
vis[i][j] = false;
}
}
return false;
}
};
14 黄金矿工
14.1 题目链接
14.2 题目描述
你要开发一座金矿,地质勘测学家已经探明了这座金矿中的资源分布,并用大小为 m * n 的网格 grid 进行了标注。每个单元格中的整数就表示这一单元格中的黄金数量;如果该单元格是空的,那么就是 0。
为了使收益最大化,矿工需要按以下规则来开采黄金:
- 每当矿工进入一个单元,就会收集该单元格中的所有黄金。
- 矿工每次可以从当前位置向上下左右四个方向走。
- 每个单元格只能被开采(进入)一次。
- 不得开采(进入)黄金数目为
0的单元格。 - 矿工可以从网格中 任意一个 有黄金的单元格出发或者是停止。
示例 1:
输入: grid = [[0,6,0],[5,8,7],[0,9,0]]
输出: 24
解释:
[[0,6,0],
[5,8,7],
[0,9,0]]
一种收集最多黄金的路线是:9 -> 8 -> 7。
示例 2:
输入: grid = [[1,0,7],[2,0,6],[3,4,5],[0,3,0],[9,0,20]]
输出: 28
解释:
[[1,0,7],
[2,0,6],
[3,4,5],
[0,3,0],
[9,0,20]]
一种收集最多黄金的路线是:1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7。
提示:
1 <= grid.length, grid[i].length <= 150 <= grid[i][j] <= 100- 最多 25 个单元格中有黄金。
14.3 解法
算法思路:
枚举矩阵中所有的位置当成起点,来⼀次深度优先遍历,统计出所有情况下能收集到的⻩⾦数的最⼤值即可。
14.4 C++算法代码:
class Solution {
bool vis[16][16];
int m, n;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
int ret;
public:
int getMaximumGold(vector<vector<int>>& g)
{
m = g.size(), n = g[0].size();
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
if (g[i][j])
{
vis[i][j] = true;
dfs(g, i, j, g[i][j]);
vis[i][j] = false;
}
}
}
return ret;
}
void dfs(vector<vector<int>>& g, int i, int j, int path)
{
ret = max(path, ret);
for (int k = 0; k < 4; k++)
{
int x = i + dx[k], y = j + dy[k];
if (x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] && g[x][y])
{
vis[x][y] = true;
dfs(g, x, y, path + g[x][y]);
vis[x][y] = false;
}
}
}
};
15 Unique Paths III(不同路径 Ⅲ)
15.1 题目链接
15.2 题目描述
在二维网格 grid 上,有 4 种类型的方格:
1表示起始方格。且只有一个起始方格。2表示结束方格,且只有一个结束方格。0表示我们可以走过的空方格。-1表示我们无法跨越的障碍。
返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目 。
每一个无障碍方格都要通过一次,但是一条路径中不能重复通过同一个方格。
示例 1:
输入: [[1,0,0,0],[0,0,0,0],[0,0,2,-1]]
输出: 2
解释: 我们有以下两条路径:
1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2)
2. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2)
示例 2:
输入: [[1,0,0,0],[0,0,0,0],[0,0,0,2]]
输出: 4
解释: 我们有以下四条路径:
1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2),(2,3)
2. (0,0),(0,1),(1,1),(1,0),(2,0),(2,1),(2,2),(1,2),(0,2),(0,3),(1,3),(2,3)
3. (0,0),(1,0),(2,0),(2,1),(2,2),(1,2),(1,1),(0,1),(0,2),(0,3),(1,3),(2,3)
4. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2),(2,3)
示例 3:
输入: [[0,1],[2,0]]
输出: 0
解释:
没有一条路能完全穿过每一个空的方格一次。
请注意,起始和结束方格可以位于网格中的任意位置。
提示:
1 <= grid.length * grid[0].length <= 20
15.3 解法
算法思路:
对于四个⽅向,我们可以定义⼀个⼆维数组 next ,⼤⼩为 4 ,每⼀维存储四个⽅向的坐标偏移量(详⻅代码)。题⽬要求到达⽬标位置时所有⽆障碍⽅格都存在路径中,我们可以定义⼀个变量记录 num 当前状态中剩余的未⾛过的⽆障碍⽅格个数,则当我们⾛到⽬标地点时只需要判断 num 是否为 0 即可。在移动时需要判断是否越界。
递归函数设计:
void dfs(vector<vector<int>>& grid, int x, int y, int num)
参数:x,y(当前需要处理元素的坐标),num(当前剩余⽆障碍⽅格个数);
返回值:⽆;
函数作⽤:判断当前位置的四个⽅向是否可以添加⾄当前状态,查找在满⾜条件下从起始⽅格到结束⽅格的不同路径的数⽬。
递归流程:
- 递归结束条件:当前位置的元素值为 2,若此时可⾛的位置数量 num 的值为 0,则 cnt 的值加⼀;
- 遍历四个⽅向,若移动后未越界,⽆障碍并且未被标记,则标记当前位置,并递归移动后的位置,在回溯时撤销标记操作。
15.4 C++算法代码:
class Solution {
bool vis[21][21];
int m, n;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
int ret;
int step;
public:
int uniquePathsIII(vector<vector<int>>& grid) {
m = grid.size(), n = grid[0].size();
int bx = 0, by = 0;
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
if (grid[i][j] == 0)
step++;
else if (grid[i][j] == 1)
{
bx = i;
by = j;
}
}
}
step += 2;
vis[bx][by] = true;
dfs(grid, bx, by, 1);
return ret;
}
void dfs(vector<vector<int>>& grid, int i, int j, int count)
{
if(grid[i][j] == 2)
{
if(count == step) // 判断是否合法
ret++;
return;
}
for(int k = 0; k < 4; k++)
{
int x = i + dx[k], y = j +dy[k];
if (x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] && grid[x][y] != -1)
{
vis[x][y] = true;
dfs(grid, x, y, count +1);
vis[x][y] = false;
}
}
}
};