综合练习 —— 递归、搜索与回溯算法

server/2025/3/1 16:53:25/

目录

一、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;  // 初始化 pathsum = 0;   // 初始化 sumdfs(nums, 0);  // 从第 0 个元素开始 DFSreturn sum;}void dfs(vector<int>& nums, int pos) {sum += path;  // 将当前子集的异或和累加到 sumfor (int i = pos; i < nums.size(); i++) {path ^= nums[i];  // 将 nums[i] 加入当前子集,更新 pathdfs(nums, i + 1);  // 递归处理下一个元素path ^= nums[i];   // 回溯,恢复 path 的值}}
};

 代码思路

  1. 问题分析

    • 给定一个数组 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

  2. 核心思想

    • 使用深度优先搜索(DFS)遍历所有可能的子集。

    • 在 DFS 过程中,维护一个变量 path,表示当前子集的异或和。

    • 每次递归时,将当前子集的异或和 path 累加到 sum 中。

    • 通过回溯恢复现场,确保 path 的正确性。

  3. 实现细节

    • path:表示当前子集的异或和。

    • sum:表示所有子集异或和的总和。

    • dfs 函数:

      • 将当前 path 的值累加到 sum

      • 遍历数组 nums,从当前位置 pos 开始,逐个尝试将元素加入当前子集。

      • 递归调用 dfs,继续处理下一个元素。

      • 回溯时,恢复 path 的值(通过再次异或当前元素)。


代码解析

  1. 初始化

    • path 和 sum 初始化为 0。

  2. DFS 函数

    • 累加当前子集的异或和sum += path

    • 遍历数组

      • 将当前元素 nums[i] 异或到 path 中,表示将其加入当前子集。

      • 递归调用 dfs,处理下一个元素。

      • 回溯时,再次异或 nums[i],恢复 path 的值。

  3. 时间复杂度

    • 由于每个元素有两种选择(加入或不加入子集),总共有 2n 个子集,因此时间复杂度为 O(2n)。

  4. 空间复杂度

    • 递归栈的深度为 O(n),因此空间复杂度为 O(n)。


示例运行

输入

nums = [1, 2, 3];

运行过程

  1. 初始状态:path = 0sum = 0

  2. DFS 遍历所有子集:

    • []path = 0sum += 0

    • [1]path = 1sum += 1

    • [1, 2]path = 3sum += 3

    • [1, 2, 3]path = 0sum += 0

    • [1, 3]path = 2sum += 2

    • [2]path = 2sum += 2

    • [2, 3]path = 1sum += 1

    • [3]path = 3sum += 3

  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 个位置开始 DFSreturn 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();          // 回溯,恢复 pathcheck[i] = false;        // 回溯,恢复 check}}}
};

代码思路

  1. 问题分析

    • 给定一个可能包含重复元素的数组 nums,需要生成所有不重复的全排列。

    • 例如,nums = [1, 1, 2],不重复的全排列为 [[1,1,2], [1,2,1], [2,1,1]]

  2. 核心思想

    • 使用深度优先搜索(DFS)生成所有可能的排列。

    • 通过排序和剪枝,避免生成重复的排列。

    • 使用一个布尔数组 check 记录哪些元素已经被使用过。

  3. 实现细节

    • path:记录当前生成的排列。

    • ret:存储所有不重复的排列。

    • check:记录每个元素是否已经被使用。

    • dfs 函数:

      • 如果当前排列的长度等于 nums 的长度,将其加入 ret

      • 遍历数组 nums,尝试将未使用的元素加入当前排列。

      • 通过剪枝条件避免重复排列。

      • 回溯时恢复现场,确保 path 和 check 的正确性。


代码解析

  1. 排序

    • 对数组 nums 进行排序,使得相同的元素相邻,方便剪枝。

  2. DFS 函数

    • 终止条件:如果当前排列的长度等于 nums 的长度,将其加入 ret

    • 遍历数组

      • 如果当前元素未被使用(check[i] == false),并且满足剪枝条件,则将其加入当前排列。

      • 递归调用 dfs,处理下一个位置。

      • 回溯时,恢复 path 和 check 的值。

  3. 剪枝条件

    • i == 0:第一个元素无需判断是否重复。

    • nums[i] != nums[i - 1]:当前元素与前一个元素不同,无需剪枝。

    • check[i - 1] != false:前一个相同元素已被使用,说明当前元素是新的排列起点。

  4. 时间复杂度

    • 最坏情况下,所有排列都不重复,时间复杂度为 O(n×n!)O(n×n!),其中 n!n! 是排列数,nn 是生成每个排列的时间。

  5. 空间复杂度

    • 递归栈的深度为 O(n)O(n),结果集 ret 的空间为 O(n×n!)O(n×n!)。


示例运行

输入

nums = [1, 1, 2];

运行过程

  1. 排序后:nums = [1, 1, 2]

  2. DFS 遍历:

    • 生成 [1, 1, 2]

    • 生成 [1, 2, 1]

    • 生成 [2, 1, 1]

  3. 最终结果: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),并且满足以下条件之一:

      1. 当前元素是第一个元素(i == 0)。

      2. 当前元素与前一个元素不同(nums[i] != nums[i - 1])。

      3. 前一个相同元素已经被使用过(check[i - 1] != false)。

    • 则继续递归,否则跳过。

  • 为什么这样写

    • 通过排序,相同的元素会相邻。

    • 对于相同的元素,只有当前一个相同元素已经被使用过时,才允许使用当前元素。

    • 这样可以避免生成重复的排列。


示例说明

输入:
nums = [1, 1, 2];
排序后:
nums = [1, 1, 2];
运行过程:
  1. 第一次递归

    • 选择 nums[0] = 1check[0] = true

    • 进入下一层递归。

  2. 第二次递归

    • 选择 nums[1] = 1,此时 nums[1] == nums[0],但 check[0] == true,因此允许选择。

    • check[1] = true

    • 进入下一层递归。

  3. 第三次递归

    • 选择 nums[2] = 2check[2] = true

    • 生成排列 [1, 1, 2]

  4. 回溯

    • 恢复 check[2] = false

    • 恢复 check[1] = false

    • 恢复 check[0] = false

  5. 第二次递归(另一种选择)

    • 选择 nums[2] = 2check[2] = true

    • 进入下一层递归。

  6. 第三次递归

    • 选择 nums[0] = 1check[0] = true

    • 生成排列 [1, 2, 1]

  7. 回溯

    • 恢复 check[0] = false

    • 恢复 check[2] = false

  8. 继续递归

    • 类似过程生成 [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)来遍历所有可能的组合。

代码思路解析

  1. 类的成员变量

    • hash[10]:一个字符串数组,存储了每个数字对应的字母。例如,2 对应 "abc"3 对应 "def",依此类推。

    • path:一个字符串,用于存储当前正在构建的字母组合。

    • ret:一个字符串向量,用于存储所有可能的字母组合。

  2. 主函数 letterCombinations

    • 首先检查输入字符串 digits 是否为空。如果为空,直接返回空的 ret

    • 否则,调用 dfs 函数开始深度优先搜索。

  3. DFS 函数 dfs

    • pos 参数表示当前处理到 digits 中的第几个字符。

    • 如果 pos 等于 digits 的长度,说明已经处理完所有字符,当前的 path 就是一个完整的字母组合,将其加入 ret 中。

    • 否则,遍历当前数字对应的所有字母(通过 hash[digits[pos] - '0'] 获取),并将每个字母依次加入 path 中,然后递归调用 dfs 处理下一个字符。

    • 递归调用结束后,通过 path.pop_back() 恢复现场,以便尝试下一个字母。

代码优化建议

  1. 参数传递优化

    • digits 和 path 可以通过引用传递,避免不必要的拷贝。

  2. 提前终止

    • 如果 digits 为空,可以直接返回空结果,不需要进入 DFS。

  3. 代码可读性

    • 可以添加注释,解释每个步骤的作用,方便他人理解。

优化后的代码

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)和回溯的思想来遍历所有可能的组合。

代码思路解析

  1. 类的成员变量

    • left:记录当前路径中左括号的数量。

    • right:记录当前路径中右括号的数量。

    • n:表示需要生成的括号对数。

    • path:一个字符串,用于存储当前正在构建的括号组合。

    • ret:一个字符串向量,用于存储所有有效的括号组合。

  2. 主函数 generateParenthesis

    • 初始化 n 为输入的 _n

    • 调用 dfs 函数开始深度优先搜索。

    • 返回结果 ret

  3. DFS 函数 dfs

    • 如果 right == n,说明当前路径中的右括号数量已经达到 n,即已经生成了一个有效的括号组合,将其加入 ret 中。

    • 如果 left < n,说明还可以添加左括号:

      • 将左括号 ( 加入 path 中。

      • 增加 left 的计数。

      • 递归调用 dfs

      • 回溯时,移除刚刚添加的左括号,并减少 left 的计数。

    • 如果 right < left,说明可以添加右括号:

      • 将右括号 ) 加入 path 中。

      • 增加 right 的计数。

      • 递归调用 dfs

      • 回溯时,移除刚刚添加的右括号,并减少 right 的计数。

代码优化建议

  1. 参数传递优化

    • path 可以通过引用传递,避免不必要的拷贝。

  2. 提前终止

    • 如果 left 或 right 超过 n,可以直接返回,避免无效的递归调用。

  3. 代码可读性

    • 可以添加注释,解释每个步骤的作用,方便他人理解。

优化后的代码

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 = 4k = 2 时,输出 [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]。代码使用了深度优先搜索(DFS)和回溯的思想来遍历所有可能的组合。


代码思路解析

  1. 类的成员变量

    • path:一个整数向量,用于存储当前正在构建的组合。

    • ret:一个二维整数向量,用于存储所有可能的组合。

    • n:表示数字范围的上限(从 1 到 n)。

    • k:表示每个组合中数字的个数。

  2. 主函数 combine

    • 初始化 n 和 k 为输入的 _n 和 _k

    • 调用 dfs(1) 开始深度优先搜索,从数字 1 开始。

    • 返回结果 ret

  3. DFS 函数 dfs

    • start 参数表示当前可以选择的起始数字。

    • 如果 path.size() == k,说明当前路径中的数字数量已经达到 k,即已经生成了一个有效的组合,将其加入 ret 中。

    • 否则,从 start 开始遍历到 n

      • 将当前数字 i 加入 path 中。

      • 递归调用 dfs(i + 1),确保下一个数字比当前数字大,避免重复组合。

      • 回溯时,移除刚刚添加的数字 i,恢复现场。


代码优化建议

  1. 剪枝优化

    • 在遍历时,如果剩余的数字不足以填满 k 个数的组合,可以直接跳过。例如,当 path.size() + (n - i + 1) < k 时,可以直接 break

  2. 参数传递优化

    • path 可以通过引用传递,避免不必要的拷贝。

  3. 代码可读性

    • 可以添加注释,解释每个步骤的作用,方便他人理解。


优化后的代码

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(); // 回溯,撤销选择}}
};

优化点详解

  1. 剪枝优化

    • 在 for 循环中,i 的上限从 n 改为 n - (k - path.size()) + 1

    • 这是因为如果剩余的数字不足以填满 k 个数的组合,继续遍历是没有意义的。

    • 例如,当 n = 4k = 2,且 path.size() = 1 时,i 的最大值应该是 3(因为 4 只能和 5 组合,但 5 超出了范围)。

  2. 回溯的恢复现场

    • 在递归调用结束后,通过 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)来遍历所有可能的加减组合,并通过回溯的方式统计满足条件的组合数量。


代码思路解析

  1. 类的成员变量

    • ret:用于记录满足条件的组合数量。

    • aim:存储目标值 target

  2. 主函数 findTargetSumWays

    • 初始化 aim 为 target

    • 调用 dfs(nums, 0, 0) 开始深度优先搜索,从数组的第一个位置(pos = 0)和初始路径和(path = 0)开始。

    • 返回结果 ret

  3. 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; // 初始化 pathdfs(nums, 0);return ret;}void dfs(vector<int>& nums, int pos) {if (pos == nums.size()) {if (path == aim) {ret++;}return;}// 加法path += nums[pos]; // 修改全局变量 pathdfs(nums, pos + 1);path -= nums[pos]; // 恢复现场// 减法path -= nums[pos]; // 修改全局变量 pathdfs(nums, pos + 1);path += nums[pos]; // 恢复现场}
};

把 path 改为全局变量时会超时,为什么呢?

如果将 path 改为全局变量(即类的成员变量),代码可能会超时,原因如下:

  1. 回溯的恢复现场问题

    • 在当前的实现中,path 是通过参数传递的,每次递归调用都会创建一个新的 path 值。这样,在回溯时不需要手动恢复 path 的状态。

    • 如果将 path 改为全局变量,每次递归调用都会修改同一个 path 变量。在回溯时,必须手动恢复 path 的状态(例如,path -= nums[pos] 或 path += nums[pos]),否则会导致状态混乱。

  2. 递归调用栈的深度

    • 如果 path 是全局变量,每次递归调用都会修改同一个变量,这会导致递归调用栈的深度增加,从而增加时间和空间复杂度。

    • 而通过参数传递 path,每次递归调用都会创建一个新的 path 值,递归调用栈的深度不会增加。

  3. 多线程问题

    • 如果代码在多线程环境中运行,全局变量 path 可能会导致线程安全问题。而通过参数传递 path,每个线程可以维护自己的状态。

  4. 代码可读性和调试难度

    • 使用全局变量 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(); // 回溯,撤销选择}}
};

代码思路解析

  1. 类的成员变量

    • ret:用于存储所有满足条件的组合。

    • path:用于存储当前正在构建的组合。

  2. 主函数 combinationSum

    • 调用 dfs(nums, target, 0) 开始深度优先搜索,从数组的第一个位置开始。

  3. 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)和回溯的思想来遍历所有可能的组合,并通过剪枝优化减少不必要的递归调用。


代码思路解析

  1. 类的成员变量

    • aim:存储目标值 target

    • path:一个整数向量,用于存储当前正在构建的组合。

    • ret:一个二维整数向量,用于存储所有满足条件的组合。

  2. 主函数 combinationSum

    • 初始化 aim 为 target

    • 调用 dfs(nums, 0, 0) 开始深度优先搜索,从数组的第一个位置(pos = 0)和初始和(sum = 0)开始。

    • 返回结果 ret

  3. 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 中移除。


代码优化建议

  1. 剪枝优化

    • 在枚举 k 时,可以通过 k * nums[pos] + sum <= aim 来限制 k 的最大值,避免不必要的递归调用。

  2. 恢复现场的优化

    • 在回溯时,可以通过一个循环将 nums[pos] 从 path 中移除,确保 path 恢复到之前的状态。

  3. 代码可读性

    • 可以添加注释,解释每个步骤的作用,方便他人理解。


优化后的代码

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();}}
};

代码细节解析

  1. 枚举 k 的作用

    • k 表示当前数字 nums[pos] 的使用次数。

    • 通过 k * nums[pos] + sum <= aim 限制 k 的最大值,避免不必要的递归调用。

  2. 恢复现场

    • 在回溯时,通过一个循环将 nums[pos] 从 path 中移除,确保 path 恢复到之前的状态。

  3. 递归调用

    • 每次递归调用都会处理下一个数字(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;elsech += 32;return ch;}
};

        这段代码的目的是生成给定字符串 s 的所有可能的字母大小写排列组合。例如,输入 s = "a1b2",输出 ["a1b2","a1B2","A1b2","A1B2"]。代码使用了深度优先搜索(DFS)和回溯的思想来遍历所有可能的组合。


代码思路解析

  1. 类的成员变量

    • path:一个字符串,用于存储当前正在构建的排列组合。

    • ret:一个字符串向量,用于存储所有可能的排列组合。

  2. 主函数 letterCasePermutation

    • 调用 dfs(s, 0) 开始深度优先搜索,从字符串的第一个字符(pos = 0)开始。

    • 返回结果 ret

  3. 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),处理下一个字符。

        • 回溯时,移除刚刚添加的字符,恢复现场。

  4. 辅助函数 change

    • 如果字符是小写字母(a-z),将其转换为大写字母。

    • 如果字符是大写字母(A-Z),将其转换为小写字母。

    • 返回改变后的字符。


代码优化建议

  1. 减少函数调用

    • 可以将 change 函数的逻辑直接嵌入到 dfs 函数中,减少函数调用的开销。

  2. 剪枝优化

    • 如果当前字符是数字,直接跳过大小写转换的逻辑,避免不必要的递归调用。

  3. 代码可读性

    • 可以添加注释,解释每个步骤的作用,方便他人理解。


优化后的代码

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(); // 回溯,撤销选择}}
};

代码细节解析

  1. 不改变字符

    • 将当前字符 ch 加入 path 中。

    • 递归处理下一个字符。

    • 回溯时,移除刚刚添加的字符 ch

  2. 改变字符大小写

    • 如果当前字符是字母,调用 change 函数改变其大小写。

    • 将改变后的字符加入 path 中。

    • 递归处理下一个字符。

    • 回溯时,移除刚刚添加的字符。

  3. 剪枝优化

    • 如果当前字符是数字,直接跳过大小写转换的逻辑,避免不必要的递归调用。


总结

        这段代码的核心思想是通过 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,如果对于每个位置 inums[i] 满足 nums[i] % i == 0 或 i % nums[i] == 0,那么这个排列就是优美的。例如,n = 2,优美排列有 [1, 2] 和 [2, 1],输出 2

代码使用了深度优先搜索(DFS)和回溯的思想来遍历所有可能的排列,并通过剪枝优化减少不必要的递归调用。


代码思路解析

  1. 类的成员变量

    • check[16]:一个布尔数组,用于记录数字 1 到 n 是否已经被使用。

    • ret:用于记录优美排列的数量。

  2. 主函数 countArrangement

    • 调用 dfs(1, n) 开始深度优先搜索,从位置 1 开始。

    • 返回结果 ret

  3. 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. 剪枝优化

    • 在遍历数字 1 到 n 时,可以通过条件 pos % i == 0 或 i % pos == 0 来限制选择范围,避免不必要的递归调用。

  2. 代码可读性

    • 可以添加注释,解释每个步骤的作用,方便他人理解。


优化后的代码

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 到 nif (!check[i] && (pos % i == 0 || i % pos == 0)) { // 如果数字 i 未被使用且满足条件check[i] = true; // 选择数字 idfs(pos + 1, n); // 递归处理下一个位置check[i] = false; // 回溯,撤销选择}}}
};

代码细节解析

  1. check 数组的作用

    • check[i] 用于记录数字 i 是否已经被使用,避免重复选择。

  2. 剪枝条件

    • pos % i == 0 或 i % pos == 0:确保选择的数字 i 满足优美排列的条件。

  3. 回溯的恢复现场

    • 在递归调用结束后,通过 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)和回溯的思想来遍历所有可能的放置方案。


代码思路解析

  1. 类的成员变量

    • checkCol[10]:一个布尔数组,用于记录每一列是否已经被占用。

    • checkDig1[20]:一个布尔数组,用于记录主对角线(从左上到右下)是否已经被占用。

    • checkDig2[20]:一个布尔数组,用于记录副对角线(从右上到左下)是否已经被占用。

    • ret:一个二维字符串向量,用于存储所有合法的棋盘布局。

    • path:一个字符串向量,用于存储当前正在构建的棋盘布局。

    • n:棋盘的大小(n x n)。

  2. 主函数 solveNQueens

    • 初始化 n 为输入的 _n

    • 初始化 path 为一个 n x n 的棋盘,所有位置初始化为 '.'

    • 调用 dfs(0) 开始深度优先搜索,从第 0 行开始。

    • 返回结果 ret

  3. DFS 函数 dfs

    • row:当前处理到的行。

    • 如果 row == n,说明已经成功放置了 n 个皇后,将当前棋盘布局 path 加入 ret 中。

    • 否则,遍历当前行的每一列:

      • 如果当前列 col、主对角线 row - col + n 和副对角线 row + col 都没有被占用:

        • 在 path[row][col] 放置皇后 'Q'

        • 标记当前列、主对角线和副对角线为已占用。

        • 递归调用 dfs(row + 1),处理下一行。

        • 回溯时,移除刚刚放置的皇后,并恢复列、主对角线和副对角线的状态。


代码优化建议

  1. 剪枝优化

    • 在遍历列时,通过 checkColcheckDig1 和 checkDig2 数组快速判断当前位置是否可以放置皇后,避免不必要的递归调用。

  2. 代码可读性

    • 可以添加注释,解释每个步骤的作用,方便他人理解。


优化后的代码

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;}}}
};

代码细节解析

  1. checkColcheckDig1checkDig2 的作用

    • checkCol[col]:记录第 col 列是否被占用。

    • checkDig1[row - col + n]:记录主对角线是否被占用。row - col + n 是为了避免负数索引。

    • checkDig2[row + col]:记录副对角线是否被占用。

  2. 放置皇后

    • 在 path[row][col] 放置皇后 'Q'

    • 标记当前列、主对角线和副对角线为已占用。

  3. 回溯的恢复现场

    • 移除刚刚放置的皇后 'Q',恢复为 '.'

    • 恢复列、主对角线和副对角线的状态。


总结

        这段代码的核心思想是通过 DFS 遍历所有可能的皇后放置方案,并通过回溯来恢复现场,确保每个方案都是合法的。通过 checkColcheckDig1 和 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. 每一行必须包含数字1-9,且不能重复。

  2. 每一列必须包含数字1-9,且不能重复。

  3. 每一个3x3的小宫格必须包含数字1-9,且不能重复。

代码思路解析

  1. 数据结构

    • 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是否已经出现过。

  2. 遍历棋盘

    • 使用双重循环遍历棋盘的每一个格子(i, j)

    • 如果当前格子不是空的(即board[i][j] != '.'),则提取出数字num = board[i][j] - '0'

  3. 检查有效性

    • 检查当前数字num是否在当前行、当前列或当前3x3小宫格中已经出现过。如果出现过,则数独无效,返回false

    • 如果没有出现过,则在rowcolgrid中标记该数字已经出现过。

  4. 返回结果

    • 如果遍历完整个棋盘都没有发现重复的数字,则数独有效,返回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-9for (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. 每一行必须包含数字1-9,且不能重复。

  2. 每一列必须包含数字1-9,且不能重复。

  3. 每一个3x3的小宫格必须包含数字1-9,且不能重复。

代码思路解析

  1. 数据结构

    • 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是否已经出现过。

  2. 初始化

    • solveSudoku函数中,首先遍历整个棋盘,初始化rowcolgrid数组,记录已经填入的数字。

  3. 深度优先搜索(DFS)

    • dfs函数通过递归尝试填充每一个空格子。

    • 对于每一个空格子(i, j),尝试填入数字1-9。

    • 如果填入的数字num在当前行、当前列和当前3x3小宫格中都没有出现过,则填入该数字,并更新rowcolgrid数组。

    • 递归调用dfs继续填充下一个空格子。

    • 如果递归调用返回true,表示找到了一个有效的解,直接返回true

    • 如果递归调用返回false,表示当前填入的数字num无效,需要恢复现场(即回溯),尝试下一个数字。

  4. 回溯

    • 如果所有数字1-9都尝试过,仍然没有找到有效的解,则返回false,表示当前路径无效,需要回溯到上一步。

  5. 终止条件

    • 如果所有格子都填满,并且没有冲突,则返回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;  // 当前路径无效}
};

        这个代码的思路是解决一个经典的单词搜索问题:在一个二维字符网格中,判断是否存在一条路径,使得路径上的字符按顺序连接起来恰好等于给定的单词。路径可以是水平或垂直相邻的单元格,且每个单元格只能使用一次。

代码思路解析

  1. 数据结构

    • vis[7][7]:用于记录网格中的每个单元格是否已经被访问过。vis[i][j]表示第i行第j列的单元格是否已经被访问。

    • m 和 n:分别表示网格的行数和列数。

    • dx[4] 和 dy[4]:用于表示四个方向(上、下、左、右)的偏移量。

  2. 主函数 exist

    • 遍历整个网格,寻找与单词的第一个字符匹配的单元格。

    • 如果找到匹配的单元格,则标记该单元格为已访问,并调用深度优先搜索(DFS)函数 dfs 继续匹配剩余的字符。

    • 如果 dfs 返回 true,表示找到了匹配的路径,直接返回 true

    • 如果遍历完所有可能的起始单元格都没有找到匹配的路径,则返回 false

  3. 深度优先搜索(DFS)函数 dfs

    • 如果当前匹配的位置 pos 等于单词的长度,说明已经成功匹配了整个单词,返回 true

    • 否则,尝试从当前单元格向四个方向(上、下、左、右)扩展,寻找下一个匹配的字符。

    • 如果扩展的单元格在网格范围内、未被访问过,并且字符与单词的下一个字符匹配,则标记该单元格为已访问,并递归调用 dfs

    • 如果递归调用返回 true,表示找到了匹配的路径,直接返回 true

    • 如果递归调用返回 false,表示当前路径无效,需要回溯(即恢复现场),尝试其他方向。

  4. 回溯

    • 在递归调用返回 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]);  // 开始DFSvis[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]);  // 递归DFSvis[x][y] = false;  // 回溯}}}
};

        这个代码的思路是解决一个黄金矿工问题:在一个二维网格中,每个单元格包含一定数量的黄金(用非负整数表示),矿工可以从任意一个非零单元格出发,向上下左右四个方向移动,收集黄金。矿工不能进入黄金数量为0的单元格,也不能重复访问同一个单元格。目标是找到一条路径,使得矿工收集的黄金总量最大。

代码思路解析

  1. 数据结构

    • vis[16][16]:用于记录网格中的每个单元格是否已经被访问过。vis[i][j]表示第i行第j列的单元格是否已经被访问。

    • dx[4] 和 dy[4]:用于表示四个方向(上、下、左、右)的偏移量。

    • m 和 n:分别表示网格的行数和列数。

    • ret:用于记录当前找到的最大黄金总量。

  2. 主函数 getMaximumGold

    • 遍历整个网格,寻找所有非零的单元格作为起点。

    • 对于每一个非零的单元格,标记该单元格为已访问,并调用深度优先搜索(DFS)函数 dfs 开始收集黄金。

    • 在 dfs 调用结束后,恢复该单元格的访问状态(回溯),以便其他路径可以尝试使用该单元格。

    • 最终返回 ret,即找到的最大黄金总量。

  3. 深度优先搜索(DFS)函数 dfs

    • 更新当前路径的黄金总量 path,并与 ret 比较,更新 ret 为较大值。

    • 尝试从当前单元格向四个方向(上、下、左、右)扩展,寻找下一个可以收集黄金的单元格。

    • 如果扩展的单元格在网格范围内、未被访问过,并且黄金数量不为0,则标记该单元格为已访问,并递归调用 dfs

    • 在递归调用结束后,恢复该单元格的访问状态(回溯),以便其他路径可以尝试使用该单元格。

  4. 回溯

    • 在递归调用结束后,需要将当前单元格的访问状态恢复为未访问,以便其他路径可以尝试使用该单元格。

代码优化建议

  • 代码已经非常简洁高效,通过深度优先搜索(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);  // 开始DFSreturn 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);  // 递归DFSvis[x][y] = false;  // 回溯}}}
};

 这个代码的思路是解决一个独特路径问题 III:在一个二维网格中,每个单元格可能包含以下值:

  • 0:表示空单元格,可以通过。

  • 1:表示起点。

  • 2:表示终点。

  • -1:表示障碍物,不能通过。

        目标是找到从起点到终点的所有路径,要求路径必须经过所有的空单元格(0),并且每个单元格只能访问一次。

代码思路解析

  1. 数据结构

    • vis[21][21]:用于记录网格中的每个单元格是否已经被访问过。vis[i][j]表示第i行第j列的单元格是否已经被访问。

    • dx[4] 和 dy[4]:用于表示四个方向(上、下、左、右)的偏移量。

    • m 和 n:分别表示网格的行数和列数。

    • step:表示需要经过的空单元格数量(包括起点和终点)。

    • ret:用于记录满足条件的路径数量。

  2. 主函数 uniquePathsIII

    • 遍历整个网格,统计空单元格的数量(0),并找到起点的位置(1)。

    • 将需要经过的总步数 step 设置为空单元格数量加2(包括起点和终点)。

    • 标记起点为已访问,并调用深度优先搜索(DFS)函数 dfs 开始搜索路径。

    • 最终返回 ret,即满足条件的路径数量。

  3. 深度优先搜索(DFS)函数 dfs

    • 如果当前单元格是终点(2),并且已经经过的步数 count 等于 step,则说明找到了一条合法路径,ret 加1。

    • 否则,尝试从当前单元格向四个方向(上、下、左、右)扩展,寻找下一个可以访问的单元格。

    • 如果扩展的单元格在网格范围内、未被访问过,并且不是障碍物(-1),则标记该单元格为已访问,并递归调用 dfs

    • 在递归调用结束后,恢复该单元格的访问状态(回溯),以便其他路径可以尝试使用该单元格。

  4. 回溯

    • 在递归调用结束后,需要将当前单元格的访问状态恢复为未访问,以便其他路径可以尝试使用该单元格。

代码优化建议

  • 代码已经非常简洁高效,通过深度优先搜索(DFS)和回溯法系统地尝试所有可能的路径。

  • 代码的时间复杂度较高,因为需要尝试所有可能的路径,但在实际应用中,由于网格的大小和路径的限制,通常可以在合理的时间内找到解。

总结

        这个代码通过深度优先搜索(DFS)和回溯法系统地尝试所有可能的路径,解决了独特路径问题 III。代码逻辑清晰,实现简洁,适合解决类似的二维网格搜索问题。通过递归和回溯,代码能够高效地找到满足条件的路径数量。


http://www.ppmy.cn/server/171585.html

相关文章

数据结构——二叉树经典习题讲解

各位看官早安午安晚安呀 如果您觉得这篇文章对您有帮助的话 欢迎您一键三连&#xff0c;小编尽全力做到更好 欢迎您分享给更多人哦 大家好&#xff0c;我们今天来学习java数据结构的二叉树 递归很重要的一些注意事项&#xff1a; 1&#xff1a;递归你能不能掌握在于&#xff1…

HTTPS 与 HTTP 的区别在哪?

HTTP与HTTPS作为互联网数据传输的核心协议&#xff0c;其通信机制与安全特性深刻影响着现代网络应用的可靠性与用户体验。本文将解析两者的通信流程、安全机制及核心差异。 一、HTTP的通信机制 先来看看HTTP是什么吧。 HTTP基于TCP/IP协议栈&#xff0c;采用经典客户端-服务…

lambda表达式,函数式接口,方法引用,Stream流

1.lambda表达式 前提&#xff1a;必须是函数式接口 特殊的匿名内部类&#xff0c;语法更简洁 允许把函数作为一个方法的参数&#xff08;函数作为方法参数传递&#xff09;&#xff0c;将代码像数据一样传递 语法&#xff1a; <函数式接口><变量名>&#xff08…

vscode下载安装教程(附安装包)vscode图文安装教程最新版

文章目录 一、vscode下载二、vscod安装教程1.启动vscode安装程序&#xff1a;2.应对提示&#xff1a;3.接受协议&#xff1a;4.更改vscode安装路径&#xff1a;5.推进安装vscode&#xff1a;6.创建vscode快捷方式&#xff1a;7.开始安装vscode&#xff1a;8.完成vscode安装&…

AI数据分析:用DeepSeek做数据清洗

在当今数据驱动的时代&#xff0c;数据分析已成为企业和个人决策的重要工具。随着人工智能技术的快速发展&#xff0c;AI 驱动的数据分析工具正在改变我们处理和分析数据的方式。本文将着重介绍如何使用 DeepSeek 进行数据清洗。 数据清洗是数据分析的基础&#xff0c;其目的是…

《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》

作者&#xff1a; 周志明 DeepSeek建议JVM书籍首选。 第一部分 走进Java 第1章 走进Java 世界上并没有完美的程序&#xff0c;但我们并不因此而沮丧&#xff0c;因为写程序本来就是一个不断追求完美的过程。 JAVA的优点&#xff0c;摆脱了平台的束缚&#xff0c;实现了一次…

spring注解开发(Spring整合MyBatis——Mapper代理开发模式、(Spring、MyBatis、Jdbc)配置类)(6)

目录 一、纯MyBatis独立开发程序。 &#xff08;1&#xff09;数据库与数据表。 &#xff08;2&#xff09;实体类。 &#xff08;3&#xff09;dao层接口。&#xff08;Mapper代理模式、无SQL映射文件——注解配置映射关系&#xff09; &#xff08;4&#xff09;MyBatis核心配…

Fisher信息矩阵(Fisher Information Matrix, FIM)与自然梯度下降:机器学习中的优化利器

Fisher信息矩阵与自然梯度下降&#xff1a;机器学习中的优化利器 在机器学习尤其是深度学习中&#xff0c;优化模型参数是一个核心任务。我们通常依赖梯度下降&#xff08;Gradient Descent&#xff09;来调整参数&#xff0c;但普通的梯度下降有时会显得“笨拙”&#xff0c;…