目录
一、1863. 找出所有子集的异或总和再求和 - 力扣(LeetCode)
算法代码:
代码思路
问题分析
核心思想
实现细节
代码解析
初始化
DFS 函数
时间复杂度
空间复杂度
示例运行
输入
运行过程
总结
二、 47. 全排列 II - 力扣(LeetCode)
算法代码:
代码思路
问题分析
核心思想
实现细节
代码解析
排序
DFS 函数
剪枝条件
时间复杂度
空间复杂度
示例运行
输入
运行过程
总结
以下是 if 条件的详细解析:
if 条件
1. check[i] == false
2. i == 0
3. nums[i] != nums[i - 1]
4. check[i - 1] != false
if 条件的逻辑
示例说明
输入:
排序后:
运行过程:
三、17. 电话号码的字母组合 - 力扣(LeetCode)
算法代码:
代码思路解析
类的成员变量
主函数 letterCombinations
DFS 函数 dfs
代码优化建议
参数传递优化
提前终止
代码可读性
优化后的代码
总结
四、 22. 括号生成 - 力扣(LeetCode)
算法代码:(不传参)
代码思路解析
类的成员变量
主函数 generateParenthesis
DFS 函数 dfs
代码优化建议
参数传递优化
提前终止
代码可读性
优化后的代码
总结
算法代码:(传参)
五、77. 组合 - 力扣(LeetCode)
算法代码:
代码思路解析
类的成员变量
主函数 combine
DFS 函数 dfs
代码优化建议
剪枝优化
参数传递优化
代码可读性
优化后的代码
优化点详解
剪枝优化
回溯的恢复现场
总结
六、494. 目标和 - 力扣(LeetCode)
算法代码:
代码思路解析
类的成员变量
主函数 findTargetSumWays
DFS 函数 dfs
把 path 改为全局变量时会超时,为什么呢?
回溯的恢复现场问题
递归调用栈的深度
多线程问题
代码可读性和调试难度
问题分析:
总结
七、39. 组合总和 - 力扣(LeetCode)
解法一:算法代码(回溯)
代码思路解析
类的成员变量
主函数 combinationSum
DFS 函数 dfs
解法二:算法代码(无限次使用判断有无越界)
代码思路解析
类的成员变量
主函数 combinationSum
DFS 函数 dfs
代码优化建议
剪枝优化
恢复现场的优化
代码可读性
优化后的代码
代码细节解析
枚举 k 的作用
恢复现场
递归调用
总结
八、 784. 字母大小写全排列 - 力扣(LeetCode)
算法代码:
代码思路解析
类的成员变量
主函数 letterCasePermutation
DFS 函数 dfs
辅助函数 change
代码优化建议
减少函数调用
剪枝优化
代码可读性
优化后的代码
代码细节解析
不改变字符
改变字符大小写
剪枝优化
总结
九、526. 优美的排列 - 力扣(LeetCode)
算法代码:
代码思路解析
类的成员变量
主函数 countArrangement
DFS 函数 dfs
代码优化建议
剪枝优化
代码可读性
优化后的代码
代码细节解析
check 数组的作用
剪枝条件
回溯的恢复现场
总结
十、 51. N 皇后 - 力扣(LeetCode)
算法代码:
代码思路解析
类的成员变量
主函数 solveNQueens
DFS 函数 dfs
代码优化建议
剪枝优化
代码可读性
优化后的代码
代码细节解析
checkCol、checkDig1、checkDig2 的作用
放置皇后
回溯的恢复现场
总结
十一、36. 有效的数独 - 力扣(LeetCode)
算法代码:
代码思路解析
数据结构
遍历棋盘
检查有效性
返回结果
代码优化建议
总结:
十二、 37. 解数独 - 力扣(LeetCode)
算法代码:
代码思路解析
数据结构
初始化
深度优先搜索(DFS)
回溯
终止条件
代码优化建议
总结
十三、 79. 单词搜索 - 力扣(LeetCode)
算法代码:
代码思路解析
数据结构
主函数 exist
深度优先搜索(DFS)函数 dfs
回溯
代码优化建议
总结
十四、 1219. 黄金矿工 - 力扣(LeetCode)
算法代码:
代码思路解析
数据结构
主函数 getMaximumGold
深度优先搜索(DFS)函数 dfs
回溯
代码优化建议
总结
十五、980. 不同路径 III - 力扣(LeetCode)
算法代码:
代码思路解析
数据结构
主函数 uniquePathsIII
深度优先搜索(DFS)函数 dfs
回溯
代码优化建议
总结
一、1863. 找出所有子集的异或总和再求和 - 力扣(LeetCode)
算法代码:
class Solution {
int path; // 当前子集的异或和
int sum; // 所有子集异或和的总和
public:
int subsetXORSum(vector<int>& nums) {
path = 0; // 初始化 path
sum = 0; // 初始化 sum
dfs(nums, 0); // 从第 0 个元素开始 DFS
return sum;
}
void dfs(vector<int>& nums, int pos) {
sum += path; // 将当前子集的异或和累加到 sum
for (int i = pos; i < nums.size(); i++) {
path ^= nums[i]; // 将 nums[i] 加入当前子集,更新 path
dfs(nums, i + 1); // 递归处理下一个元素
path ^= nums[i]; // 回溯,恢复 path 的值
}
}
};
代码思路
-
问题分析
-
给定一个数组
nums
,需要计算所有子集的异或和的总和。 -
例如,
nums = [1, 2, 3]
,子集包括[], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]
,它们的异或和分别为0, 1, 2, 3, 3, 2, 1, 0
,总和为0 + 1 + 2 + 3 + 3 + 2 + 1 + 0 = 12
。
-
-
核心思想
-
使用深度优先搜索(DFS)遍历所有可能的子集。
-
在 DFS 过程中,维护一个变量
path
,表示当前子集的异或和。 -
每次递归时,将当前子集的异或和
path
累加到sum
中。 -
通过回溯恢复现场,确保
path
的正确性。
-
-
实现细节
-
path
:表示当前子集的异或和。 -
sum
:表示所有子集异或和的总和。 -
dfs
函数:-
将当前
path
的值累加到sum
。 -
遍历数组
nums
,从当前位置pos
开始,逐个尝试将元素加入当前子集。 -
递归调用
dfs
,继续处理下一个元素。 -
回溯时,恢复
path
的值(通过再次异或当前元素)。
-
-
代码解析
-
初始化
-
path
和sum
初始化为 0。
-
-
DFS 函数
-
累加当前子集的异或和:
sum += path
。 -
遍历数组:
-
将当前元素
nums[i]
异或到path
中,表示将其加入当前子集。 -
递归调用
dfs
,处理下一个元素。 -
回溯时,再次异或
nums[i]
,恢复path
的值。
-
-
-
时间复杂度
-
由于每个元素有两种选择(加入或不加入子集),总共有 2n 个子集,因此时间复杂度为 O(2n)。
-
-
空间复杂度
-
递归栈的深度为 O(n),因此空间复杂度为 O(n)。
-
示例运行
输入
nums = [1, 2, 3];
运行过程
-
初始状态:
path = 0
,sum = 0
。 -
DFS 遍历所有子集:
-
[]
:path = 0
,sum += 0
。 -
[1]
:path = 1
,sum += 1
。 -
[1, 2]
:path = 3
,sum += 3
。 -
[1, 2, 3]
:path = 0
,sum += 0
。 -
[1, 3]
:path = 2
,sum += 2
。 -
[2]
:path = 2
,sum += 2
。 -
[2, 3]
:path = 1
,sum += 1
。 -
[3]
:path = 3
,sum += 3
。
-
-
最终结果:
sum = 0 + 1 + 3 + 0 + 2 + 2 + 1 + 3 = 12
。
总结
-
这段代码通过 DFS 和回溯,高效地计算了所有子集的异或和的总和。
-
核心思想是利用递归遍历所有可能的子集,并通过回溯恢复现场,确保计算的正确性。
-
代码简洁清晰,适合解决类似子集相关的问题。
二、 47. 全排列 II - 力扣(LeetCode)
算法代码:
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); // 从第 0 个位置开始 DFS
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]); // 将 nums[i] 加入当前排列
check[i] = true; // 标记 nums[i] 已被使用
dfs(nums, pos + 1); // 递归处理下一个位置
path.pop_back(); // 回溯,恢复 path
check[i] = false; // 回溯,恢复 check
}
}
}
};
代码思路
-
问题分析
-
给定一个可能包含重复元素的数组
nums
,需要生成所有不重复的全排列。 -
例如,
nums = [1, 1, 2]
,不重复的全排列为[[1,1,2], [1,2,1], [2,1,1]]
。
-
-
核心思想
-
使用深度优先搜索(DFS)生成所有可能的排列。
-
通过排序和剪枝,避免生成重复的排列。
-
使用一个布尔数组
check
记录哪些元素已经被使用过。
-
-
实现细节
-
path
:记录当前生成的排列。 -
ret
:存储所有不重复的排列。 -
check
:记录每个元素是否已经被使用。 -
dfs
函数:-
如果当前排列的长度等于
nums
的长度,将其加入ret
。 -
遍历数组
nums
,尝试将未使用的元素加入当前排列。 -
通过剪枝条件避免重复排列。
-
回溯时恢复现场,确保
path
和check
的正确性。
-
-
代码解析
-
排序
-
对数组
nums
进行排序,使得相同的元素相邻,方便剪枝。
-
-
DFS 函数
-
终止条件:如果当前排列的长度等于
nums
的长度,将其加入ret
。 -
遍历数组:
-
如果当前元素未被使用(
check[i] == false
),并且满足剪枝条件,则将其加入当前排列。 -
递归调用
dfs
,处理下一个位置。 -
回溯时,恢复
path
和check
的值。
-
-
-
剪枝条件
-
i == 0
:第一个元素无需判断是否重复。 -
nums[i] != nums[i - 1]
:当前元素与前一个元素不同,无需剪枝。 -
check[i - 1] != false
:前一个相同元素已被使用,说明当前元素是新的排列起点。
-
-
时间复杂度
-
最坏情况下,所有排列都不重复,时间复杂度为 O(n×n!)O(n×n!),其中 n!n! 是排列数,nn 是生成每个排列的时间。
-
-
空间复杂度
-
递归栈的深度为 O(n)O(n),结果集
ret
的空间为 O(n×n!)O(n×n!)。
-
示例运行
输入
nums = [1, 1, 2];
运行过程
-
排序后:
nums = [1, 1, 2]
。 -
DFS 遍历:
-
生成
[1, 1, 2]
。 -
生成
[1, 2, 1]
。 -
生成
[2, 1, 1]
。
-
-
最终结果:
ret = [[1,1,2], [1,2,1], [2,1,1]]
。
总结
-
这段代码通过 DFS 和剪枝,高效地生成了所有不重复的全排列。
-
核心思想是利用排序和剪枝条件,避免重复排列的生成。
-
代码简洁清晰,适合解决类似排列相关的问题。
在代码中,if
条件的写法是为了避免生成重复的排列,尤其是在数组 nums
包含重复元素的情况下。这个条件的作用是剪枝,即跳过不必要的递归分支,从而减少重复计算。
以下是 if
条件的详细解析:
if
条件
if (check[i] == false &&
(i == 0 || nums[i] != nums[i - 1] || check[i - 1] != false))
1. check[i] == false
-
作用:确保当前元素
nums[i]
未被使用过。 -
解释:
-
check[i]
是一个布尔数组,记录nums[i]
是否已经被加入当前排列。 -
如果
check[i] == true
,说明nums[i]
已经被使用过,不能重复使用。
-
2. i == 0
-
作用:处理第一个元素,避免越界。
-
解释:
-
当
i == 0
时,nums[i - 1]
不存在,因此不需要判断nums[i] != nums[i - 1]
或check[i - 1] != false
。
-
3. nums[i] != nums[i - 1]
-
作用:确保当前元素与前一个元素不同。
-
解释:
-
如果
nums[i] == nums[i - 1]
,说明当前元素和前一个元素相同。 -
为了避免重复排列,只有当当前元素与前一个元素不同时,才继续递归。
-
4. check[i - 1] != false
-
作用:确保前一个相同元素已经被使用过。
-
解释:
-
如果
nums[i] == nums[i - 1]
,并且check[i - 1] == false
,说明前一个相同元素未被使用过。 -
这种情况下,如果直接使用
nums[i]
,会导致重复排列。 -
只有当
check[i - 1] != false
(即前一个相同元素已经被使用过),才允许使用nums[i]
。
-
if
条件的逻辑
-
整体逻辑:
-
如果当前元素未被使用过(
check[i] == false
),并且满足以下条件之一:-
当前元素是第一个元素(
i == 0
)。 -
当前元素与前一个元素不同(
nums[i] != nums[i - 1]
)。 -
前一个相同元素已经被使用过(
check[i - 1] != false
)。
-
-
则继续递归,否则跳过。
-
-
为什么这样写:
-
通过排序,相同的元素会相邻。
-
对于相同的元素,只有当前一个相同元素已经被使用过时,才允许使用当前元素。
-
这样可以避免生成重复的排列。
-
示例说明
输入:
nums = [1, 1, 2];
排序后:
nums = [1, 1, 2];
运行过程:
-
第一次递归:
-
选择
nums[0] = 1
,check[0] = true
。 -
进入下一层递归。
-
-
第二次递归:
-
选择
nums[1] = 1
,此时nums[1] == nums[0]
,但check[0] == true
,因此允许选择。 -
check[1] = true
。 -
进入下一层递归。
-
-
第三次递归:
-
选择
nums[2] = 2
,check[2] = true
。 -
生成排列
[1, 1, 2]
。
-
-
回溯:
-
恢复
check[2] = false
。 -
恢复
check[1] = false
。 -
恢复
check[0] = false
。
-
-
第二次递归(另一种选择):
-
选择
nums[2] = 2
,check[2] = true
。 -
进入下一层递归。
-
-
第三次递归:
-
选择
nums[0] = 1
,check[0] = true
。 -
生成排列
[1, 2, 1]
。
-
-
回溯:
-
恢复
check[0] = false
。 -
恢复
check[2] = false
。
-
-
继续递归:
-
类似过程生成
[2, 1, 1]
。
-
三、17. 电话号码的字母组合 - 力扣(LeetCode)
算法代码:
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(); // 恢复现场
}
}
};
这段代码的目的是生成给定数字字符串 digits
所代表的所有可能的字母组合。例如,输入 "23"
,输出 ["ad","ae","af","bd","be","bf","cd","ce","cf"]
。代码使用了深度优先搜索(DFS)来遍历所有可能的组合。
代码思路解析
-
类的成员变量
-
hash[10]
:一个字符串数组,存储了每个数字对应的字母。例如,2
对应"abc"
,3
对应"def"
,依此类推。 -
path
:一个字符串,用于存储当前正在构建的字母组合。 -
ret
:一个字符串向量,用于存储所有可能的字母组合。
-
-
主函数
letterCombinations
-
首先检查输入字符串
digits
是否为空。如果为空,直接返回空的ret
。 -
否则,调用
dfs
函数开始深度优先搜索。
-
-
DFS 函数
dfs
-
pos
参数表示当前处理到digits
中的第几个字符。 -
如果
pos
等于digits
的长度,说明已经处理完所有字符,当前的path
就是一个完整的字母组合,将其加入ret
中。 -
否则,遍历当前数字对应的所有字母(通过
hash[digits[pos] - '0']
获取),并将每个字母依次加入path
中,然后递归调用dfs
处理下一个字符。 -
递归调用结束后,通过
path.pop_back()
恢复现场,以便尝试下一个字母。
-
代码优化建议
-
参数传递优化
-
digits
和path
可以通过引用传递,避免不必要的拷贝。
-
-
提前终止
-
如果
digits
为空,可以直接返回空结果,不需要进入 DFS。
-
-
代码可读性
-
可以添加注释,解释每个步骤的作用,方便他人理解。
-
优化后的代码
class Solution {
// 数字到字母的映射
const string hash[10] = {"", "", "abc", "def", "ghi",
"jkl", "mno", "pqrs", "tuv", "wxyz"};
string path; // 当前路径
vector<string> ret; // 结果集
public:
vector<string> letterCombinations(string digits) {
if (digits.empty()) {
return ret; // 如果输入为空,直接返回空结果
}
dfs(digits, 0); // 开始深度优先搜索
return ret;
}
void dfs(const string& digits, int pos) {
if (pos == digits.size()) {
ret.push_back(path); // 找到一个完整的组合,加入结果集
return;
}
// 遍历当前数字对应的所有字母
for (char ch : hash[digits[pos] - '0']) {
path.push_back(ch); // 选择当前字母
dfs(digits, pos + 1); // 递归处理下一个数字
path.pop_back(); // 回溯,撤销选择
}
}
};
总结
这段代码的核心思想是通过 DFS 遍历所有可能的字母组合,并通过回溯来恢复现场,确保每个组合都被正确生成。代码结构清晰,逻辑简单,适合处理这类组合问题。
四、 22. 括号生成 - 力扣(LeetCode)
算法代码:(不传参)
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--; // 恢复现场
}
}
};
这段代码的目的是生成所有有效的括号组合,给定一个整数 n
,表示生成 n
对括号。例如,当 n = 3
时,输出 ["((()))","(()())","(())()","()(())","()()()"]
。代码使用了深度优先搜索(DFS)和回溯的思想来遍历所有可能的组合。
代码思路解析
-
类的成员变量
-
left
:记录当前路径中左括号的数量。 -
right
:记录当前路径中右括号的数量。 -
n
:表示需要生成的括号对数。 -
path
:一个字符串,用于存储当前正在构建的括号组合。 -
ret
:一个字符串向量,用于存储所有有效的括号组合。
-
-
主函数
generateParenthesis
-
初始化
n
为输入的_n
。 -
调用
dfs
函数开始深度优先搜索。 -
返回结果
ret
。
-
-
DFS 函数
dfs
-
如果
right == n
,说明当前路径中的右括号数量已经达到n
,即已经生成了一个有效的括号组合,将其加入ret
中。 -
如果
left < n
,说明还可以添加左括号:-
将左括号
(
加入path
中。 -
增加
left
的计数。 -
递归调用
dfs
。 -
回溯时,移除刚刚添加的左括号,并减少
left
的计数。
-
-
如果
right < left
,说明可以添加右括号:-
将右括号
)
加入path
中。 -
增加
right
的计数。 -
递归调用
dfs
。 -
回溯时,移除刚刚添加的右括号,并减少
right
的计数。
-
-
代码优化建议
-
参数传递优化
-
path
可以通过引用传递,避免不必要的拷贝。
-
-
提前终止
-
如果
left
或right
超过n
,可以直接返回,避免无效的递归调用。
-
-
代码可读性
-
可以添加注释,解释每个步骤的作用,方便他人理解。
-
优化后的代码
class Solution {
int left, right, n; // 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--; // 恢复右括号计数
}
}
};
总结
这段代码的核心思想是通过 DFS 遍历所有可能的括号组合,并通过回溯来恢复现场,确保每个组合都是有效的。代码结构清晰,逻辑简单,适合处理这类组合问题。通过限制左括号和右括号的数量,确保生成的括号组合始终是有效的。
算法代码:(传参)
class Solution {
vector<string> ret; // 结果集
public:
vector<string> generateParenthesis(int n) {
dfs(0, 0, n, ""); // 开始深度优先搜索
return ret;
}
void dfs(int left, int right, int n, string path) {
if (right == n) {
ret.push_back(path); // 找到一个有效的括号组合,加入结果集
return;
}
if (left < n) { // 可以添加左括号
dfs(left + 1, right, n, path + '('); // 选择左括号,递归处理
}
if (right < left) { // 可以添加右括号
dfs(left, right + 1, n, path + ')'); // 选择右括号,递归处理
}
}
};
五、77. 组合 - 力扣(LeetCode)
算法代码:
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(); // 恢复现场
}
}
};
这段代码的目的是生成从 1
到 n
中所有可能的 k
个数的组合。例如,当 n = 4
,k = 2
时,输出 [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
。代码使用了深度优先搜索(DFS)和回溯的思想来遍历所有可能的组合。
代码思路解析
-
类的成员变量
-
path
:一个整数向量,用于存储当前正在构建的组合。 -
ret
:一个二维整数向量,用于存储所有可能的组合。 -
n
:表示数字范围的上限(从1
到n
)。 -
k
:表示每个组合中数字的个数。
-
-
主函数
combine
-
初始化
n
和k
为输入的_n
和_k
。 -
调用
dfs(1)
开始深度优先搜索,从数字1
开始。 -
返回结果
ret
。
-
-
DFS 函数
dfs
-
start
参数表示当前可以选择的起始数字。 -
如果
path.size() == k
,说明当前路径中的数字数量已经达到k
,即已经生成了一个有效的组合,将其加入ret
中。 -
否则,从
start
开始遍历到n
:-
将当前数字
i
加入path
中。 -
递归调用
dfs(i + 1)
,确保下一个数字比当前数字大,避免重复组合。 -
回溯时,移除刚刚添加的数字
i
,恢复现场。
-
-
代码优化建议
-
剪枝优化
-
在遍历时,如果剩余的数字不足以填满
k
个数的组合,可以直接跳过。例如,当path.size() + (n - i + 1) < k
时,可以直接break
。
-
-
参数传递优化
-
path
可以通过引用传递,避免不必要的拷贝。
-
-
代码可读性
-
可以添加注释,解释每个步骤的作用,方便他人理解。
-
优化后的代码
class Solution {
vector<int> path; // 当前路径
vector<vector<int>> ret; // 结果集
int n, k; // n: 数字范围上限,k: 组合中数字的个数
public:
vector<vector<int>> combine(int _n, int _k) {
n = _n;
k = _k;
dfs(1); // 从数字 1 开始深度优先搜索
return ret;
}
void dfs(int start) {
if (path.size() == k) {
ret.push_back(path); // 找到一个有效的组合,加入结果集
return;
}
// 剪枝:如果剩余的数字不足以填满 k 个数的组合,直接返回
for (int i = start; i <= n - (k - path.size()) + 1; i++) {
path.push_back(i); // 选择当前数字
dfs(i + 1); // 递归处理下一个数字
path.pop_back(); // 回溯,撤销选择
}
}
};
优化点详解
-
剪枝优化
-
在
for
循环中,i
的上限从n
改为n - (k - path.size()) + 1
。 -
这是因为如果剩余的数字不足以填满
k
个数的组合,继续遍历是没有意义的。 -
例如,当
n = 4
,k = 2
,且path.size() = 1
时,i
的最大值应该是3
(因为4
只能和5
组合,但5
超出了范围)。
-
-
回溯的恢复现场
-
在递归调用结束后,通过
path.pop_back()
移除刚刚添加的数字,确保path
恢复到之前的状态。
-
总结
这段代码的核心思想是通过 DFS 遍历所有可能的组合,并通过回溯来恢复现场,确保每个组合都是唯一的。通过剪枝优化,可以减少不必要的递归调用,提高代码效率。代码结构清晰,逻辑简单,适合处理这类组合问题。
六、494. 目标和 - 力扣(LeetCode)
算法代码:
class Solution {
int ret, aim; // 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]);
}
};
这段代码的目的是在给定一个整数数组 nums
和一个目标值 target
的情况下,计算有多少种不同的方式可以通过在数组中的每个数字前添加 +
或 -
,使得表达式的值等于 target
。例如,nums = [1,1,1,1,1]
,target = 3
,输出 5
。
代码使用了深度优先搜索(DFS)来遍历所有可能的加减组合,并通过回溯的方式统计满足条件的组合数量。
代码思路解析
-
类的成员变量
-
ret
:用于记录满足条件的组合数量。 -
aim
:存储目标值target
。
-
-
主函数
findTargetSumWays
-
初始化
aim
为target
。 -
调用
dfs(nums, 0, 0)
开始深度优先搜索,从数组的第一个位置(pos = 0
)和初始路径和(path = 0
)开始。 -
返回结果
ret
。
-
-
DFS 函数
dfs
-
nums
:输入的整数数组。 -
pos
:当前处理到的数组位置。 -
path
:当前路径的和(即当前表达式的值)。 -
如果
pos == nums.size()
,说明已经处理完所有数字,检查当前路径和是否等于aim
。如果相等,ret++
。 -
否则,分别尝试对当前数字进行加法和减法操作,并递归调用
dfs
。
-
class Solution {
int ret, aim, path; // path 改为全局变量
public:
int findTargetSumWays(vector<int>& nums, int target) {
aim = target;
path = 0; // 初始化 path
dfs(nums, 0);
return ret;
}
void dfs(vector<int>& nums, int pos) {
if (pos == nums.size()) {
if (path == aim) {
ret++;
}
return;
}
// 加法
path += nums[pos]; // 修改全局变量 path
dfs(nums, pos + 1);
path -= nums[pos]; // 恢复现场
// 减法
path -= nums[pos]; // 修改全局变量 path
dfs(nums, pos + 1);
path += nums[pos]; // 恢复现场
}
};
把 path
改为全局变量时会超时,为什么呢?
如果将 path
改为全局变量(即类的成员变量),代码可能会超时,原因如下:
-
回溯的恢复现场问题
-
在当前的实现中,
path
是通过参数传递的,每次递归调用都会创建一个新的path
值。这样,在回溯时不需要手动恢复path
的状态。 -
如果将
path
改为全局变量,每次递归调用都会修改同一个path
变量。在回溯时,必须手动恢复path
的状态(例如,path -= nums[pos]
或path += nums[pos]
),否则会导致状态混乱。
-
-
递归调用栈的深度
-
如果
path
是全局变量,每次递归调用都会修改同一个变量,这会导致递归调用栈的深度增加,从而增加时间和空间复杂度。 -
而通过参数传递
path
,每次递归调用都会创建一个新的path
值,递归调用栈的深度不会增加。
-
-
多线程问题
-
如果代码在多线程环境中运行,全局变量
path
可能会导致线程安全问题。而通过参数传递path
,每个线程可以维护自己的状态。
-
-
代码可读性和调试难度
-
使用全局变量
path
会使代码的逻辑变得复杂,尤其是在回溯时需要手动恢复状态。这会增加调试的难度。 -
通过参数传递
path
,代码的逻辑更加清晰,调试也更加方便。
-
问题分析:
-
每次递归调用都会修改全局变量
path
,导致回溯时需要手动恢复状态。 -
如果忘记恢复状态,会导致错误的结果。
-
递归调用栈的深度增加,可能导致超时。
总结
-
path
作为参数:每次递归调用都会创建一个新的path
值,回溯时不需要手动恢复状态,代码逻辑清晰,效率高。 -
path
作为全局变量:需要手动恢复状态,容易出错,递归调用栈深度增加,可能导致超时。
因此,在这种回溯问题中,推荐将 path
作为参数传递,而不是作为全局变量。
七、39. 组合总和 - 力扣(LeetCode)
解法一:算法代码(回溯)
class Solution {
vector<vector<int>> ret; // 存储所有满足条件的组合
vector<int> path; // 当前路径
public:
vector<vector<int>> combinationSum(vector<int>& nums, int target) {
dfs(nums, target, 0); // 开始深度优先搜索
return ret;
}
void dfs(vector<int>& nums, int target, int start) {
if (target == 0) { // 如果目标值为0,说明当前路径满足条件
ret.push_back(path);
return;
}
if (target < 0) { // 如果目标值小于0,说明当前路径不满足条件
return;
}
for (int i = start; i < nums.size(); i++) { // 从start开始遍历数组
path.push_back(nums[i]); // 选择当前数字
dfs(nums, target - nums[i], i); // 递归处理,注意可以重复选择当前数字
path.pop_back(); // 回溯,撤销选择
}
}
};
代码思路解析
-
类的成员变量
-
ret
:用于存储所有满足条件的组合。 -
path
:用于存储当前正在构建的组合。
-
-
主函数
combinationSum
-
调用
dfs(nums, target, 0)
开始深度优先搜索,从数组的第一个位置开始。
-
-
DFS 函数
dfs
-
nums
:输入的整数数组。 -
target
:当前剩余的目标值。 -
start
:当前处理到的数组位置。 -
如果
target == 0
,说明当前路径的和等于目标值,将path
加入ret
中。 -
如果
target < 0
,说明当前路径的和已经超过目标值,直接返回。 -
否则,从
start
开始遍历数组:-
将当前数字
nums[i]
加入path
中。 -
递归调用
dfs(nums, target - nums[i], i)
,允许重复选择当前数字。 -
回溯时,移除刚刚添加的数字
nums[i]
,恢复现场。
-
-
解法二:算法代码(无限次使用判断有无越界)
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();
}
}
};
这段代码的目的是在给定一个无重复元素的整数数组 nums
和一个目标值 target
的情况下,找出所有满足和为 target
的组合。数组中的数字可以无限次使用(即允许重复选择)。例如,nums = [2,3,6,7]
,target = 7
,输出 [[2,2,3],[7]]
。
代码使用了深度优先搜索(DFS)和回溯的思想来遍历所有可能的组合,并通过剪枝优化减少不必要的递归调用。
代码思路解析
-
类的成员变量
-
aim
:存储目标值target
。 -
path
:一个整数向量,用于存储当前正在构建的组合。 -
ret
:一个二维整数向量,用于存储所有满足条件的组合。
-
-
主函数
combinationSum
-
初始化
aim
为target
。 -
调用
dfs(nums, 0, 0)
开始深度优先搜索,从数组的第一个位置(pos = 0
)和初始和(sum = 0
)开始。 -
返回结果
ret
。
-
-
DFS 函数
dfs
-
nums
:输入的整数数组。 -
pos
:当前处理到的数组位置。 -
sum
:当前路径的和。 -
如果
sum == aim
,说明当前路径的和等于目标值,将path
加入ret
中。 -
如果
sum > aim
或pos == nums.size()
,说明当前路径的和已经超过目标值,或者已经处理完所有数字,直接返回。 -
否则,枚举当前数字
nums[pos]
的使用次数k
:-
如果
k > 0
,将nums[pos]
加入path
中。 -
递归调用
dfs(nums, pos + 1, sum + k * nums[pos])
,处理下一个数字。
-
-
回溯时,恢复现场,将
nums[pos]
从path
中移除。
-
代码优化建议
-
剪枝优化
-
在枚举
k
时,可以通过k * nums[pos] + sum <= aim
来限制k
的最大值,避免不必要的递归调用。
-
-
恢复现场的优化
-
在回溯时,可以通过一个循环将
nums[pos]
从path
中移除,确保path
恢复到之前的状态。
-
-
代码可读性
-
可以添加注释,解释每个步骤的作用,方便他人理解。
-
优化后的代码
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 > 0) { // 如果 k > 0,将当前数字加入路径
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();
}
}
};
代码细节解析
-
枚举
k
的作用-
k
表示当前数字nums[pos]
的使用次数。 -
通过
k * nums[pos] + sum <= aim
限制k
的最大值,避免不必要的递归调用。
-
-
恢复现场
-
在回溯时,通过一个循环将
nums[pos]
从path
中移除,确保path
恢复到之前的状态。
-
-
递归调用
-
每次递归调用都会处理下一个数字(
pos + 1
),并更新当前路径和(sum + k * nums[pos]
)。
-
总结
这段代码的核心思想是通过 DFS 遍历所有可能的组合,并通过回溯来恢复现场,确保每个组合都是有效的。通过枚举 k
来限制当前数字的使用次数,并通过剪枝优化减少不必要的递归调用。代码结构清晰,逻辑简单,适合处理这类组合问题。
八、 784. 字母大小写全排列 - 力扣(LeetCode)
算法代码:
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;
}
};
这段代码的目的是生成给定字符串 s
的所有可能的字母大小写排列组合。例如,输入 s = "a1b2"
,输出 ["a1b2","a1B2","A1b2","A1B2"]
。代码使用了深度优先搜索(DFS)和回溯的思想来遍历所有可能的组合。
代码思路解析
-
类的成员变量
-
path
:一个字符串,用于存储当前正在构建的排列组合。 -
ret
:一个字符串向量,用于存储所有可能的排列组合。
-
-
主函数
letterCasePermutation
-
调用
dfs(s, 0)
开始深度优先搜索,从字符串的第一个字符(pos = 0
)开始。 -
返回结果
ret
。
-
-
DFS 函数
dfs
-
s
:输入的字符串。 -
pos
:当前处理到的字符位置。 -
如果
pos == s.length()
,说明已经处理完所有字符,当前的path
就是一个完整的排列组合,将其加入ret
中。 -
否则,获取当前字符
ch = s[pos]
:-
不改变字符:
-
将当前字符
ch
加入path
中。 -
递归调用
dfs(s, pos + 1)
,处理下一个字符。 -
回溯时,移除刚刚添加的字符
ch
,恢复现场。
-
-
改变字符大小写:
-
如果当前字符是字母(不是数字),调用
change(ch)
函数改变其大小写。 -
将改变后的字符加入
path
中。 -
递归调用
dfs(s, pos + 1)
,处理下一个字符。 -
回溯时,移除刚刚添加的字符,恢复现场。
-
-
-
-
辅助函数
change
-
如果字符是小写字母(
a-z
),将其转换为大写字母。 -
如果字符是大写字母(
A-Z
),将其转换为小写字母。 -
返回改变后的字符。
-
代码优化建议
-
减少函数调用
-
可以将
change
函数的逻辑直接嵌入到dfs
函数中,减少函数调用的开销。
-
-
剪枝优化
-
如果当前字符是数字,直接跳过大小写转换的逻辑,避免不必要的递归调用。
-
-
代码可读性
-
可以添加注释,解释每个步骤的作用,方便他人理解。
-
优化后的代码
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 (isalpha(ch)) { // 剪枝:只处理字母
char tmp = (ch >= 'a' && ch <= 'z') ? ch - 32 : ch + 32; // 改变大小写
path.push_back(tmp); // 选择改变后的字符
dfs(s, pos + 1); // 递归处理下一个字符
path.pop_back(); // 回溯,撤销选择
}
}
};
代码细节解析
-
不改变字符
-
将当前字符
ch
加入path
中。 -
递归处理下一个字符。
-
回溯时,移除刚刚添加的字符
ch
。
-
-
改变字符大小写
-
如果当前字符是字母,调用
change
函数改变其大小写。 -
将改变后的字符加入
path
中。 -
递归处理下一个字符。
-
回溯时,移除刚刚添加的字符。
-
-
剪枝优化
-
如果当前字符是数字,直接跳过大小写转换的逻辑,避免不必要的递归调用。
-
总结
这段代码的核心思想是通过 DFS 遍历所有可能的字符大小写排列组合,并通过回溯来恢复现场,确保每个组合都是唯一的。通过剪枝优化,可以减少不必要的递归调用,提高代码效率。代码结构清晰,逻辑简单,适合处理这类排列组合问题。
九、526. 优美的排列 - 力扣(LeetCode)
算法代码:
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; // 恢复现场
}
}
}
};
这段代码的目的是计算给定整数 n
的所有优美排列的数量。优美排列的定义是:对于一个排列 nums
,如果对于每个位置 i
,nums[i]
满足 nums[i] % i == 0
或 i % nums[i] == 0
,那么这个排列就是优美的。例如,n = 2
,优美排列有 [1, 2]
和 [2, 1]
,输出 2
。
代码使用了深度优先搜索(DFS)和回溯的思想来遍历所有可能的排列,并通过剪枝优化减少不必要的递归调用。
代码思路解析
-
类的成员变量
-
check[16]
:一个布尔数组,用于记录数字1
到n
是否已经被使用。 -
ret
:用于记录优美排列的数量。
-
-
主函数
countArrangement
-
调用
dfs(1, n)
开始深度优先搜索,从位置1
开始。 -
返回结果
ret
。
-
-
DFS 函数
dfs
-
pos
:当前处理到的位置。 -
n
:排列的长度。 -
如果
pos == n + 1
,说明已经生成了一个完整的排列,且该排列是优美的,ret++
。 -
否则,遍历数字
1
到n
:-
如果数字
i
未被使用(!check[i]
),并且满足pos % i == 0
或i % pos == 0
,则选择该数字:-
将
check[i]
标记为true
,表示数字i
已被使用。 -
递归调用
dfs(pos + 1, n)
,处理下一个位置。 -
回溯时,将
check[i]
标记为false
,恢复现场。
-
-
-
代码优化建议
-
剪枝优化
-
在遍历数字
1
到n
时,可以通过条件pos % i == 0
或i % pos == 0
来限制选择范围,避免不必要的递归调用。
-
-
代码可读性
-
可以添加注释,解释每个步骤的作用,方便他人理解。
-
优化后的代码
class Solution {
bool check[16]; // 记录数字是否被使用
int ret; // 优美排列的数量
public:
int countArrangement(int n) {
dfs(1, n); // 从位置 1 开始深度优先搜索
return ret;
}
void dfs(int pos, int n) {
if (pos == n + 1) { // 已经生成了一个完整的排列
ret++; // 增加优美排列的数量
return;
}
for (int i = 1; i <= n; i++) { // 遍历数字 1 到 n
if (!check[i] && (pos % i == 0 || i % pos == 0)) { // 如果数字 i 未被使用且满足条件
check[i] = true; // 选择数字 i
dfs(pos + 1, n); // 递归处理下一个位置
check[i] = false; // 回溯,撤销选择
}
}
}
};
代码细节解析
-
check
数组的作用-
check[i]
用于记录数字i
是否已经被使用,避免重复选择。
-
-
剪枝条件
-
pos % i == 0
或i % pos == 0
:确保选择的数字i
满足优美排列的条件。
-
-
回溯的恢复现场
-
在递归调用结束后,通过
check[i] = false
恢复现场,确保数字i
可以被其他排列使用。
-
总结
这段代码的核心思想是通过 DFS 遍历所有可能的排列,并通过剪枝优化减少不必要的递归调用。通过 check
数组记录数字的使用情况,并通过条件 pos % i == 0
或 i % pos == 0
确保排列是优美的。代码结构清晰,逻辑简单,适合处理这类排列问题。
十、 51. N 皇后 - 力扣(LeetCode)
算法代码:
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;
}
}
}
};
这段代码的目的是解决 N 皇后问题,即在 n x n
的棋盘上放置 n
个皇后,使得它们互不攻击(即任意两个皇后不能在同一行、同一列或同一对角线上)。代码使用了深度优先搜索(DFS)和回溯的思想来遍历所有可能的放置方案。
代码思路解析
-
类的成员变量
-
checkCol[10]
:一个布尔数组,用于记录每一列是否已经被占用。 -
checkDig1[20]
:一个布尔数组,用于记录主对角线(从左上到右下)是否已经被占用。 -
checkDig2[20]
:一个布尔数组,用于记录副对角线(从右上到左下)是否已经被占用。 -
ret
:一个二维字符串向量,用于存储所有合法的棋盘布局。 -
path
:一个字符串向量,用于存储当前正在构建的棋盘布局。 -
n
:棋盘的大小(n x n
)。
-
-
主函数
solveNQueens
-
初始化
n
为输入的_n
。 -
初始化
path
为一个n x n
的棋盘,所有位置初始化为'.'
。 -
调用
dfs(0)
开始深度优先搜索,从第0
行开始。 -
返回结果
ret
。
-
-
DFS 函数
dfs
-
row
:当前处理到的行。 -
如果
row == n
,说明已经成功放置了n
个皇后,将当前棋盘布局path
加入ret
中。 -
否则,遍历当前行的每一列:
-
如果当前列
col
、主对角线row - col + n
和副对角线row + col
都没有被占用:-
在
path[row][col]
放置皇后'Q'
。 -
标记当前列、主对角线和副对角线为已占用。
-
递归调用
dfs(row + 1)
,处理下一行。 -
回溯时,移除刚刚放置的皇后,并恢复列、主对角线和副对角线的状态。
-
-
-
代码优化建议
-
剪枝优化
-
在遍历列时,通过
checkCol
、checkDig1
和checkDig2
数组快速判断当前位置是否可以放置皇后,避免不必要的递归调用。
-
-
代码可读性
-
可以添加注释,解释每个步骤的作用,方便他人理解。
-
优化后的代码
class Solution {
bool checkCol[10]; // 记录列是否被占用
bool checkDig1[20]; // 记录主对角线是否被占用
bool 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); // 从第 0 行开始深度优先搜索
return ret;
}
void dfs(int row) {
if (row == n) { // 已经成功放置了 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;
}
}
}
};
代码细节解析
-
checkCol
、checkDig1
、checkDig2
的作用-
checkCol[col]
:记录第col
列是否被占用。 -
checkDig1[row - col + n]
:记录主对角线是否被占用。row - col + n
是为了避免负数索引。 -
checkDig2[row + col]
:记录副对角线是否被占用。
-
-
放置皇后
-
在
path[row][col]
放置皇后'Q'
。 -
标记当前列、主对角线和副对角线为已占用。
-
-
回溯的恢复现场
-
移除刚刚放置的皇后
'Q'
,恢复为'.'
。 -
恢复列、主对角线和副对角线的状态。
-
总结
这段代码的核心思想是通过 DFS 遍历所有可能的皇后放置方案,并通过回溯来恢复现场,确保每个方案都是合法的。通过 checkCol
、checkDig1
和 checkDig2
数组快速判断当前位置是否可以放置皇后,避免不必要的递归调用。代码结构清晰,逻辑简单,适合解决 N 皇后问题。
十一、36. 有效的数独 - 力扣(LeetCode)
算法代码:
class Solution {
bool row[9][10] = {false}; // 记录每一行数字是否出现
bool col[9][10] = {false}; // 记录每一列数字是否出现
bool grid[3][3][10] = {false}; // 记录每一个3x3小宫格数字是否出现
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;
}
};
这个代码的思路是判断一个9x9的数独棋盘是否有效。数独的有效性规则是:
-
每一行必须包含数字1-9,且不能重复。
-
每一列必须包含数字1-9,且不能重复。
-
每一个3x3的小宫格必须包含数字1-9,且不能重复。
代码思路解析
-
数据结构
-
row[9][10]
:用于记录每一行中数字1-9是否已经出现过。row[i][num]
表示第i
行中数字num
是否已经出现过。 -
col[9][10]
:用于记录每一列中数字1-9是否已经出现过。col[j][num]
表示第j
列中数字num
是否已经出现过。 -
grid[3][3][10]
:用于记录每一个3x3的小宫格中数字1-9是否已经出现过。grid[i/3][j/3][num]
表示第(i/3, j/3)
个小宫格中数字num
是否已经出现过。
-
-
遍历棋盘
-
使用双重循环遍历棋盘的每一个格子
(i, j)
。 -
如果当前格子不是空的(即
board[i][j] != '.'
),则提取出数字num = board[i][j] - '0'
。
-
-
检查有效性
-
检查当前数字
num
是否在当前行、当前列或当前3x3小宫格中已经出现过。如果出现过,则数独无效,返回false
。 -
如果没有出现过,则在
row
、col
和grid
中标记该数字已经出现过。
-
-
返回结果
-
如果遍历完整个棋盘都没有发现重复的数字,则数独有效,返回
true
。
-
代码优化建议
-
代码已经非常简洁高效,时间复杂度为O(1),因为数独棋盘的大小是固定的9x9。
-
代码的空间复杂度也是O(1),因为使用的辅助空间是固定的。
总结:
这个代码通过使用三个辅助数组来记录每一行、每一列和每一个3x3小宫格中数字的出现情况,从而高效地判断数独棋盘的有效性。代码逻辑清晰,实现简洁,适合解决数独有效性问题。
十二、 37. 解数独 - 力扣(LeetCode)
算法代码:
class Solution {
bool row[9][10] = {false}; // 记录每一行数字是否出现
bool col[9][10] = {false}; // 记录每一列数字是否出现
bool grid[3][3][10] = {false}; // 记录每一个3x3小宫格数字是否出现
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] == '.') {
// 尝试填入数字1-9
for (int num = 1; num <= 9; num++) {
if (!row[i][num] && !col[j][num] &&
!grid[i / 3][j / 3][num]) {
board[i][j] = '0' + num;
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; // 所有格子都填满,找到解
}
};
这个代码的思路是解决一个9x9的数独问题。数独的规则是:
-
每一行必须包含数字1-9,且不能重复。
-
每一列必须包含数字1-9,且不能重复。
-
每一个3x3的小宫格必须包含数字1-9,且不能重复。
代码思路解析
-
数据结构
-
row[9][10]
:用于记录每一行中数字1-9是否已经出现过。row[i][num]
表示第i
行中数字num
是否已经出现过。 -
col[9][10]
:用于记录每一列中数字1-9是否已经出现过。col[j][num]
表示第j
列中数字num
是否已经出现过。 -
grid[3][3][10]
:用于记录每一个3x3的小宫格中数字1-9是否已经出现过。grid[i/3][j/3][num]
表示第(i/3, j/3)
个小宫格中数字num
是否已经出现过。
-
-
初始化
-
在
solveSudoku
函数中,首先遍历整个棋盘,初始化row
、col
和grid
数组,记录已经填入的数字。
-
-
深度优先搜索(DFS)
-
dfs
函数通过递归尝试填充每一个空格子。 -
对于每一个空格子
(i, j)
,尝试填入数字1-9。 -
如果填入的数字
num
在当前行、当前列和当前3x3小宫格中都没有出现过,则填入该数字,并更新row
、col
和grid
数组。 -
递归调用
dfs
继续填充下一个空格子。 -
如果递归调用返回
true
,表示找到了一个有效的解,直接返回true
。 -
如果递归调用返回
false
,表示当前填入的数字num
无效,需要恢复现场(即回溯),尝试下一个数字。
-
-
回溯
-
如果所有数字1-9都尝试过,仍然没有找到有效的解,则返回
false
,表示当前路径无效,需要回溯到上一步。
-
-
终止条件
-
如果所有格子都填满,并且没有冲突,则返回
true
,表示找到了一个有效的解。
-
代码优化建议
-
代码已经非常简洁高效,通过回溯法系统地尝试所有可能的数字组合,直到找到一个有效的解。
-
代码的时间复杂度较高,因为需要尝试所有可能的组合,但在实际应用中,由于数独问题的约束条件较强,通常可以在合理的时间内找到解。
总结
这个代码通过深度优先搜索(DFS)和回溯法系统地尝试所有可能的数字组合,解决了数独问题。代码逻辑清晰,实现简洁,适合解决数独问题。通过递归和回溯,代码能够高效地找到数独的有效解。
十三、 79. 单词搜索 - 力扣(LeetCode)
算法代码:
class Solution {
bool vis[7][7] = {false}; // 记录每个单元格是否被访问过
int m, n; // 网格的行数和列数
int dx[4] = {0, 0, -1, 1}; // 四个方向的x偏移量
int dy[4] = {1, -1, 0, 0}; // 四个方向的y偏移量
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; // 未找到匹配路径
}
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[x][y] = true; // 标记当前单元格为已访问
if (dfs(board, x, y, word, pos + 1)) {
return true; // 找到匹配路径
}
vis[x][y] = false; // 回溯
}
}
return false; // 当前路径无效
}
};
这个代码的思路是解决一个经典的单词搜索问题:在一个二维字符网格中,判断是否存在一条路径,使得路径上的字符按顺序连接起来恰好等于给定的单词。路径可以是水平或垂直相邻的单元格,且每个单元格只能使用一次。
代码思路解析
-
数据结构
-
vis[7][7]
:用于记录网格中的每个单元格是否已经被访问过。vis[i][j]
表示第i
行第j
列的单元格是否已经被访问。 -
m
和n
:分别表示网格的行数和列数。 -
dx[4]
和dy[4]
:用于表示四个方向(上、下、左、右)的偏移量。
-
-
主函数
exist
-
遍历整个网格,寻找与单词的第一个字符匹配的单元格。
-
如果找到匹配的单元格,则标记该单元格为已访问,并调用深度优先搜索(DFS)函数
dfs
继续匹配剩余的字符。 -
如果
dfs
返回true
,表示找到了匹配的路径,直接返回true
。 -
如果遍历完所有可能的起始单元格都没有找到匹配的路径,则返回
false
。
-
-
深度优先搜索(DFS)函数
dfs
-
如果当前匹配的位置
pos
等于单词的长度,说明已经成功匹配了整个单词,返回true
。 -
否则,尝试从当前单元格向四个方向(上、下、左、右)扩展,寻找下一个匹配的字符。
-
如果扩展的单元格在网格范围内、未被访问过,并且字符与单词的下一个字符匹配,则标记该单元格为已访问,并递归调用
dfs
。 -
如果递归调用返回
true
,表示找到了匹配的路径,直接返回true
。 -
如果递归调用返回
false
,表示当前路径无效,需要回溯(即恢复现场),尝试其他方向。
-
-
回溯
-
在递归调用返回
false
后,需要将当前单元格的访问状态恢复为未访问,以便其他路径可以尝试使用该单元格。
-
代码优化建议
-
代码已经非常简洁高效,通过深度优先搜索(DFS)和回溯法系统地尝试所有可能的路径。
-
代码的时间复杂度较高,因为需要尝试所有可能的路径,但在实际应用中,由于单词的长度和网格的大小有限,通常可以在合理的时间内找到解。
总结
这个代码通过深度优先搜索(DFS)和回溯法系统地尝试所有可能的路径,解决了单词搜索问题。代码逻辑清晰,实现简洁,适合解决类似的二维网格搜索问题。通过递归和回溯,代码能够高效地找到匹配的路径。
十四、 1219. 黄金矿工 - 力扣(LeetCode)
算法代码:
class Solution {
bool vis[16][16] = {false}; // 记录每个单元格是否被访问过
int dx[4] = {0, 0, 1, -1}; // 四个方向的x偏移量
int dy[4] = {1, -1, 0, 0}; // 四个方向的y偏移量
int m, n; // 网格的行数和列数
int ret = 0; // 记录最大黄金总量
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]); // 开始DFS
vis[i][j] = false; // 回溯
}
}
}
return ret; // 返回最大黄金总量
}
void dfs(vector<vector<int>>& g, int i, int j, int path) {
ret = max(ret, path); // 更新最大黄金总量
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]); // 递归DFS
vis[x][y] = false; // 回溯
}
}
}
};
这个代码的思路是解决一个黄金矿工问题:在一个二维网格中,每个单元格包含一定数量的黄金(用非负整数表示),矿工可以从任意一个非零单元格出发,向上下左右四个方向移动,收集黄金。矿工不能进入黄金数量为0的单元格,也不能重复访问同一个单元格。目标是找到一条路径,使得矿工收集的黄金总量最大。
代码思路解析
-
数据结构
-
vis[16][16]
:用于记录网格中的每个单元格是否已经被访问过。vis[i][j]
表示第i
行第j
列的单元格是否已经被访问。 -
dx[4]
和dy[4]
:用于表示四个方向(上、下、左、右)的偏移量。 -
m
和n
:分别表示网格的行数和列数。 -
ret
:用于记录当前找到的最大黄金总量。
-
-
主函数
getMaximumGold
-
遍历整个网格,寻找所有非零的单元格作为起点。
-
对于每一个非零的单元格,标记该单元格为已访问,并调用深度优先搜索(DFS)函数
dfs
开始收集黄金。 -
在
dfs
调用结束后,恢复该单元格的访问状态(回溯),以便其他路径可以尝试使用该单元格。 -
最终返回
ret
,即找到的最大黄金总量。
-
-
深度优先搜索(DFS)函数
dfs
-
更新当前路径的黄金总量
path
,并与ret
比较,更新ret
为较大值。 -
尝试从当前单元格向四个方向(上、下、左、右)扩展,寻找下一个可以收集黄金的单元格。
-
如果扩展的单元格在网格范围内、未被访问过,并且黄金数量不为0,则标记该单元格为已访问,并递归调用
dfs
。 -
在递归调用结束后,恢复该单元格的访问状态(回溯),以便其他路径可以尝试使用该单元格。
-
-
回溯
-
在递归调用结束后,需要将当前单元格的访问状态恢复为未访问,以便其他路径可以尝试使用该单元格。
-
代码优化建议
-
代码已经非常简洁高效,通过深度优先搜索(DFS)和回溯法系统地尝试所有可能的路径。
-
代码的时间复杂度较高,因为需要尝试所有可能的路径,但在实际应用中,由于网格的大小和黄金数量的限制,通常可以在合理的时间内找到解。
总结
这个代码通过深度优先搜索(DFS)和回溯法系统地尝试所有可能的路径,解决了黄金矿工问题。代码逻辑清晰,实现简洁,适合解决类似的二维网格搜索问题。通过递归和回溯,代码能够高效地找到收集黄金的最大路径。
十五、980. 不同路径 III - 力扣(LeetCode)
算法代码:
class Solution {
bool vis[21][21] = {false}; // 记录每个单元格是否被访问过
int dx[4] = {1, -1, 0, 0}; // 四个方向的x偏移量
int dy[4] = {0, 0, 1, -1}; // 四个方向的y偏移量
int ret = 0; // 记录满足条件的路径数量
int m, n, step; // 网格的行数、列数和需要经过的总步数
public:
int uniquePathsIII(vector<vector<int>>& grid) {
m = grid.size(), n = grid[0].size();
int bx = 0, by = 0; // 起点的位置
step = 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); // 开始DFS
return ret; // 返回满足条件的路径数量
}
void dfs(vector<vector<int>>& grid, int i, int j, int count) {
if (grid[i][j] == 2) { // 如果当前单元格是终点
if (count == step) { // 判断是否经过所有空单元格
ret++; // 满足条件,路径数量加1
}
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); // 递归DFS
vis[x][y] = false; // 回溯
}
}
}
};
这个代码的思路是解决一个独特路径问题 III:在一个二维网格中,每个单元格可能包含以下值:
-
0
:表示空单元格,可以通过。 -
1
:表示起点。 -
2
:表示终点。 -
-1
:表示障碍物,不能通过。
目标是找到从起点到终点的所有路径,要求路径必须经过所有的空单元格(0
),并且每个单元格只能访问一次。
代码思路解析
-
数据结构
-
vis[21][21]
:用于记录网格中的每个单元格是否已经被访问过。vis[i][j]
表示第i
行第j
列的单元格是否已经被访问。 -
dx[4]
和dy[4]
:用于表示四个方向(上、下、左、右)的偏移量。 -
m
和n
:分别表示网格的行数和列数。 -
step
:表示需要经过的空单元格数量(包括起点和终点)。 -
ret
:用于记录满足条件的路径数量。
-
-
主函数
uniquePathsIII
-
遍历整个网格,统计空单元格的数量(
0
),并找到起点的位置(1
)。 -
将需要经过的总步数
step
设置为空单元格数量加2(包括起点和终点)。 -
标记起点为已访问,并调用深度优先搜索(DFS)函数
dfs
开始搜索路径。 -
最终返回
ret
,即满足条件的路径数量。
-
-
深度优先搜索(DFS)函数
dfs
-
如果当前单元格是终点(
2
),并且已经经过的步数count
等于step
,则说明找到了一条合法路径,ret
加1。 -
否则,尝试从当前单元格向四个方向(上、下、左、右)扩展,寻找下一个可以访问的单元格。
-
如果扩展的单元格在网格范围内、未被访问过,并且不是障碍物(
-1
),则标记该单元格为已访问,并递归调用dfs
。 -
在递归调用结束后,恢复该单元格的访问状态(回溯),以便其他路径可以尝试使用该单元格。
-
-
回溯
-
在递归调用结束后,需要将当前单元格的访问状态恢复为未访问,以便其他路径可以尝试使用该单元格。
-
代码优化建议
-
代码已经非常简洁高效,通过深度优先搜索(DFS)和回溯法系统地尝试所有可能的路径。
-
代码的时间复杂度较高,因为需要尝试所有可能的路径,但在实际应用中,由于网格的大小和路径的限制,通常可以在合理的时间内找到解。
总结
这个代码通过深度优先搜索(DFS)和回溯法系统地尝试所有可能的路径,解决了独特路径问题 III。代码逻辑清晰,实现简洁,适合解决类似的二维网格搜索问题。通过递归和回溯,代码能够高效地找到满足条件的路径数量。