【递归、搜索与回溯算法】(综合练习:一网打尽常见题型分类总结与方法归纳)


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

学完递归、搜索与回溯的基本概念之后,很多人都会进入下一个常见阶段:概念好像听懂了,模板也能照着写,但一到具体题目,还是不知道该怎么下手.这是因为,回溯算法真正的难点,往往不在"代码怎么写",而在于:你能不能一眼看出这道题属于什么模型,这棵搜索树该怎么展开,每一层到底在决策什么.同样是递归搜索,有的题是在做"选或不选"的二叉决策,有的题是在枚举当前位置可以填什么数,有的题是在固定起点后不断向后扩展;表面看起来题目各不相同,背后却往往对应着相似的搜索结构.也正因为如此,学习回溯不能只停留在背几个模板上.真正重要的是把常见题型拆开、分类、归纳,建立起"看到题目就能识别模型"的能力.只有这样,递归、搜索和回溯这些看似抽象的概念,才能真正转化为稳定可复用的解题方法.本篇文章就不再停留在概念层面,而是围绕"综合练习"展开,系统梳理回溯算法中最常见的几类题型,并结合典型例题总结它们各自的搜索思路、递归框架和解题要点.希望通过这一轮集中训练,把前面学过的递归、搜索与回溯思想真正串起来,形成一套清晰、完整、可迁移的题型认知体系.换句话说,这篇文章的目标不是再教你"记住一个模板",而是帮助你做到:看到题目,先判断类型;识别类型,再设计搜索树;想清搜索树,最后自然写出递归与回溯.接下来,我们就从最常见、也最基础的几类回溯题型开始,一步步把这些高频模型一网打尽.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录

1.找出所有子集的异或总和再求和(OJ题)


算法思路:解法(递归):

所有子集可以解释为:每个元素选择在或不在一个集合中(因此,子集有2n个).本题我们需要求出所有子集,将它们的异或和相加.因为异或操作满足交换律,所以我们可以定义一个变量,直接记录当前状态的异或和.使用递归保存当前集合的状态(异或和),选择将当前元素添加至当前状态与否,并依次递归数组中下一个元素.当递归到空元素时,表示所有元素都被考虑到,记录当前状态(将当前状态的异或和添加至答案中).

例如集合中的元素为 [1, 2],则它的子集状态选择过程如下:

复制代码
1       []
2      /  \
3    []    [1]    // 第一个元素选择与否
4   / \   /  \
5 [] [2] [1] [1, 2]  // 第二个元素选择与否,每个状态到这一层时需要记录异或和

递归函数设计:

cpp 复制代码
void dfs(int val, int idx, vector<int>& nums)
  • 参数
    val(当前状态的异或和),
    idx(当前需要处理的元素下标,处理过程:选择将其添加至当前状态或不进行操作);
  • 返回值:无;
  • 函数作用:选择对元素进行添加与否处理.

递归流程:

  1. 递归结束条件:当前下标与数组长度相等,即已经越界,表示已经考虑到所有元素;将当前异或和添加至答案中,并返回;
  2. 考虑将当前元素添加至当前状态,当前状态更新为与当前元素值的异或和,然后递归下一个元素;
  3. 考虑不选择当前元素,当前状态不变,直接递归下一个元素;

核心代码

cpp 复制代码
class Solution 
{
    //1.定义两个成员变量(全局生效,回溯中记录状态)
    int path;  //记录【当前子集的异或和】
    int sum;   //记录【所有子集异或和的总和】(最终答案)

public:
    //主函数:入口,接收数组,返回最终结果
    int subsetXORSum(vector<int>& nums) 
    {
        path = 0; //初始化:空集的异或和为0
        sum = 0;  //初始化:总和为0
        dfs(nums, 0); //从数组下标0开始,执行回溯
        return sum;   //返回所有子集异或和的总和
    }

    //核心:回溯函数(DFS),枚举所有子集
    //nums:目标数组  pos:当前遍历的起始下标
    void dfs(vector<int>& nums, int pos) 
    {
        //关键1:每次进入递归,当前path就是一个【有效子集的异或和】,直接累加到总和
        sum += path;

        //关键2:for循环枚举:从pos开始选元素,避免生成重复子集
        for (int i = pos; i < nums.size(); i++) 
        {
            path ^= nums[i];    //选择:把当前元素加入子集,更新异或和
            dfs(nums, i + 1);  //递归:处理下一个元素(不回头,保证子集唯一)
            path ^= nums[i];   //回溯:撤销选择,恢复path的原始状态(恢复现场)
        }
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution
{
    int path = 0;
    int sum = 0;

public:
    int subsetXORSum(vector<int>& nums)
    {
        path = 0;
        sum = 0;
        dfs(nums, 0);
        return sum;
    }

    void dfs(vector<int>& nums, int pos)
    {
        sum += path;

        for (int i = pos; i < nums.size(); i++)
        {
            path ^= nums[i];
            dfs(nums, i + 1);
            path ^= nums[i]; // 恢复现场
        }
    }
};

int main()
{
    Solution s;
    vector<int> nums = {1, 3};

    int ans = s.subsetXORSum(nums);

    cout << "数组 nums = { ";
    for (int x : nums)
    {
        cout << x << " ";
    }
    cout << "}" << endl;

    cout << "所有子集的异或总和为: " << ans << endl;

    return 0;
}

2.全排列||(OJ题)


算法思路:

因为题目不要求返回的排列顺序,因此我们可以对初始状态排序,将所有相同的元素放在各自相邻的位置,方便之后操作.因为重复元素的存在,我们在选择元素进行全排列时,可能会存在重复排列,例如:[1, 2, 1],所有的下标排列为:

复制代码
1  123
2  132
3  213
4  231
5  312
6  321

按照以上下标进行排列的结果为:

复制代码
1  121
2  112
3  211
4  211
5  112
6  121

可以看到,有效排列只有三种[1, 1, 2][1, 2, 1][2, 1, 1],其中每个排列都出现两次.因此,我们需要对相同元素定义一种规则,使得其组成的排列不会形成重复的情况:

  1. 我们可以将相同的元素按照排序后的下标顺序出现在排列中,通俗来讲,若元素s出现x次,则排序后的第2个元素s一定出现在第1个元素s后面,排序后的第3个元素s一定出现在第2个元素s后面,以此类推,此时的全排列一定不会出现重复结果.
  2. 例如:a1=1,a2=1,a3=2,排列结果为 [1, 1, 2] 的情况只有一次,即a1a2前面,因为a2不会出现在a1前面从而避免了重复排列.
  3. 我们在每一个位置上考虑所有的可能情况并且不出现重复;
  4. 注意:若当前元素的前一个相同元素未出现在当前状态中,则当前元素也不能直接放入当前状态的数组,此做法可以保证相同元素的排列顺序与排序后的相同元素的顺序相同,即避免了重复排列出现.
  5. 通过深度优先搜索的方式,不断地枚举每个数在当前位置的可能性,并在递归结束时回溯到上一个状态,直到枚举完所有可能性,得到正确的结果.

递归函数设计:void backtrack(vector<int>& nums, int idx)

  • 参数:idx(当前需要填入的位置);
  • 返回值:无;
  • 函数作用:查找所有合理的排列并存储在答案列表中.

递归流程如下:

  1. 定义一个二维数组 ans 用来存放所有可能的排列,一个一维数组 perm 用来存放每个状态的排列,一个一维数组 visited 标记元素,然后从第一个位置开始进行递归;
  2. 在每个递归的状态中,我们维护一个步数 idx,表示当前已经处理了几个数字;
  3. 递归结束条件:当 idx 等于 nums 数组的长度时,说明我们已经处理完了所有数字,将当前数组存入结果中;
  4. 在每个递归状态中,枚举所有下标 i,若这个下标未被标记,并且在它之前的相同元素被标记过,则使用 nums 数组中当前下标的元素:
    (1)将 visited[i] 标记为 1;
    (2)将 nums[i] 添加至 perm 数组末尾;
    (3)对第 idx+1 个位置进行递归;
    (4)将 visited[i] 重新赋值为0,并删除 perm 末尾元素表示回溯;
  5. 最后,返回 ans.

核心代码

cpp 复制代码
class Solution {
public:
    //主函数:入口,接收数组,返回所有不重复的全排列
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<vector<int>> ans;       //存储最终所有不重复的排列结果
        vector<int> perm;             //存储当前正在拼接的一个排列
        vector<bool> visited(nums.size(), false); //标记元素是否被使用
        sort(nums.begin(), nums.end()); //【关键1】排序:让相同元素相邻,为去重做准备
        backtrack(nums, 0, ans, perm, visited); //启动回溯递归
        return ans;
    }

private:
    //回溯递归函数:核心逻辑
    //idx:当前要填充的位置(从0开始)
    void backtrack(vector<int>& nums, int idx, vector<vector<int>>& ans, vector<int>& perm, vector<bool>& visited) {
        //【递归终止条件】:所有位置都填完了,一个完整排列生成
        if (idx == nums.size()) {
            ans.push_back(perm); //把当前排列加入结果集
            return;
        }

        //枚举所有元素,尝试填入当前位置
        for (int i = 0; i < nums.size(); ++i) {
            if (visited[i]) continue; //跳过已经用过的元素
            
            //【关键2:核心去重逻辑】跳过重复元素,避免生成重复排列
            if (i > 0 && nums[i] == nums[i-1] && !visited[i-1]) continue;

            //1.选择:标记当前元素为已使用,加入当前排列
            visited[i] = true;
            perm.push_back(nums[i]);
            
            //2.递归:填充下一个位置
            backtrack(nums, idx+1, ans, perm, visited);
            
            //3.回溯:撤销选择,恢复状态(核心!)
            perm.pop_back();
            visited[i] = false;
        }
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<vector<int>> ans;
        vector<int> perm;
        vector<bool> visited(nums.size(), false);

        sort(nums.begin(), nums.end());
        backtrack(nums, 0, ans, perm, visited);

        return ans;
    }

private:
    void backtrack(vector<int>& nums, int idx, vector<vector<int>>& ans, vector<int>& perm, vector<bool>& visited) {
        if (idx == nums.size()) {
            ans.push_back(perm);
            return;
        }

        for (int i = 0; i < nums.size(); ++i) {
            if (visited[i]) continue;

            // 去重:同一层中,前一个相同数字没被使用时,跳过当前数字
            if (i > 0 && nums[i] == nums[i - 1] && !visited[i - 1]) continue;

            visited[i] = true;
            perm.push_back(nums[i]);

            backtrack(nums, idx + 1, ans, perm, visited);

            perm.pop_back();
            visited[i] = false;
        }
    }
};

int main() {
    Solution s;

    vector<int> nums = {1, 1, 2};

    vector<vector<int>> result = s.permuteUnique(nums);

    cout << "输入数组:";
    for (int x : nums) {
        cout << x << " ";
    }
    cout << endl;

    cout << "去重后的全排列结果如下:" << endl;
    for (const auto& perm : result) {
        cout << "{ ";
        for (int x : perm) {
            cout << x << " ";
        }
        cout << "}" << endl;
    }

    return 0;
}

3.电话号码的字母组合(OJ题)


算法思路:

每个位置可选择的字符与其他位置并不冲突,因此不需要标记已经出现的字符,只需要将每个数字对应的字符依次填入字符串中进行递归,在回溯时撤销填入操作即可.

在递归之前我们需要定义一个字典 phoneMap,记录 2~9 各自对应的字符.
递归函数设计:
void backtrack(unordered_map<char, string>& phoneMap, string& digits, int index)

  • 参数:index(已经处理的元素个数),ans(字符串当前状态),res(所有成立的字符串);
  • 返回值:无;
  • 函数作用:查找所有合理的字母组合并存储在答案列表中.

递归函数流程如下:

  1. 递归结束条件:当 index 等于 digits 的长度时,将 ans 加入到 res 中并返回;
  2. 取出当前处理的数字 digit,根据 phoneMap 取出对应的字母列表 letters;
  3. 遍历字母列表 letters,将当前字母加入到组合字符串 ans 的末尾,然后递归处理下一个数字(传入 index + 1,表示处理下一个数字);
  4. 递归处理结束后,将加入的字母从 ans 的末尾删除,表示回溯.
  5. 最终返回 res 即可.

核心代码

cpp 复制代码
class Solution
{
    //1.成员变量:九宫格数字 -> 字母的映射表
    //下标对应数字0-9,0和1无字母,为空字符串
    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;
        //启动深度优先搜索,从第0个数字开始处理
        dfs(digits, 0);
        //返回所有组合结果
        return ret;
    }

    //核心:回溯递归函数
    //digits:输入的数字字符串  pos:当前正在处理的数字下标
    void dfs(string& digits, int pos)
    {
        //递归终止条件:处理完所有数字(pos等于数字长度)
        if(pos == digits.size())
        {
            //把当前拼接好的完整字母组合,加入结果集
            ret.push_back(path);
            return;
        }

        //遍历【当前数字对应的所有字母】
        //digits[pos] - '0':把字符数字转为整型(如'2'→2),拿到hash中对应的字母串
        for(auto ch : hash[digits[pos] - '0'])
        {
            path.push_back(ch);    //1.选择:把当前字母加入组合
            dfs(digits, pos + 1);  //2.递归:处理下一个数字
            path.pop_back();       //3.回溯:撤销选择,删除最后一个字母(恢复现场)
        }
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Solution
{
    string hash[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    string path;
    vector<string> ret;

public:
    vector<string> letterCombinations(string digits)
    {
        ret.clear();
        path.clear();

        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(); // 恢复现场
        }
    }
};

int main()
{
    Solution s;
    string digits = "23";

    vector<string> ans = s.letterCombinations(digits);

    cout << "输入数字串: " << digits << endl;
    cout << "字母组合结果如下:" << endl;

    for (const auto& str : ans)
    {
        cout << str << endl;
    }

    return 0;
}

4.括号生成(OJ题)


算法思路:

从左往右进行递归,在每个位置判断放置左右括号的可能性,若此时放置左括号合理,则放置左括号继续进行递归,右括号同理.

一种判断括号是否合法的方法:从左往右遍历,左括号的数量始终大于等于右括号的数量,并且左括号的总数量与右括号的总数量相等.因此我们在递归时需要进行以下判断:

  1. 放入左括号时需判断此时左括号数量是否小于字符串总长度的一半(若左括号的数量大于等于字符串长度的一半时继续放置左括号,则左括号的总数量一定大于右括号的总数量);
  2. 放入右括号时需判断此时右括号数量是否小于左括号数量.

递归函数设计:void dfs(int step, int left)

  • 参数:step(当前需要填入的位置),left(当前状态的字符串中的左括号数量);
  • 返回值:无;
  • 函数作用:查找所有合理的括号序列并存储在答案列表中.

递归函数参数设置为当前状态的字符串长度以及当前状态的左括号数量,递归流程如下:

  1. 递归结束条件:当前状态字符串长度与 2*n 相等,记录当前状态并返回;
  2. 若此时左括号数量小于字符串总长度的一半,则在当前状态的字符串末尾添加左括号并继续递归,递归结束撤销添加操作;
  3. 若此时右括号数量小于左括号数量(右括号数量可以由当前状态的字符串长度减去左括号数量求得),则在当前状态的字符串末尾添加右括号并递归,递归结束撤销添加操作;

核心代码

cpp 复制代码
class Solution
{
    //成员变量(全局共享,记录递归状态)
    int left, right;  //left:当前已用左括号数  right:当前已用右括号数
    int n;           //需要生成的括号总对数
    string path;     //记录【当前正在拼接的括号字符串】(回溯路径)
    vector<string> ret; //存储【所有合法的括号组合】(最终答案)

public:
    //主函数:入口,接收括号对数n,返回所有合法组合
    vector<string> generateParenthesis(int _n)
    {
        n = _n;   //初始化目标括号对数
        dfs();    //启动深度优先搜索(回溯)
        return ret; //返回结果集
    }

    //核心:递归+回溯函数
    void dfs()
    {
        //1.递归终止条件:右括号数量达到n,说明生成了完整的n对合法括号
        if(right == n)
        {
            ret.push_back(path); //将合法括号串加入结果
            return;
        }

        //2. 选择添加左括号:只要左括号数量没超过n,就可以加
        if(left < n) 
        {
            path.push_back('('); //拼接左括号
            left++;             //左括号计数+1
            dfs();              //递归继续拼接下一个字符
            //回溯:恢复现场(撤销本次选择)
            path.pop_back();    
            left--;
        }

        //3.选择添加右括号:必须满足 右括号数量 < 左括号数量(保证括号合法!)
        if(right < left) 
        {
            path.push_back(')'); //拼接右括号
            right++;            //右括号计数+1
            dfs();              //递归继续拼接下一个字符
            //回溯:恢复现场(撤销本次选择)
            path.pop_back();
            right--;
        }
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Solution
{
    int left = 0, right = 0, n = 0;
    string path;
    vector<string> ret;

public:
    vector<string> generateParenthesis(int _n)
    {
        n = _n;
        left = 0;
        right = 0;
        path.clear();
        ret.clear();

        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--; // 恢复现场
        }
    }
};

int main()
{
    Solution s;
    int n = 3;

    vector<string> ans = s.generateParenthesis(n);

    cout << "n = " << n << endl;
    cout << "所有合法括号组合如下:" << endl;

    for (const auto& str : ans)
    {
        cout << str << endl;
    }

    return 0;
}

5.组合(OJ题)


算法思路:解法(回溯):

题目要求我们从 1 到 n 中选择 k 个数的所有组合,其中不考虑顺序.也就是说,[1,2][2,1] 等价.我们需要找出所有的组合,但不能重复计算相同元素的不同顺序的组合.对于选择组合,我们需要进行如下流程:

  1. 所有元素分别作为首位元素进行处理;
  2. 在之后的位置上同理,选择所有元素分别作为当前位置元素进行处理;
  3. 为避免计算重复组合,规定选择之后位置的元素时必须比前一个元素大,这样就不会有重复的组合([1,2][2,1][2,1] 不会出现).

递归函数设计:
void dfs(vector<vector<int>>& ans, vector<int>& v, int step, int &n, int &k)

  • 参数:step(当前需要进行处理的位置);
  • 返回值:无;
  • 函数作用:某个元素作为首位元素出现时,查找所有可能的组合.

具体实现方法如下:

  1. 定义一个二维数组和一维数组.二维数组用来记录所有组合,一维数组用来记录当前状态下的组合.
  2. 遍历1到 n-k+1,以当前数作为组合的首位元素进行递归(从 n-k+1n 作为首位元素时,组合中一定不会存在k个元素).
  3. 递归函数的参数为两个数组、当前步骤以及n和k.递归流程如下:
    (1)结束条件:当前组合中已经有 k 个元素,将当前组合存进二维数组并返回.
    (2)剪枝:如果当前位置之后的所有元素放入组合也不能满足组合中存在 k 个元素,直接返回.
    (3)从当前位置的下一个元素开始遍历到n,将元素赋值到当前位置,递归下一个位置.

核心代码

cpp 复制代码
class Solution
{
    //成员变量:递归中共享的状态变量
    vector<int> path;   //存储【当前正在拼接的组合】(回溯路径)
    vector<vector<int>> ret; //存储【所有符合要求的组合】(最终答案)
    int n, k;           //n:数字范围 1~n;k:组合需要的元素个数

public:
    //主函数:入口,接收参数n、k,返回所有组合
    vector<vector<int>> combine(int _n, int _k)
    {
        n = _n;  //初始化数字范围
        k = _k;  //初始化组合长度
        dfs(1);  //启动回溯,从数字 1 开始遍历
        return ret; //返回结果集
    }

    //核心:回溯递归函数
    //start:当前遍历的起始位置(关键!避免生成重复组合)
    void dfs(int start)
    {
        //1.递归终止条件:当前组合的长度等于 k,说明找到了一个有效组合
        if(path.size() == k)
        {
            ret.push_back(path); //将有效组合存入结果
            return;
        }

        //2.遍历:从 start 开始,依次选择数字加入组合
        for(int i = start; i <= n; i++)
        {
            path.push_back(i);    //选择:把当前数字加入组合
            dfs(i + 1);          //递归:下一个数字必须比当前大(i+1),保证不重复
            path.pop_back();     //回溯:撤销选择,删除最后一个数字(恢复现场)
        }
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

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;
        path.clear();
        ret.clear();

        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(); // 恢复现场
        }
    }
};

int main()
{
    Solution s;
    int n = 4, k = 2;

    vector<vector<int>> ans = s.combine(n, k);

    cout << "n = " << n << ", k = " << k << endl;
    cout << "所有组合结果如下:" << endl;

    for (const auto& group : ans)
    {
        cout << "{ ";
        for (int x : group)
        {
            cout << x << " ";
        }
        cout << "}" << endl;
    }

    return 0;
}

6.目标和(OJ题)


算法思路:解法(回溯):

对于每个数,可以选择加上或减去它,依次枚举每一个数字,在每个数都被选择时检查得到的和是否等于目标值.如果等于,则记录结果.需要注意的是,为了优化时间复杂度,可以提前计算出数组中所有数字的和 sum,以及数组的长度 len.这样可以快速判断当前的和减去剩余的所有数是否已经超过了目标值 target,或者当前的和加上剩下的数的和是否小于目标值 target,如果满足条件,则可以直接回溯.

递归流程:

  1. 递归结束条件:index 与数组长度相等,判断当前状态的 sum 是否与目标值相等,若是计数加一;
  2. 选择当前元素进行加操作,递归下一个位置,并更新参数 sum;
  3. 选择当前元素进行减操作,递归下一个位置,并更新参数 sum;
  • 特别地,此问题可以转化为另一个问题:若所有元素初始状态均为减,选择其中几个元素将他们的状态修改为加,计算修改后的元素和与目标值相等的方案个数.
    1. 选择其中 x 个元素进行修改,并且这 x 个元素的和为 y;
    2. 检查使得 -sum + 2*y = target(移项:y=(sum+target)/2)成立的方案个数,即选择 x 个元素和为 (sum+target)/2 的方案个数;
      a. 若 sum+target 为奇数,则不存在这种方案;
    3. 递归流程:
      a. 传入参数:index(当前要处理的元素下标),sum(当前状态和),nums(元素数组),aim(目标值:(sum+target)/2);
      b. 递归结束条件:index 与数组长度相等,判断当前 sum 是否与目标值相等,若是返回 1,否则返回 0;
      c. 返回递归选择当前元素以及递归不选择当前元素函数值的和.

核心代码

cpp 复制代码
class Solution
{
    //成员变量
    int ret;  //记录符合条件的表达式总数
    int aim;  //存储目标值 target

public:
    //主函数:入口,接收数组和目标值,返回总方案数
    int findTargetSumWays(vector<int>& nums, int target)
    {
        ret = 0;    //初始化方案数为 0
        aim = target; //把目标值存入成员变量
        dfs(nums, 0, 0); //启动DFS:从下标0开始,当前和为0
        return ret;   //返回最终统计的方案数
    }

    //核心:DFS递归函数
    //nums:数组  pos:当前处理到的元素下标  path:当前计算的累加和
    void dfs(vector<int>& nums, int pos, int path)
    {
        //1.递归终止条件:已经处理完数组中所有元素
        if(pos == nums.size())
        {
            //如果当前和 == 目标值,方案数+1
            if(path == aim) ret++;
            return;
        }

        //2.每个数字有两种选择:加 或者 减
        //选择一:给当前数字加 + 号,递归处理下一个元素
        dfs(nums, pos + 1, path + nums[pos]);
        //选择二:给当前数字加 - 号,递归处理下一个元素
        dfs(nums, pos + 1, path - nums[pos]);
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution
{
    int ret = 0, aim = 0;

public:
    int findTargetSumWays(vector<int>& nums, int target)
    {
        ret = 0;
        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]);
    }
};

int main()
{
    Solution s;

    vector<int> nums = {1, 1, 1, 1, 1};
    int target = 3;

    int ans = s.findTargetSumWays(nums, target);

    cout << "nums = { ";
    for (int x : nums)
    {
        cout << x << " ";
    }
    cout << "}" << endl;

    cout << "target = " << target << endl;
    cout << "方案数 = " << ans << endl;

    return 0;
}

7.组合总和(OJ题)


算法思路:
candidates 的所有元素互不相同,因此我们在递归状态时只需要对每个元素进行如下判断:

  1. 跳过,对下一个元素进行判断;
  2. 将其添加至当前状态中,我们在选择添加当前元素时,之后仍可以继续选择当前元素(可以重复选择同一元素).
  3. 因此,我们在选择当前元素并向下传递下标时,应该直接传递当前元素下标.

递归函数设计:
void dfs(vector<int>& candidates, int target, vector<vector<int>>& ans, vector<int>& combine, int idx)

  • 参数:target(当前状态和与目标值的差),idx(当前需要处理的元素下标);
  • 返回值:无;
  • 函数作用:向下传递两个状态(跳过或者选择当前元素),找出所有组合使得元素和为目标值.

递归函数流程如下:

  1. 结束条件:
    a. 当前需要处理的元素下标越界;
    b. 当前状态的元素和已经与目标值相同;
  2. 跳过当前元素,当前状态不变,对下一个元素进行处理;
  3. 选择将当前元素添加至当前状态,并保留状态继续对当前元素进行处理,递归结束时撤销添加操作.

核心代码

cpp 复制代码
class Solution
{
    int aim;                //目标值 target
    vector<int> path;       //存储当前正在拼接的组合(回溯路径)
    vector<vector<int>> ret;//存储所有符合条件的组合(最终答案)

public:
    //主函数:入口,接收数组和目标值,返回结果集
    vector<vector<int>> combinationSum(vector<int>& nums, int target)
    {
        aim = target;  //初始化目标值
        dfs(nums, 0, 0); //启动DFS:从下标0开始,当前和为0
        return ret;
    }

    //核心:回溯递归函数
    //nums:数组  pos:当前处理的元素下标  sum:当前组合的累加和
    void dfs(vector<int>& nums, int pos, int sum)
    {
        //1.终止条件1:当前和 = 目标值 → 找到有效组合,存入结果
        if(sum == aim)
        {
            ret.push_back(path);
            return;
        }
        //2.终止条件2:和超标 / 遍历完所有元素 → 无效,直接返回(剪枝)
        if(sum > aim || pos == nums.size()) return;

        //3.核心逻辑:枚举当前数字 nums[pos] 选 k 个(k=0,1,2...)
        for(int k = 0; k * nums[pos] + sum <= aim; k++)
        {
            if(k) path.push_back(nums[pos]); //k≥1:把当前数字加入组合
            //递归:处理下一个数字(pos+1,保证组合不重复)
            dfs(nums, pos + 1, sum + k * nums[pos]);
        }

        //4.回溯:恢复现场,把刚才加入的所有当前数字全部删除
        for(int k = 1; k * nums[pos] + sum <= aim; k++)
        {
            path.pop_back();
        }
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution
{
    int aim;
    vector<int> path;
    vector<vector<int>> ret;

public:
    vector<vector<int>> combinationSum(vector<int>& nums, int target)
    {
        aim = target;
        path.clear();
        ret.clear();

        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;

        // 枚举当前 nums[pos] 选择 0 个、1 个、2 个......
        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]);
        }

        // 恢复现场:把刚才加入的 nums[pos] 全部弹出
        for (int k = 1; k * nums[pos] + sum <= aim; k++)
        {
            path.pop_back();
        }
    }
};

int main()
{
    Solution s;

    vector<int> nums = {2, 3, 6, 7};
    int target = 7;

    vector<vector<int>> ans = s.combinationSum(nums, target);

    cout << "nums = { ";
    for (int x : nums)
    {
        cout << x << " ";
    }
    cout << "}" << endl;

    cout << "target = " << target << endl;
    cout << "满足条件的组合如下:" << endl;

    for (const auto& group : ans)
    {
        cout << "{ ";
        for (int x : group)
        {
            cout << x << " ";
        }
        cout << "}" << endl;
    }

    return 0;
}

8.字母大小写全排列(OJ题)


算法思路:

只需要对英文字母进行处理,处理每个元素时存在三种情况:

  1. 不进行处理;
  2. 若当前字母是英文字母并且是大写,将其修改为小写;
  3. 若当前字母是英文字母并且是小写,将其修改为大写.

递归函数设计:void dfs(int step)

  • 参数:step(当前需要处理的位置);
  • 返回值:无;
  • 函数作用:查找所有有可能的字符串集合,并将其记录在答案列表.

从前往后按序进行递归,递归流程如下:

  1. 递归结束条件:当前需要处理的元素下标越界,表示处理完毕,记录当前状态并返回;
  2. 对当前元素不进行任何处理,直接递归下一位元素;
  3. 判断当前元素是否为小写字母,若是,将其修改为大写字母并递归下一个元素,递归结束时撤销修改操作;
  4. 判断当前元素是否为大写字母,若是,将其修改为小写字母并递归下一个元素,递归结束时撤销修改操作;

核心代码

cpp 复制代码
class Solution
{
    //成员变量
    string path;       //存储当前正在拼接的字符串(回溯路径)
    vector<string> ret; //存储所有最终的结果组合

public:
    //主函数:入口,接收字符串s,返回所有大小写排列
    vector<string> letterCasePermutation(string s)
    {
        dfs(s, 0); //启动DFS,从下标0开始处理字符
        return ret;
    }

    //核心:回溯递归函数
    //s:原始字符串  pos:当前处理的字符下标
    void dfs(string& s, int pos)
    {
        //1.递归终止条件:处理完所有字符,将当前拼接的字符串存入结果
        if(pos == s.length())
        {
            ret.push_back(path);
            return;
        }

        char ch = s[pos]; //取出当前要处理的字符
        
        //选择1:不改变当前字符(数字/字母都可以选这个分支)
        path.push_back(ch);    //把字符加入路径
        dfs(s, pos + 1);       //递归处理下一个字符
        path.pop_back();       //回溯:撤销选择,恢复现场

        //选择2:改变当前字符(**仅字母**可以选这个分支,数字跳过)
        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; //小写转大写(ASCII码差值32)
        } else {
            ch += 32; //大写转小写
        }
        return ch;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Solution
{
    string path;
    vector<string> ret;

public:
    vector<string> letterCasePermutation(string s)
    {
        path.clear();
        ret.clear();

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

int main()
{
    Solution s;
    string str = "a1b2";

    vector<string> ans = s.letterCasePermutation(str);

    cout << "输入字符串: " << str << endl;
    cout << "大小写排列结果如下:" << endl;

    for (const auto& item : ans)
    {
        cout << item << endl;
    }

    return 0;
}

9.优美的排列(OJ题)

算法思路:

我们需要在每一个位置上考虑所有的可能情况并且不能出现重复.通过深度优先搜索的方式,不断地枚举每个数在当前位置的可能性,并回溯到上一个状态,直到枚举完所有可能性,得到正确的结果.我们需要定义一个变量 用来记录所有可能的排列数量,一个一维数组 visited 标记元素,然后从第一个位置开始进行递归;

递归函数设计:void backtrack(int index, int &n)

  • 参数:index(当前需要处理的位置);
  • 返回值:无;
  • 函数作用:在当前位置填入一个合理的数字,查找所有满足条件的排列.

递归流程如下:

  1. 递归结束条件:当 index 等于 n 时,说明已经处理完了所有数字,将当前数组存入结果中;
  2. 在每个递归状态中,枚举所有下标 x,若这个下标未被标记,并且满足题目条件之一:
    a. 将 visited[x] 标记为 1;
    b. 对第 index+1 个位置进行递归;
    c. 将 visited[x] 重新赋值为 0,表示回溯;

核心代码

cpp 复制代码
class Solution
{
    //成员变量
    bool check[16]; //标记数字1~15是否被使用(n最大为15,数组开16足够)
    int ret;        //统计优美排列的总数量

public:
    //主函数:入口,接收n,返回优美排列数量
    int countArrangement(int n)
    {
        dfs(1, n); //启动DFS:从第1个位置开始填充数字
        return ret;
    }

    //核心:回溯递归函数
    //pos:当前正在填充的位置  n:数字最大值
    void dfs(int pos, int n)
    {
        //1.递归终止条件:位置填到 n+1,说明1~n所有位置都填满了 → 找到一个有效排列
        if(pos == n + 1)
        {
            ret++; // 计数+1
            return;
        }

        //2.遍历所有数字1~n,尝试放到当前pos位置
        for(int i = 1; i <= n; i++)
        {
            //两个核心条件:
            //① 数字i未被使用  ② 满足优美排列的规则
            if(!check[i] && (pos % i == 0 || i % pos == 0))
            {
                check[i] = true;   //标记数字i已使用
                dfs(pos + 1, n);  //递归填充下一个位置
                check[i] = false; //回溯:撤销标记,恢复现场
            }
        }
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;

class Solution
{
    bool check[16];
    int ret;

public:
    int countArrangement(int n)
    {
        ret = 0;
        memset(check, false, sizeof(check));
        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; // 恢复现场
            }
        }
    }
};

int main()
{
    Solution s;
    int n = 3;

    int ans = s.countArrangement(n);

    cout << "n = " << n << endl;
    cout << "优美排列的数量为: " << ans << endl;

    return 0;
}

10.N皇后(OJ题)


算法思路:

首先,我们在第一行放置第一个皇后,然后遍历棋盘的第二行,在可行的位置放置第二个皇后,然后再遍历第三行,在可行的位置放置第三个皇后,以此类推,直到放置了 n 个皇后为止.

我们需要用一个数组来记录每一行放置的皇后的列数.在每一行中,我们尝试放置一个皇后,并检查是否会和前面已经放置的皇后冲突.如果没有冲突,我们就继续递归地放置下一行的皇后,直到所有的皇后都放置完毕,然后把这个方案记录下来.

在检查皇后是否冲突时,我们可以用一个数组来记录每一列是否已经放置了皇后,并检查当前要放置的皇后是否会和已经放置的皇后冲突.对于对角线,我们可以用两个数组来记录从左上角到右下角的每一条对角线上是否已经放置了皇后,以及从右上角到左下角的每一条对角线上是否已经放置了皇后.

  • 对于对角线是否冲突的判断可以通过以下流程解决:
    1. 从左上到右下:相同对角线的行列之差相同;
    2. 从右上到左下:相同对角线的行列之和相同.

因此,我们需要创建用于存储解决方案的二维字符串数组 solutions,用于存储每个皇后的位置的一维整数数组 queens,以及用于记录每一列和对角线上是否已经有皇后的布尔型数组 columnsdiagonals1diagonals2.

递归函数设计:
void dfs(vector<vector<string>> &solutions, vector<int> &queens, int &n, int row, vector<bool> &columns, vector<bool> &diagonals1, vector<bool> &diagonals2)

  • 参数:row(当前需要处理的行数);
  • 返回值:无;
  • 函数作用:在当前行放入一个不发生冲突的皇后,查找所有可行的方案使得放置 n 个皇后后不发生冲突.

递归函数流程如下:

  1. 结束条件:如果 row 等于 n,则表示已经找到一组解决方案,此时将每个皇后的位置存储到字符串数组 board 中,并将 board 存储到 solutions 数组中,然后返回;
  2. 枚举当前行的每一列,判断该列、两个对角线上是否已经有皇后:
    (1)如果有皇后,则继续枚举下一列;
    (2)否则,在该位置放置皇后,并将 columnsdiagonals1diagonals2 对应的位置设为 true,表示该列和对角线上已经有皇后:
    (3)递归调用 dfs 函数,搜索下一行的皇后位置.如果该方案递归结束,则在回溯时需要将 columnsdiagonals1diagonals2 对应的位置设为 false,然后继续枚举下一列;

核心代码

cpp 复制代码
class Solution
{
    //核心:三个标记数组(剪枝,快速判断位置是否合法)
    bool checkCol[10];    //标记【列】是否被皇后占用 (n≤9,开10足够)
    bool checkDig1[20];   //标记【左上→右下对角线】是否被占用
    bool checkDig2[20];   //标记【右上→左下对角线】是否被占用
    
    vector<vector<string>> ret;  //存储所有合法的棋盘方案
    vector<string> path;         //存储当前正在拼接的棋盘
    int n;                       //棋盘大小 n×n

public:
    //主函数:入口,接收n,返回所有解法
    vector<vector<string>> solveNQueens(int _n)
    {
        n = _n;
        path.resize(n);  //初始化棋盘有n行
        //初始化棋盘:每一行都是 n 个 '.'(空位置)
        for(int i = 0; i < n; i++)
            path[i].append(n, '.');

        dfs(0);  //从第 0 行开始,递归放置皇后
        return ret;
    }

    //核心:回溯递归函数
    //row:当前正在放置皇后的【行号】
    void dfs(int row)
    {
        //1.递归终止条件:所有行都放完了皇后(0~n-1行)
        if(row == n)
        {
            ret.push_back(path); //保存当前合法棋盘
            return;
        }

        //2.遍历当前行的每一列,尝试放皇后
        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);         //递归:放下一行的皇后

                //3.回溯:恢复现场(撤销选择)
                path[row][col] = '.'; //把皇后变回空位
                //取消标记
                checkCol[col] = checkDig1[row - col + n] = checkDig2[row + col] = false;
            }
        }
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
using namespace std;

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;
        ret.clear();
        path.clear();

        memset(checkCol, false, sizeof(checkCol));
        memset(checkDig1, false, sizeof(checkDig1));
        memset(checkDig2, false, sizeof(checkDig2));

        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] = true;
                checkDig1[row - col + n] = true;
                checkDig2[row + col] = true;

                dfs(row + 1);

                // 恢复现场
                path[row][col] = '.';
                checkCol[col] = false;
                checkDig1[row - col + n] = false;
                checkDig2[row + col] = false;
            }
        }
    }
};

int main()
{
    Solution s;
    int n = 4;

    vector<vector<string>> ans = s.solveNQueens(n);

    cout << "n = " << n << endl;
    cout << "共有 " << ans.size() << " 种解法:" << endl << endl;

    for (int k = 0; k < ans.size(); k++)
    {
        cout << "第 " << k + 1 << " 种解法:" << endl;
        for (const auto& row : ans[k])
        {
            cout << row << endl;
        }
        cout << endl;
    }

    return 0;
}

11.有独的数独(OJ题)


算法思路:

创建三个数组标记⾏、列以及 3*3 ⼩⽅格中是否出现 1~9 之间的数字即可.

核心代码

cpp 复制代码
class Solution
{
    //三个标记数组:O(1) 快速判断数字是否重复
    bool row[9][10];   //row[i][num]:第 i 行,数字 num 是否出现过
    bool col[9][10];   //col[j][num]:第 j 列,数字 num 是否出现过
    bool grid[3][3][10];//grid[x][y][num]:第(x,y)个3×3小九宫格,数字num是否出现过

public:
    bool isValidSudoku(vector<vector<char>>& board)
    {
        //双重循环:遍历数独的每一个格子 (i行, j列)
        for(int i = 0; i < 9; i++)
            for(int j = 0; j < 9; j++)
            {
                //跳过空位置 '.'
                if(board[i][j] != '.')
                {
                    //把字符数字转为整型 1-9
                    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;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;

class Solution
{
    bool row[9][10];
    bool col[9][10];
    bool grid[3][3][10];

public:
    bool isValidSudoku(vector<vector<char>>& board)
    {
        memset(row, false, sizeof(row));
        memset(col, false, sizeof(col));
        memset(grid, false, sizeof(grid));

        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] = true;
                    col[j][num] = true;
                    grid[i / 3][j / 3][num] = true;
                }
            }
        }
        return true;
    }
};

int main()
{
    Solution s;

    vector<vector<char>> board =
            {
                    {'5','3','.','.','7','.','.','.','.'},
                    {'6','.','.','1','9','5','.','.','.'},
                    {'.','9','8','.','.','.','.','6','.'},
                    {'8','.','.','.','6','.','.','.','3'},
                    {'4','.','.','8','.','3','.','.','1'},
                    {'7','.','.','.','2','.','.','.','6'},
                    {'.','6','.','.','.','.','2','8','.'},
                    {'.','.','.','4','1','9','.','.','5'},
                    {'.','.','.','.','8','.','.','7','9'}
            };

    bool ans = s.isValidSudoku(board);

    cout << "当前数独盘面为:" << endl;
    for (const auto& row : board)
    {
        for (char ch : row)
        {
            cout << ch << " ";
        }
        cout << endl;
    }

    cout << endl;
    if (ans)
        cout << "这是一个有效的数独盘面。" << endl;
    else
        cout << "这不是一个有效的数独盘面。" << endl;

    return 0;
}

12.解数独(OJ题)


算法思路:

为了存储每个位置的元素,我们需要定义一个二维数组.首先,我们记录所有已知的数据,然后遍历所有需要处理的位置,并遍历数字1~9.对于每个位置,我们检查该数字是否可以存放在该位置,同时检查行、列和九宫格是否唯一.

我们可以使用一个二维数组来记录每个数字在每一行中是否出现,一个二维数组来记录每个数字在每一列中是否出现.对于九宫格,我们可以以行和列除以3得到的商作为九宫格的坐标,并使用一个三维数组来记录每个数字在每一个九宫格中是否出现.在检查是否存在冲突时,只需检查行、列和九宫格里对应的数字是否已被标记.如果数字至少有一个位置(行、列、九宫格)被标记,则存在冲突,因此不能在该位置放置当前数字.

  • 特别地,在本题中,我们需要直接修改给出的数组,因此在找到一种可行的方法时,应该停止递归,以防止正确的方法被覆盖.

初始化定义:

  1. 定义行、列、九宫格标记数组以及找到可行方法的标记变量,将它们初始化为 false.
  2. 定义一个数组来存储每个需要处理的位置.
  3. 将题目给出的所有元素的行、列以及九宫格坐标标记为 true.
  4. 将所有需要处理的位置存入数组.

递归函数设计:void dfs(vector<vector<char>>& board, int pos)

  • 参数:pos(当前需要处理的坐标);
  • 返回值:无;
  • 函数作用:在当前坐标填入合适数字,查找数独答案.

递归流程如下:

  1. 结束条件:已经处理完所有需要处理的元素.如果找到了可行的解决方案,则将标记变量更新为 true 并返回.
  2. 获取当前需要处理的元素的行列值.
  3. 遍历数字1~9.如果当前数字可以填入当前位置,并且标记变量未被赋值为 true,则将当前位置的行、列以及九宫格坐标标记为 true,将当前数字赋值给 board 数组中的相应位置元素,然后对下一个位置进行递归.
  4. 递归结束时,撤回标记.

核心代码

cpp 复制代码
class Solution
{
    bool row[9][10], col[9][10], grid[3][3][10];

public:
    void solveSudoku(vector<vector<char>>& board)
    {
        // 初始化
        for(int i = 0; i < 9; i++)
        {
            for(int j = 0; j < 9; j++)
            {
                if(board[i][j] != '.')
                {
                    int num = board[i][j] - '0';
                    row[i][num] = col[j][num] = grid[i / 3][j / 3][num] = true;
                }
            }
        }
        dfs(board);
    }

    bool dfs(vector<vector<char>>& board)
    {
        for(int i = 0; i < 9; i++)
        {
            for(int j = 0; j < 9; j++)
            {
                if(board[i][j] == '.')
                {
                    // 填数
                    for(int num = 1; num <= 9; num++)
                    {
                        if(!row[i][num] && !col[j][num] && !grid[i / 3][j / 3][num])
                        {
                            board[i][j] = '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; // 重点理解
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;

class Solution
{
    bool row[9][10], col[9][10], grid[3][3][10];

public:
    void solveSudoku(vector<vector<char>>& board)
    {
        memset(row, false, sizeof(row));
        memset(col, false, sizeof(col));
        memset(grid, false, sizeof(grid));

        // 初始化
        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] = true;
                    col[j][num] = true;
                    grid[i / 3][j / 3][num] = true;
                }
            }
        }

        dfs(board);
    }

    bool dfs(vector<vector<char>>& board)
    {
        for (int i = 0; i < 9; i++)
        {
            for (int j = 0; j < 9; j++)
            {
                if (board[i][j] == '.')
                {
                    // 尝试填数
                    for (int num = 1; num <= 9; num++)
                    {
                        if (!row[i][num] && !col[j][num] && !grid[i / 3][j / 3][num])
                        {
                            board[i][j] = '0' + num;
                            row[i][num] = true;
                            col[j][num] = true;
                            grid[i / 3][j / 3][num] = true;

                            if (dfs(board) == true) return true;

                            // 恢复现场
                            board[i][j] = '.';
                            row[i][num] = false;
                            col[j][num] = false;
                            grid[i / 3][j / 3][num] = false;
                        }
                    }
                    return false;
                }
            }
        }
        return true;
    }
};

void printBoard(const vector<vector<char>>& board)
{
    for (int i = 0; i < 9; i++)
    {
        for (int j = 0; j < 9; j++)
        {
            cout << board[i][j] << " ";
        }
        cout << endl;
    }
}

int main()
{
    Solution s;

    vector<vector<char>> board =
            {
                    {'5','3','.','.','7','.','.','.','.'},
                    {'6','.','.','1','9','5','.','.','.'},
                    {'.','9','8','.','.','.','.','6','.'},
                    {'8','.','.','.','6','.','.','.','3'},
                    {'4','.','.','8','.','3','.','.','1'},
                    {'7','.','.','.','2','.','.','.','6'},
                    {'.','6','.','.','.','.','2','8','.'},
                    {'.','.','.','4','1','9','.','.','5'},
                    {'.','.','.','.','8','.','.','7','9'}
            };

    cout << "求解前的数独:" << endl;
    printBoard(board);

    s.solveSudoku(board);

    cout << endl;
    cout << "求解后的数独:" << endl;
    printBoard(board);

    return 0;
}

13.单词搜索(OJ题)


算法思路:

我们需要假设每个位置的元素作为第一个字母,然后向相邻的四个方向进行递归,并且不能出现重复使用同一个位置的元素.通过深度优先搜索的方式,不断地枚举相邻元素作为下一个字母出现的可能性,并在递归结束时回溯,直到枚举完所有可能性,得到正确的结果.

递归函数设计:
bool dfs(int x, int y, int step, vector<vector<char>>& board, string word, vector<vector<bool>>& vis, int &n, int &m, int &len)

  • 参数:x(当前需要进行处理的元素横坐标),y(当前需要进行处理的元素横坐标),step(当前已经处理的元素个数),word(当前的字符串状态);
  • 返回值:当前坐标元素作为字符串中下标 step 的元素出现是否可以找到成立的字符串;
  • 函数作用:判断当前坐标的元素作为字符串中下标 step 的元素出现时,向四个方向传递,查找是否存在路径结果与字符串相同.

递归函数流程如下:

  1. 遍历每个位置,标记当前位置并将当前位置的字母作为首字母进行递归,并且在回溯时撤回标记.
  2. 在每个递归的状态中,我们维护一个步数 step,表示当前已经处理了几个字母:
    • 若当前位置的字母与字符串中的第 step 个字母不相等,则返回 false;
    • 若当前 step 的值与字符串长度相等,表示存在一种路径使得 word 成立,返回 true.
  3. 对当前位置的上下左右四个相邻位置进行递归,若递归结果为 true,则返回 true.
  4. 若相邻的四个位置的递归结果都为 false,则返回 false.

特别地,如果使用将当前遍历到的字符赋值为空格,并在回溯时恢复为原来的字母的方法,则在递归时不会重复遍历当前元素,可达到不使用标记数组的目的.

核心代码

cpp 复制代码
class Solution
{
    bool vis[7][7];
    int m, n;
public:
    bool exist(vector<vector<char>>& board, string word)
    {
        m = board.size(), n = board[0].size();
        for(int i = 0; i < m; i++)
            for(int j = 0; j < n; j++)
            {
                if(board[i][j] == word[0])
                {
                    vis[i][j] = true;
                    if(dfs(board, i, j, word, 1)) return true;
                    vis[i][j] = false;
                }
            }
        return false;
    }

    int dx[4] = {0, 0, -1, 1};
    int dy[4] = {1, -1, 0, 0};

    bool dfs(vector<vector<char>>& board, int i, int j, string& word, int pos)
    {
        if(pos == word.size()) return true;

        // 向量的方式,定义上下左右四个位置
        for(int k = 0; k < 4; k++)
        {
            int x = i + dx[k], y = j + dy[k];
            if(x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] && board[x][y] == word[pos])
            {
                vis[x][y] = true;
                if(dfs(board, x, y, word, pos + 1)) return true;
                vis[x][y] = false;
            }
        }
        return false;
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
using namespace std;

class Solution
{
    bool vis[7][7];
    int m, n;

public:
    bool exist(vector<vector<char>>& board, string word)
    {
        memset(vis, false, sizeof(vis));

        m = board.size();
        n = board[0].size();

        for (int i = 0; i < m; i++)
        {
            for (int j = 0; j < n; j++)
            {
                if (board[i][j] == word[0])
                {
                    vis[i][j] = true;
                    if (dfs(board, i, j, word, 1)) return true;
                    vis[i][j] = false;
                }
            }
        }
        return false;
    }

    int dx[4] = {0, 0, -1, 1};
    int dy[4] = {1, -1, 0, 0};

    bool dfs(vector<vector<char>>& board, int i, int j, string& word, int pos)
    {
        if (pos == word.size()) return true;

        // 上下左右四个方向
        for (int k = 0; k < 4; k++)
        {
            int x = i + dx[k], y = j + dy[k];
            if (x >= 0 && x < m && y >= 0 && y < n &&
                !vis[x][y] && board[x][y] == word[pos])
            {
                vis[x][y] = true;
                if (dfs(board, x, y, word, pos + 1)) return true;
                vis[x][y] = false; // 恢复现场
            }
        }
        return false;
    }
};

int main()
{
    Solution s;

    vector<vector<char>> board =
            {
                    {'A', 'B', 'C', 'E'},
                    {'S', 'F', 'C', 'S'},
                    {'A', 'D', 'E', 'E'}
            };

    string word1 = "ABCCED";
    string word2 = "SEE";
    string word3 = "ABCB";

    cout << "棋盘如下:" << endl;
    for (const auto& row : board)
    {
        for (char ch : row)
        {
            cout << ch << " ";
        }
        cout << endl;
    }

    cout << endl;

    cout << "查找单词 \"" << word1 << "\": ";
    cout << (s.exist(board, word1) ? "存在" : "不存在") << endl;

    cout << "查找单词 \"" << word2 << "\": ";
    cout << (s.exist(board, word2) ? "存在" : "不存在") << endl;

    cout << "查找单词 \"" << word3 << "\": ";
    cout << (s.exist(board, word3) ? "存在" : "不存在") << endl;

    return 0;
}

14.黄金矿工(OJ题)


算法思路:

枚举矩阵中所有的位置当成起点,来⼀次深度优先遍历,统计出所有情况下能收集到的⻩⾦数的最⼤值即可.

核心代码

cpp 复制代码
class Solution
{
    bool vis[16][16];
    int dx[4] = {0, 0, 1, -1};
    int dy[4] = {1, -1, 0, 0};
    int m, n;
    int ret;

public:
    int getMaximumGold(vector<vector<int>>& g)
    {
        m = g.size(), n = g[0].size();
        for(int i = 0; i < m; i++)
            for(int j = 0; j < n; j++)
            {
                if(g[i][j])
                {
                    vis[i][j] = true;
                    dfs(g, i, j, g[i][j]);
                    vis[i][j] = false;
                }
            }
        return ret;
    }

    void dfs(vector<vector<int>>& g, int i, int j, int path)
    {
        ret = max(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]);
                vis[x][y] = false;
            }
        }
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;

class Solution
{
    bool vis[16][16];
    int dx[4] = {0, 0, 1, -1};
    int dy[4] = {1, -1, 0, 0};
    int m, n;
    int ret;

public:
    int getMaximumGold(vector<vector<int>>& g)
    {
        m = g.size();
        n = g[0].size();
        ret = 0;
        memset(vis, false, sizeof(vis));

        for (int i = 0; i < m; i++)
        {
            for (int j = 0; j < n; j++)
            {
                if (g[i][j])
                {
                    vis[i][j] = true;
                    dfs(g, i, j, g[i][j]);
                    vis[i][j] = false;
                }
            }
        }
        return ret;
    }

    void dfs(vector<vector<int>>& g, int i, int j, int path)
    {
        ret = max(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]);
                vis[x][y] = false; // 恢复现场
            }
        }
    }
};

void printGrid(const vector<vector<int>>& grid)
{
    for (const auto& row : grid)
    {
        for (int x : row)
        {
            cout << x << "\t";
        }
        cout << endl;
    }
}

int main()
{
    Solution s;

    vector<vector<int>> grid =
            {
                    {0, 6, 0},
                    {5, 8, 7},
                    {0, 9, 0}
            };

    cout << "黄金网格如下:" << endl;
    printGrid(grid);
    cout << endl;

    int ans = s.getMaximumGold(grid);

    cout << "最多可以收集的黄金数量为: " << ans << endl;

    return 0;
}

15.不同路径|||(OJ题)


算法思路:

对于四个方向,我们可以定义一个二维数组 next,大小为 4,每一维存储四个方向的坐标偏移量(详见代码).题目要求到达目标位置时所有无障碍方格都存在路径中,我们可以定义一个变量记录 num 当前状态中剩余的未走过的无障碍方格个数,则当我们走到目标地点时只需要判断 num 是否为0即可.在移动时需要判断是否越界.

递归函数设计: void dfs(vector<vector<int>>& grid, int x, int y, int num)

  • 参数: x, y(当前需要处理元素的坐标),num(当前剩余无障碍方格个数);
  • 返回值: 无;
  • 函数作用: 判断当前位置的四个方向是否可以添加至当前状态,查找在满足条件下从起始方格到结束方格的不同路径的数目.

递归流程如下:

  1. 递归结束条件:当前位置的元素值为 2,若此时可走的位置数量 num 的值为 0,则 cnt 的值加一'
  2. 遍历四个方向,若移动后未越界,无障碍并且未被标记,则标记当前位置,并递归移动后的位置,在回溯时撤销标记操作.

核心代码

cpp 复制代码
class Solution
{
    bool vis[21][21];
    int dx[4] = {1, -1, 0, 0};
    int dy[4] = {0, 0, 1, -1};
    int ret;
    int m, n, step;

public:
    int uniquePathsIII(vector<vector<int>>& grid)
    {
        m = grid.size(), n = grid[0].size();

        int bx = 0, by = 0;
        for(int i = 0; i < m; i++)
            for(int j = 0; j < n; j++)
                if(grid[i][j] == 0) step++;
                else if(grid[i][j] == 1)
                {
                    bx = i;
                    by = j;
                }

        step += 2;
        vis[bx][by] = true;
        dfs(grid, bx, by, 1);
        return ret;
    }

    void dfs(vector<vector<int>>& grid, int i, int j, int count)
    {
        if(grid[i][j] == 2)
        {
            if(count == step) // 判断是否合法
                ret++;
            return;
        }

        for(int k = 0; k < 4; k++)
        {
            int x = i + dx[k], y = j + dy[k];
            if(x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] && grid[x][y] != -1)
            {
                vis[x][y] = true;
                dfs(grid, x, y, count + 1);
                vis[x][y] = false;
            }
        }
    }
};

完整测试代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;

class Solution
{
    bool vis[21][21];
    int dx[4] = {1, -1, 0, 0};
    int dy[4] = {0, 0, 1, -1};
    int ret;
    int m, n, step;

public:
    int uniquePathsIII(vector<vector<int>>& grid)
    {
        memset(vis, false, sizeof(vis));
        ret = 0;
        step = 0;

        m = grid.size();
        n = grid[0].size();

        int bx = 0, by = 0;
        for (int i = 0; i < m; i++)
        {
            for (int j = 0; j < n; j++)
            {
                if (grid[i][j] == 0) step++;
                else if (grid[i][j] == 1)
                {
                    bx = i;
                    by = j;
                }
            }
        }

        // 路径总共需要经过:起点 + 所有空格 + 终点
        step += 2;

        vis[bx][by] = true;
        dfs(grid, bx, by, 1);

        return ret;
    }

    void dfs(vector<vector<int>>& grid, int i, int j, int count)
    {
        if (grid[i][j] == 2)
        {
            if (count == step) // 必须恰好走完所有可走格子
                ret++;
            return;
        }

        for (int k = 0; k < 4; k++)
        {
            int x = i + dx[k], y = j + dy[k];
            if (x >= 0 && x < m && y >= 0 && y < n &&
                !vis[x][y] && grid[x][y] != -1)
            {
                vis[x][y] = true;
                dfs(grid, x, y, count + 1);
                vis[x][y] = false; // 恢复现场
            }
        }
    }
};

void printGrid(const vector<vector<int>>& grid)
{
    for (const auto& row : grid)
    {
        for (int x : row)
        {
            cout << x << "\t";
        }
        cout << endl;
    }
}

int main()
{
    Solution s;

    vector<vector<int>> grid =
            {
                    {1, 0, 0, 0},
                    {0, 0, 0, 0},
                    {0, 0, 2, -1}
            };

    cout << "网格如下:" << endl;
    printGrid(grid);
    cout << endl;

    int ans = s.uniquePathsIII(grid);

    cout << "满足条件的不同路径数量为: " << ans << endl;

    return 0;
}

🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容:【递归、搜索与回溯算法】(floodfill算法:从不会做矩阵题,到真正掌握搜索扩散思想)


每日心灵鸡汤:知足者常乐
人生没有幸福不幸福,只有知足和不知足.无病无灾就是福泽,温饱无忧就是幸事.至于其他,有则锦上添花,无则依旧风华. 赚钱的目的是为了更好的生活,而生活的目的不仅仅是赚钱.不要和别人比生活,生活永远比不完.最艰难的时候别总想着太远的将来,只需要鼓励自己熬过现在就好了.不要羡慕任何人的生活,其实谁家的锅底都有灰.不是别人风光无限,而是他们的一地鸡毛没给人看.追求不同,各有活法,路的尽头是什么,也许从来都不重要.

相关推荐
我不是懒洋洋2 小时前
【数据结构】栈和链表基本方法的实现
c语言·开发语言·数据结构·c++·链表·青少年编程·ecmascript
邪修king2 小时前
C++ vector 超全攻略:核心知识点、STL 生态联系与避坑指南
c语言·c++·面试
小江的记录本2 小时前
【网络安全】《网络安全与数据安全核心知识体系》(包括数据脱敏、数据加密、隐私合规、等保2.0)
java·网络·后端·python·算法·安全·web安全
SimpleLearingAI2 小时前
ROPE:大模型必学操作
人工智能·算法
zore_c2 小时前
【C++】C++类和对象实现日期类项目——时间计算器!!!
java·c语言·数据库·c++·笔记·算法·排序算法
草莓熊Lotso2 小时前
Linux 线程同步与互斥(二):线程同步从条件变量到生产者消费者模型全解,原理 + 源码彻底吃透
linux·运维·服务器·c语言·开发语言·数据库·c++
人道领域2 小时前
【LeetCode刷题日记】:344,541-字符串反转字符串反转技巧:双指针原地交换法
算法·leetcode·面试
Crazy________2 小时前
4.13docker仓库registry
mysql·算法·云原生·eureka
澈2072 小时前
C++ string操作指南:从入门到精通
数据结构·c++·算法