目录
一、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。代码逻辑清晰,实现简洁,适合解决类似的二维网格搜索问题。通过递归和回溯,代码能够高效地找到满足条件的路径数量。






![How to use VRX on ubuntu20.04 with ROS1 Noetic?[2]](https://i-blog.csdnimg.cn/direct/dc3aa39b85a147228f94f2cebe078903.png)












