【算法】递归、回溯、剪枝、dfs 算法题练习(组合、排列、总和问题;C++)

文章介绍了如何使用深度优先搜索(DFS)结合决策树的方法解决括号生成、目标和和组合总和等排列和子集问题。通过递归和剪枝优化算法,展示了如何在C++中实现这些解决方案。
摘要由CSDN通过智能技术生成

1. 前言

后面的练习是接着下面链接中的文章所继续的,在对后面的题练习之前,可以先将下面的的文章进行了解👇:

【算法】{画决策树 + dfs + 递归 + 回溯 + 剪枝} 解决排列、子集问题(C++)

2. 算法题

22.括号生成

在这里插入图片描述


思路

  • 题意分析:要求根据给出的数字,算出合法的括号组成个数。根据题目,我们可以总结出下面的规则:
    在这里插入图片描述
  • 解法dfs + 根据决策树设计递归、回溯、剪枝
  • 决策树
    在这里插入图片描述
    • 根据上图决策树,即可直接着手编写代码:
    • 细节问题
      在这里插入图片描述

代码

class Solution {
public:
    int left, right, _n; // 分别记录左右括号的数量
    vector<string> ret; // 结果数字
    string path;
    vector<string> generateParenthesis(int n) {
        _n = n;
        dfs();
        return ret;
    }

    void dfs()
    {
        if(right == _n) // 当前path序列有效,加入结果
        {
            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--;
        }
    }
};

494.目标和

在这里插入图片描述

思路

  • 题意分析:对于数组给出的数字,通过向所有数字前补加减号,使最后的值为 target
  • 解法dfs + 根据决策树设计递归、回溯、剪枝
    • 该解法并非最优解法,但这里依然用dfs做,并引出一些写法问题。
    • 决策树:(省略了后面的步骤)

在这里插入图片描述
- 根据决策树的思路,不难看出实际上与 题目 78.子集 非常相似,代码也是如此,但有一个需要注意的细节,先看代码:

代码1

class Solution {
public:
    int ret, path, _target;
    
    int findTargetSumWays(vector<int>& nums, int target) {
        _target = target;
        dfs(nums, 0); // 从0位置开始
        return ret;
    }

    void dfs(vector<int>& nums, int pos)
    {
        // 终止条件
        if(pos == nums.size())
        {
            if(path == _target) ret++;
            return;
        }

        // 正号
        path += nums[pos];
        dfs(nums, pos + 1);
        path -= nums[pos];

        // 负号
        path -= nums[pos];
        dfs(nums, pos + 1);
        path += nums[pos];
    }
};
  1. 最开始我们提到,深搜dfs并非该题的最优解法, 时间开销很大,对于上面的代码,更是面临超时的风险,上面的代码将 path作为全局变量
  2. 我们可以 进行优化:将path作为参数传递
  3. 更改后的代码,时间开销会小一些

代码2

class Solution {
public:
    int ret, _target;
    
    int findTargetSumWays(vector<int>& nums, int target) {
        _target = target;
        dfs(nums, 0, 0); // 从0位置开始
        return ret;
    }

    void dfs(vector<int>& nums, int pos, int path)
    {
        // 终止条件
        if(pos == nums.size())
        {
            if(path == _target) ret++;
            return;
        }

        // 正号
        dfs(nums, pos + 1, path + nums[pos]);

        // 负号
        dfs(nums, pos + 1, path - nums[pos]);
    }
};

path定义形式的理由

  1. 频繁的执行 + - 操作,是一比不小的时间消耗,直接传参可以只需要将计算后的结果直接作为参数递归
  2. 而对于 [78.子集],我们将path定义为全局变量,因为对于该题情况,path 本身为vector类型,作为参数传递会导致频繁的创建vector,不利于节省时间。

39.组合总和

在这里插入图片描述

思路

  • 题意分析:即通过给定的数组任意分配,组成和为目标值的序列,求序列的种类。
  • 解法dfs + 根据决策树设计递归、回溯、剪枝

解法1

  • 决策树:如图所示:
    • 即每次分支都进行所有数字的选择,符合条件的继续,由于题目要去重,同层下选过的不再复选
    • 剪枝:同层的选过的剪掉 + 该路径和超出目标值或该位置超出数组范围的剪掉
      在这里插入图片描述

代码

class Solution {
public:
    int _target;
    vector<int> path;
    vector<vector<int>> ret;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        _target = target;
        dfs(candidates, 0, 0);
        return ret;
    }

    void dfs(vector<int>& nums, int pos, int pathSum)
    {
        // 路径和等于目标值,记录结果,向上返回
        if(pathSum == _target)
        {
            ret.push_back(path);
            return;
        }
        // 路径和大于目标值 / 当前位置超出数组; 向上返回
        if(pathSum > _target || pos >= nums.size()) return;

        for(int i = pos; i < nums.size(); ++i)
        {
            path.push_back(nums[i]);
            dfs(nums, i, pathSum + nums[i]);
            path.pop_back();
        }
    }
};

解法2

  • 决策树
    在这里插入图片描述

对于上图,有一个需要注意的细节问题:
- 对于恢复现场:

在这里插入图片描述

此时我们可以编写代码:

代码

class Solution {
public:
    int _target;
    vector<int> path;
    vector<vector<int>> ret;

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        _target = target;
        dfs(candidates, 0, 0);
        return ret;
    }

    void dfs(vector<int>& nums, int pos, int pathSum)
    {
        // 路径和等于目标值,记录结果,向上返回
        if(pathSum == _target)
        {
            ret.push_back(path);
            return;
        }
        // 路径和大于目标值 / 当前位置超出数组; 向上返回
        if(pathSum > _target || pos >= nums.size()) return;

        for(int k = 0; k * nums[pos] <= _target; ++k)
        {
            if(k) path.push_back(nums[pos]); // 枚举 当前值的个数,添加到数组中
            dfs(nums, pos + 1, pathSum + k*nums[pos]);
        }

        // 恢复现场
        for(int k = 1; k * nums[pos] <= _target; ++k)
        {
            // 将每个元素添加过的个数值 都恢复
            path.pop_back();
        }
    }
};

784.字母大小写全排列

在这里插入图片描述

思路

  • 题意分析:即返回所有转换字母大小写后的字符串
  • 决策树:如下图所示
    • 当当前位置是字母时,进行变与不变的分支,当是数字时,直接继续,无分支,最后叶子节点处即为结果。
      在这里插入图片描述

代码

class Solution {
public:
    string path; // 记录当前字母序列
    vector<string> ret;

    vector<string> letterCasePermutation(string s) {
        dfs(s, 0);
        return ret;
    }

    void dfs(string s, int pos)
    {
        if(path.size() == s.size())
        {
            ret.push_back(path);
            return;
        }

        char ch = s[pos];
        // 不改变的情况:数字 以及 字母的第一种情况
        path += ch;
        dfs(s, pos + 1);
        path.pop_back();

        if(isalpha(ch)) // 需要改变  
        {
            // 改变大小写
            if(ch <= 'z' && ch >= 'a') ch -= 32;
            else ch += 32;

            path += ch;
            dfs(s, pos + 1);
            path.pop_back();
        }
    }
};

526. 优美的排列

在这里插入图片描述

思路

  • 题意分析:即找出所有的符合规则(优美排列)的排列
  • 解法利用全排列 与 规则
    • 决策树:这里不再画决策树了,相当于全排列的思路
    • 全排列的思路即:如果该位置未被检索过,则加入path中并继续dfs
    • 这里我们增加一条判定,即当前位置的下标与perm[i]满足整除关系时,才进行dfs

代码

class Solution {
public:
    bool used[16];
    int ret;

    int countArrangement(int n) {
        dfs(1, n); // 从下标为1处填n个数
        return ret;
    }

    void dfs(int pos, int n)
    {
        if(pos == n + 1){ // 更新结果
            ret += 1;
            return;
        }

        // 全排列 + 判断是否符合优美排列
        for(int i = 1; i <= n; ++i)
        {
            if(!used[i] && (i % pos == 0 || pos % i == 0))
            {
                used[i] = true;
                dfs(pos + 1, n);
                used[i] = false; // 恢复现场
            }   
            
        }
    }
};
  • 22
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值