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

news2025/3/1 14:36:19

目录

一、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 的值
        }
    }
};

 代码思路

  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 个位置开始 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
            }
        }
    }
};

代码思路

  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; // 初始化 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 改为全局变量(即类的成员变量),代码可能会超时,原因如下:

  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;
        else
            ch += 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 到 n
            if (!check[i] && (pos % i == 0 || i % pos == 0)) { // 如果数字 i 未被使用且满足条件
                check[i] = true; // 选择数字 i
                dfs(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-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. 每一行必须包含数字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]);  // 开始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的单元格,也不能重复访问同一个单元格。目标是找到一条路径,使得矿工收集的黄金总量最大。

代码思路解析

  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);  // 开始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),并且每个单元格只能访问一次。

代码思路解析

  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.coloradmin.cn/o/2307936.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java SE】Java中String的内存原理

参考笔记&#xff1a; Java String 类深度解析&#xff1a;内存模型、常量池与核心机制_java stringx、-CSDN博客 解析java中String的内存原理_string s1 new string("ab");内存分析-CSDN博客 目录 1.String初识 2.字符串字面量 3.内存原理图 4. 示例验证 4.…

IDEA提示将方法形参更改为(什么什么类型),要检查对应的实体类中的字段类型是否正确

IDEA提示inviteCodeId应该是字符串&#xff0c;明显不对&#xff0c;后来检查发现是FakeRegistration类中把inviteCodeId定义为String类型了。

【芯片设计】NPU芯片前端设计工程师面试记录·20250227

应聘公司 某NPU/CPU方向芯片设计公司。 小声吐槽两句,前面我问了hr需不需要带简历,hr不用公司给打好了,然后我就没带空手去的。结果hr小姐姐去开会了,手机静音( Ĭ ^ Ĭ )面试官、我、另外的hr小姐姐都联系不上,结果就变成了两个面试官和我一共三个人在会议室里一人拿出…

初阶数据结构(C语言实现)——3顺序表和链表(3)

3.链表 3.1 链表的概念及结构 概念&#xff1a;链表是一种物理存储结构上非连续、非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次序实现的 链表的物理结构 1.从上图可看出&#xff0c;链式结构在逻辑上是连续的&#xff0c;但是在物理上不一定连续…

使用 VSCode 代替 BeyondStudio for NXP 开发 JN 5169

使用 VSCode 代替 BeyondStudio for NXP 开发 JN 5169 一、安装 VSCode二、搭建 NXP JN5169 ZigBee 3.0 开发环境和下载示例工程三、配置 VSCode1、配置环境变量 MYSYS_HOME2、VSCode 安装以下插件3、VSCode 配置头文件路径 四、编译工程1、JN-AN-1219 有 6 个构建选项2、修改 …

DBGPT安装部署使用

简介 DB-GPT是一个开源的AI原生数据应用开发框架(AI Native Data App Development framework with AWEL(Agentic Workflow Expression Language) and Agents)。 目的是构建大模型领域的基础设施&#xff0c;通过开发多模型管理(SMMF)、Text2SQL效果优化、RAG框架以及优化、Mul…

How to use VRX on ubuntu20.04 with ROS1 Noetic?[2]

How to use VRX on ubuntu20.04 with ROS1 Noetic?[2] 1.Which topics2.Control1.1操作模拟船&#xff08;1&#xff09;命令行直接发布&#xff08;2&#xff09;启动键盘控制文件 3.Customer your VRX world3.1Which world parameters3.2改变风的参数 3.2.1更改默认参数&…

yolov8 目标追踪 (源码 +效果图)

1.在代码中 增加了s键开始追踪 e键结束追踪 显示移动距离(代码中可调标尺和像素的比值 以便接近实际距离) 2.绘制了监测区域 只在区域内的检测 3.规定了检测的类别 只有人类才绘制轨迹 import osimport cv2 from ultralytics import YOLO from collections import defaultdic…

基于Python的web漏洞挖掘,漏洞扫描系统(附源码,部署)

本次技术通过利用Python技术来开发一款针对web漏洞挖掘扫描的技术&#xff0c;通过web漏洞的挖掘扫描来实现对网站URL的漏洞检测&#xff0c;通过高中低风险的判断来实现对一款网站中存在的漏洞进行可视化的分析&#xff0c;从而能够找到问题并且尽快的实现问题的解决。 博主介…

岳阳市美术馆预约平台(小程序论文源码调试讲解)

第4章 系统设计 一个成功设计的系统在内容上必定是丰富的&#xff0c;在系统外观或系统功能上必定是对用户友好的。所以为了提升系统的价值&#xff0c;吸引更多的访问者访问系统&#xff0c;以及让来访用户可以花费更多时间停留在系统上&#xff0c;则表明该系统设计得比较专…

ubuntu22.04系统如何自建2级ntp服务器

一&#xff1a;ntp服务器详情 服务器型号 系统版本 IP地址 主机名 ntp服务版本 虚拟机8c-32g-1T Ubuntu22.04 10.20.30.2 DMZ-NTP-SERVER 4.2.8p15 二&#xff1a;ntp服务端部署配置脚本 #!/bin/bash # 脚本信息 echo "--------------------------…

DeepSeek赋能智慧社区:提升社区治理,优化资源配置,带来全新变革

在数字化浪潮的推动下&#xff0c;智慧社区正逐渐成为城市发展的重要方向。作为一款先进的人工智能大模型&#xff0c;DeepSeek凭借其强大的多模态数据分析和智能决策能力&#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核心配…

【MySQL】数据库-图书管理系统(CC++实现)

一.预期功能 该图书管理系统设计提供基本的设计模版&#xff0c;涉及数据库的增删查改等操作&#xff0c;包含登录功能&#xff0c;图书管理功能&#xff0c;图书借阅功能&#xff0c;用户管理功能等基础功能&#xff0c;详细功能查看以下菜单表&#xff0c;共包含三个菜单&am…

VSCode轻松调试运行C#控制台程序

1.背景 我一直都是用VS来开发C#项目的&#xff0c;用的比较顺手&#xff0c;也习惯了。看其他技术文章有介绍VS Code更轻量&#xff0c;更方便。所以我专门花时间来使用VS Code&#xff0c;看看它是如何调试代码、如何运行C#控制台。这篇文章是一个记录的过程。 2.操作 2.1 V…

python-leetcode-下一个排列

31. 下一个排列 - 力扣&#xff08;LeetCode&#xff09; class Solution:def nextPermutation(self, nums: List[int]) -> None:"""Do not return anything, modify nums in-place instead."""# Step 1: Find the first decreasing element …

Jsmoke-一款强大的js检测工具,浏览器部署即用,使用方便且高效

目录标题 Jsmoke &#x1f6ac;&#x1f6ac; by Yn8rt使用方式界面预览功能特性支持的敏感信息类型 Jsmoke &#x1f6ac;&#x1f6ac; by Yn8rt ​ 该插件由 Yn8rt师傅 开发&#xff0c;插件可以理解为主动版的hae和apifinder&#xff0c;因为其中的大多数规则我都引用了&a…

iphone上ios设备开启safari开发者debug模式,配合mac电脑使用

1.mac操作 mac的safari上打开开发者模式&#xff0c;打开显示网页开发者功能 2.开启IPhone的Safari调试模式 启用 Web 检查 功能&#xff0c;打开 iPhone 依次进入 设置 > Safari浏览器 > 高级 > 网页检查器 > 启用。 3.调试步骤 先用IPhone 的Safari打开要调试…

Datawhale 数学建模导论二 笔记5 多模数据与智能模型

主要涉及到的知识点有&#xff1a; 数字图像处理与计算机视觉 计算语言学与自然语言处理 数字信号处理与智能感知 10.1 数字图像处理与计算机视觉 视觉信息是我们第一种非常规的数据模式&#xff0c;在Python当中可以使用opencv处理数字图像&#xff0c;并提取出视觉特征用…

如何在一台服务器上搭建 mongodb副本集1主2从节点

在一台服务器上搭建 MongoDB 副本集&#xff08;1 主节点 2 从节点&#xff09;可以通过运行多个 MongoDB 实例并使用不同端口和数据目录来实现。以下是详细步骤&#xff1a; 1. 准备工作 确保已安装 MongoDB。为每个实例创建独立的数据目录和日志文件。 2. 创建数据目录和…