LeetCode-算法(六)——递归+动态规划

本文详细介绍了LeetCode中涉及递归和动态规划的算法问题,包括组合、全排列、字母大小写全排列等。通过递归回溯的方法解决组合和全排列问题,利用动态规划求解爬楼梯、打家劫舍和三角形最小路径问题。文章深入浅出地阐述了递归和动态规划的基本思想和优化技巧,是理解这两种算法的宝贵资源。
摘要由CSDN通过智能技术生成

第十一天-递归

77.组合

在这里插入图片描述
组合和排列是经典的递归回溯问题,并且可以利用剪枝的技巧对其进行优化。
方法1:非剪枝的递归回溯方法
组合不考虑顺序,从给定的N个数字中选定M个进行组合,输出组合的数字。

1. 组合需要选数,排列不需要
2. 那么可以考虑成当前位置的数选还是不选
3. 选,则数加入组合,不选则直接跳过
4. 当组合内数量满M时,输出组合并return
5. 当所有的数都经过选与不选的抉择后,也进行return
6. 回溯之后需要状态恢复,进入另一个状态分支——不选当前的数

考虑清楚上面几个问题,则很容易写出以下代码:

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> temp;
    void dfs(int cur, int n, int k)
    {
        if(temp.size()==k)//选满k个数进行回溯
        {
            ans.push_back(temp);
            return;
        }
        if(cur==n+1)//当前指针越界也回溯
        {
            return;
        }
        temp.push_back(cur);//选择当前的数
        dfs(cur+1, n, k);
        temp.pop_back();//状态恢复
        dfs(cur+1, n, k);//不选择当前的数
    }
    vector<vector<int>> combine(int n, int k) {
        dfs(1, n, k);
        return ans;
    }
};

方法2:剪枝优化

1. 考虑到存在大量不必要的判断,可以对当前状态进行判断,提前剪枝
2. 剪枝条件:当后续的数字全选依然无法满足组合中存在M个数时,该状态可以提前剪枝
3. 可以证明,该剪枝条件中包含了指针越界的情况

则代码可以优化为:

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> temp;
    void dfs(int cur, int n, int k)
    {
        //提前剪枝,当后续的数字全选也不够K个时,直接返回
        if(temp.size()+(n-cur+1)<k)
        {
            return;
        }
        if(temp.size()==k)
        {
            ans.push_back(temp);
            return;
        }
        temp.push_back(cur);
        dfs(cur+1, n, k);
        temp.pop_back();
        dfs(cur+1, n, k);
    }
    vector<vector<int>> combine(int n, int k) {
        dfs(1, n, k);
        return ans;
    }
};

46.全排列

在这里插入图片描述
方法一:next_permutation
第一种方法属于利用函数作弊的方法,next_permutation可以返回当前排列的下一个字典序排列,由于刚开始给出的不一定是从小到大的最小字典序,所以需要利用sort进行排序后,调用next_permutation进行输出

class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>>ans;
        sort(nums.begin(), nums.end());
        do//注意这里需要使用do-while结构将最开始的排列也加入
        {
            ans.push_back(nums);
        }while(next_permutation(nums.begin(), nums.end()));
        return ans;
    }
};

方法二:递归+回溯

1. 全排列只考虑数的位置
2. 如何获取全部的顺序?利用递归+循环+回溯实现
3. 建立一个visit数组记录所有位置的元素的访问情况
4. 每次需要选择元素时,将没有访问过的元素按从小到大的顺序加入,然后visit置为true
5. 当元素符合N个时表明当前排列完成,则进行输出,并return
6. 回溯是将该元素的visit对应位置修改为false,并从排列中删除
例子演示:
1234的全排列
首先第一个排列没有回溯,输出1234
回溯后4的visit变为false,此时排列中为123
由于4的循环到头所以循环退出,到3的循环中
此时对3进行回溯,visit置为false,排列中剩余12
3的循环没有走完,所以下一个位置4未被访问,则4进入排列中,visit变为true,此时排列为124
新的递归中对没有标记的元素遍历时,只有3没被访问,则入排列,visit变为true,此时排列1243
元素满足排列数量,则输出,进行新一轮的回溯
……
class Solution {
public:
vector<int>temp;
vector<bool>visit;
vector<vector<int>>ans;
    void dfs(vector<int>&nums, int n)
    {
        if(temp.size()==n)
        {
            ans.push_back(temp);
            return;
        }
        for(int i=0;i<n;i++)
        {
            if(!visit[i])//寻找未在当前排列中的数字
            {
                temp.push_back(nums[i]);
                visit[i] = true;
                dfs(nums, n);//递归调用
                visit[i] = false;//回溯
                temp.pop_back();
            }
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        int n = nums.size();
        visit.resize(n, false);//reshape
        dfs(nums, n);
        return ans;
    }
};

784.字母大小写全排列

在这里插入图片描述
全排列的变形,通过大小写的转换来生成不同的排列,分析则有:

1. 对于大写字母:两种选择——不变或者变为小写字母
2. 对于小写字母:两种选择——不变或者变为大写字母
3. 对于数字:一种选择——不变

排列的多样主要通过大小写的变化而得到
所以需要写分条件的递归和回溯

4. 当前字符为大小写字母时,需要进行递归和回溯——类似组合中的选与不选的回溯
5. 当前字符为数字时,直接加入即可
6. 当字符的个数符合排列的数量时,输出并返回
class Solution {
public:
    vector <string> ans;
    void huisu(string s, int n, int cur)
    {
        if(cur==n+1)
        {
            ans.push_back(s);
            return;
        }
        if(s[cur]>='a'&&s[cur]<='z')//分情况对于当前字符进行递归和回溯
        {
            huisu(s, n, cur+1);
            s[cur] = s[cur]-32;//转换为大写
            huisu(s, n, cur+1);
        }
        else if(s[cur]>='A'&&s[cur]<='Z')
        {
            huisu(s, n, cur+1);
            s[cur] = s[cur]+32;//转换为小写
            huisu(s, n, cur+1);
        }
        else
        {
            huisu(s, n, cur+1);//不变直接递归
        }
    }
    vector<string> letterCasePermutation(string s) {
        int n = s.size();
        huisu(s, n, 0);
        return ans;
    }
};

第十二天-DP

动态规划把握好三个方面:

 - 大问题转换为小问题——每一个小问题的解是否容易求得?是否存在关系?(问题转换)
 - 动态规划的初始条件——最小规模下的问题的解?一般在DP之前进行定义(最小规模)
 - 动态规划的状态转移方程——由小问题之间的递推关系求得(状态转换)

70.爬楼梯

在这里插入图片描述
我们从上述的三个方面问题进行分析:
假设当前仅有三层台阶

  • 最终的问题:从底层到达第三层台阶有几种方法?
规模更小的问题:
到第一层台阶有几种方法?
到第二层台阶有几种方法?
……
到第i-1层台阶有几种方法?
  • 最小规模问题:到第一层台阶有几种方法?——很简单只有一种
  • 状态转移方程:
    我们可以考虑,到第二层台阶有几种方法?——很简单只有两种,一次一步和一次两步
    那么到第三层台阶能够有几种方法呢?
可以想象:
当前站在第一层台阶时,一次跨两步就可以到第三层;
当前站在第二层台阶时,一次跨一步就可以到第三层;
那么从底层到第三层的方法就应该是到第一层台阶的方法+第二层台阶的方法
从前面的状态推得当前状态就可以利用DP方法实现
——一个疑问,从第一层台阶到第三层还可以两次跨一步,为什么不算上呢?
——其实这种方法在上到第二层台阶时已经计算过一次,所以不需要重复计算

所以递推公式可以写成如下形式:

dp[i] = dp[i-1]+d[i-2]

是的就是大家非常熟悉的斐波那契数列

class Solution {
public:
    int climbStairs(int n) {
        if(n==1)
            return 1;
        if(n==2)
            return 2;
        int a = 1;
        int b = 2;
        int ans;
        while((n--)-2)
        {
            ans = a + b;
            a = b;
            b = ans;
        }
        return ans;
    }
};

198.打家劫舍

在这里插入图片描述
题目的大概意思是求得最大的不连续数字之和。
最开始的想法是不连续且最大,那不就是奇数和偶数位置相加比较不就行了——当然不行
比如例子:4,1,1,4,最大的不连续数字之和应该是8,而不是奇偶位置。

1.规模缩放:
只有一个屋子时偷窃的最高金额?
只有两个屋子时偷窃的最高金额?
……
只有n个屋子时偷窃的最高金额?
2.初始状态:
只有一个屋子时,则偷窃的最高金额为该屋子中的金额,用dp数组表示有i个屋子时偷窃的最高金额,则
dp[0] = nums[0]
3.状态转移方程:
由于条件规定无法偷窃相邻的房间,所以计算含i个屋子时的偷窃的最高金额是的状态转移方程:
dp[i] = max{dp[i-2]+nums[i], dp[i-1]}——用以表示当前屋子是否偷取
class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if(n==1)
            return nums[0];
        if(n==2)
            return max(nums[0], nums[1]);
        vector<int> dp(n);
        dp[0] = nums[0];
        dp[1] = max(nums[1], dp[0]);//状态初始化
        for(int i=2;i<n;i++)
        {
            dp[i] = max(nums[i]+dp[i-2], dp[i-1]);
        }
        return dp[n-1];
    }
};

本题可以利用滚动数组对空间进行优化,仅保存第i-1和第i-2个dp结果即可

120.三角形最小路径

在这里插入图片描述
二维DP,作为背包问题的一个入门热身题目,也是非常经典的DP题目之一

给定三角形数据的自顶向下的最短路径,同样也是先计算i-1层的最短路径,在此基础上计算第i层的最短路径,规定只能从相邻节点往下,也就规定了状态转移方程的写法

初始条件:第一层的最短路径就是其本身dp[0][0] = triangle[0][0]
状态转移方程:
d[i][j] = max{triangle[i][j]+d[i-1][j], triangle[i][j]+d[i-1][j-1]}
从上一层相邻的两个元素往下计算
这里需要注意边界条件,当元素处于该行的开头或结尾时,只存在一个上层的相邻元素
if(j==0)
d[i][j] = triangle[i][j]+d[i-1][j]
if(j==col-1)
d[i][j] = triangle[i][j]+d[i-1][j-1]

空间优化——滚动数组:可以知道当前状态仅和上一层状态有关,则可以利用两个长度为n的一维数据存储新旧结果,在每一层结果计算之后,将结果进行更新即可

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int n = triangle.size();
        vector<int> dp(n, 0);//存储第i层到当前元素的最短路径
        int ans = INT_MAX;
        if(n==1)
        {
            return triangle[0][0];
        }
        dp[0] = triangle[0][0];//状态初始化
        for(int i=1;i<n;i++)
        {
            vector<int> temp(n, 0);//临时存储计算结果
            for(int j=0;j<triangle[i].size();j++)
            {
                if(j!=0&&j!=triangle[i].size()-1)
                {
                    temp[j] = min(dp[j]+triangle[i][j], dp[j-1]+triangle[i][j]);
                    //cout<<dp[j]<<endl;
                }
                else if(j==0)
                {
                    temp[j] = dp[j]+triangle[i][j];
                    //cout<<dp[j]<<endl;
                }
                else
                {
                    temp[j] = dp[j-1]+triangle[i][j];    
                    //cout << dp[j]<<endl;
                }
            }
            for(int t=0;t<temp.size();t++)//将结果进行复制
                dp[t] = temp[t];
        }
        for(auto &it:dp)//从最后一层的结果中挑选最小的路径长度返回
        {
            if(it<ans)
                ans = it;
        }
        return ans;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

国家一级假勤奋研究牲

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值