动态规划 第1讲: 一维动态规划

这篇文章参考了以下博客或者公众号:
1、动态规划图解 作者:NFGC(Leetcode题解)

之前 我写了一篇博客:动态规划 第0讲:基础入门课
对动态规划算法进行了大致的讲解。
接下来两周,我们将会一步一步、由浅到深来分析各个类型的题目。
首先,当然是最简单的一维动态规划算法了。

这篇文章包含了LeetCode的以下题目:
一、70. 爬楼梯 (难易程度:easy)
二、91. 解码方法 (难易程度:medium)
三、96. 不同的二叉搜索树 (难易程度:medium)
四、53. 最大子序和 (难易程度:easy)
五、152. 乘积最大子数组 (难易程度:medium)
六、198.打家劫舍(难易程度:easy)

一、70. 爬楼梯 (难易程度:easy)

原题链接:https://leetcode-cn.com/problems/climbing-stairs/
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。

示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
one. 1 阶 + 1 阶
two. 2 阶

示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
one. 1 阶 + 1 阶 + 1 阶
two. 1 阶 + 2 阶
three. 2 阶 + 1 阶

解题思路:

这是十分经典的斐波那契数列题。定义一个数组 dp,dp[i] 表示走到第 i 阶的方法数。因为我们每次可以走一步或者两步,所以第 i 阶可以从第 i-1 或 i-2 阶到达。换句话说,走到第 i 阶的方法数即为走到第 i-1 阶的方法数加上走到第 i-2 阶的方法数。这样我们就得到了状态转移方程dp[i] = dp[i-1] + dp[i-2]。注意边界条件的处理。
首先,很自然地想到用**动态规划(递归)**去求解:
动态规划 第1讲: 一维动态规划_第1张图片
看看代码:

class Solution {
public:
    int climbStairs(int n) {
        int ans;
        if(n<=2) return n;
        return climbStairs(n-1)+climbStairs(n-2); //递归
    }
};

但是呢,超时了:
动态规划 第1讲: 一维动态规划_第2张图片
好,我们把递归改成递推试试看:

class Solution {
public:
    int climbStairs(int n) {
        int ans;
        if(n<=2) return n;
        vector<int>dp(n+1, 0);
        dp[1]=1; dp[2]=2;
        for(int i=3; i<=n; i++) //递推
           dp[i]=dp[i-1]+dp[i-2];
        return dp[n];
    }
};

好,改成递推之后就通过了!

二、91. 解码方法 (难易程度:medium)

原题链接: https://leetcode-cn.com/problems/decode-ways/
一条包含字母 A-Z 的消息通过以下方式进行了编码:
‘A’ -> 1
‘B’ -> 2

‘Z’ -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。

示例 1:
输入: “12”
输出: 2
解释: 它可以解码为 “AB”(1 2)或者 “L”(12)。

示例 2:
输入: “226”
输出: 3
解释: 它可以解码为 “BZ” (2 26), “VF” (22 6), 或者 “BBF” (2 2 6) 。

解题思路:

这道题和70.爬楼梯是同一个类型的问题
难点在于其有很多corner case,只要避开这些corner case就可以完美解题了:
每次递进,可以选取一个数也可以选取两个数
(1)s[i] != ‘0’
如果 s[i-1]s[i] <= 26, 则 dp[i] = dp[i-1] + dp[i-2];
如果 s[i-1]s[i] > 26, 则 dp[i] = dp[i-1], 这是因为 s[i-1]s[i] 组成的两位数无法翻译。
(2)s[i] == ‘0’
如果 s[i-1]s[i] <= 26,(即s[i-1]等于’1’或者’2’), 则 dp[i] = dp[i-2], 这是因为 s[i] 无法翻译。
如果s[i-1]大于等于’3’ 或者 s[i-1]=0,则没有结果,return 0.
去除这些限制条件,此题就是爬楼梯的问题了,一次可以爬一步,也可以爬两步,问有多少中方式到达终点。
好,一起来看一下代码:

class Solution {
public:
    int numDecodings(string s) {
        //如果0个阶梯,返回0步
        if(s[0]=='0') return 0;
        int n=s.length();
        vector<int>dp(n+1);
        dp[0]=1,dp[1]=1;
        for(int i=1; i<n; i++)
        {
            if(s[i]=='0') //s[i] == '0'
            {
                //如果 s[i-1]s[i] <= 26, 则 dp[i] = dp[i-2], 这是因为 s[i] 无法翻译
                if(s[i-1]=='1'|| s[i-1]=='2')
                dp[i+1]= dp[i-1];
                //相邻的两个 ‘0’,或者大于等于30,则没有结果
                else
                return 0;
            }
            else //s[i] != '0'
            {
                //如果 s[i-1]s[i] <= 26, 则 dp[i] = dp[i-1] + dp[i-2]
                if (s[i - 1] == '1' || (s[i - 1] == '2' && s[i] <= '6'))
                dp[i+1]= dp[i]+dp[i-1];
                //如果 s[i-1]s[i] > 26, 则 dp[i] = dp[i-1], 这是因为 s[i-1]s[i] 组成的两位数无法翻译
                else
                dp[i+1]= dp[i];
            }
        }
        return dp[n];
    }
};

结果好快啊:
动态规划 第1讲: 一维动态规划_第3张图片

三、96. 不同的二叉搜索树 (难易程度:medium)

原题链接: https://leetcode-cn.com/problems/unique-binary-search-trees/
动态规划 第1讲: 一维动态规划_第4张图片

解题思路:

首先,我们回顾一下动态规划的一个最重要的内容:
为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间。

0- 给一个整数n,则有序序列 为1,2,3 ,…,n-1,n;
根据序列构建一棵二叉搜索树,我们可以从1开始,遍历并形成以i(1<= i <=n)为根节点的二叉树。左子树为 i-1(即1,2,…,i-1), 右子树为 n-i(即i+1,…,n-1,n)
动态规划 第1讲: 一维动态规划_第5张图片
1- ans[i]等于1…n所有情况的总和;
2- 1…n的其中一种情况(第i个)为:ans(i−1)⋅ans(n−i);
3- 结合1和2,得出递推公式:ans(n)= (i=1)∑n ans(i−1)⋅ans(n−i);
4- 边界处理。注意:ans[0]=1,而不是0,这样方便后面的运算。ans[1]=1.
好我们来看一下代码:

class Solution {
public:
    int numTrees(int n) {
        vector<int>ans(n+1);
        ans[0]=1; //易错:0的时候是1,而不是0,这样方便后面的运算
        ans[1]=1;
        //从2开始,“递推”求ans
        for(int i=2; i<=n; i++)
        {
            //ans[n]= 从1开始求和(G(i−1)⋅G(n−i))
            for(int j=1; j<=i; j++)
            {
                ans[i]+= ans[j-1]*ans[i-j];
            }
        }
        return ans[n];
    }
};

这次也好快,C++牛逼!
动态规划 第1讲: 一维动态规划_第6张图片

四、53. 最大子序和 (难易程度:easy)

原题链接: https://leetcode-cn.com/problems/maximum-subarray/

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

解题思路:

这道题不难没有什么好说的。
至于分治法,我会在以后的博客中补上去。
那就看一下代码吧:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int ans; //ans为当前最大值
        ans=nums[0];
        int max_ans=ans; //max_ans为整体最大值
        for(int i=1; i<nums.size(); i++)
        {
            //计算当前最大值
            if(ans > 0)
            ans = nums[i]+ ans ;
            else
            ans =nums[i];
            max_ans= max(ans, max_ans); //更新整体最大值
        }
        return max_ans;
    }
};

五、152. 乘积最大子数组 (难易程度:medium)

原题链接: https://leetcode-cn.com/problems/maximum-product-subarray/

给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。

示例 2:
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。

解题思路:

这道题和上一题(53. 最大子序和)很相似。
但是乘法的性质不像加法那样,累加结果只要是正的一定是递增,乘法中有可能现在看起来小的一个负数,后面跟另一个负数相乘就会得到最大的乘积。
我们只需要在维护一个局部最大(即正数的最大绝对值)的同时,再维护一个局部最小(即负数最大的绝对值),这样如果下一个元素遇到负数时,就有可能与这个最小相乘得到当前最大的乘积和,这也是利用乘法的性质得到的。
还有一点要注意,因为数组中可能有0的存在,所以比较大小时要与当前值比较。比如说[0,5,4],在 5 时,不仅要max(mul_pos, mul_minus),还要比较当前值,即pos_max= max(max(mul_pos, nums[i]), mul_minus).

好,我们来看一下代码:

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int ans=nums[0];
        int pos_max=ans; //最大值(正数的最大绝对值)
        int minus_max=ans; //最小值(负数的最大绝对值)
        for(int i=1; i<nums.size(); i++)
        {
            int mul_pos=pos_max*nums[i]; //和最大值相乘
            int mul_minus=minus_max*nums[i]; //和最小值相乘
            pos_max= max(max(mul_pos, nums[i]), mul_minus);
            minus_max= min(mul_pos, min(mul_minus, nums[i]));

            if(ans<pos_max)
            ans=pos_max;
        }
        return ans;
    }
};

六、198. 打家劫舍(难易程度:easy)

原题链接: https://leetcode-cn.com/problems/house-robber/

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:
0 <= nums.length <= 100
0 <= nums[i] <= 400

解题思路:

这道题比较简单,用普通的动态规划就可以了。
但是,注意数组的边界

错误1:LeetCode–runtime error: reference binding to null pointer of type ‘struct value_type’(stl_vector.h)
解决方法:
if(nums.empty()) return 0;

错误2:LeetCode == 45==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000018 at pc 0x000
解决方法:
这是数组越界了的错误。因为有可能nums只有1个数,而我直接访问了nums[1](即第二个数),所以越界了。加一个if判断语句就可以了。

好,我们来看一下代码:

class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.empty()) return 0; 
        vector<int>value(nums.size()+1);
        value[0]=nums[0];
        if(nums.size()>=2)
        value[1]=max(nums[0], nums[1]);
    
        for(int i=2; i<nums.size(); i++)
        {
            value[i]=max(value[i-1], value[i-2]+nums[i]);
        }
        return value[nums.size()-1];
    }
};

你可能感兴趣的:(Leetcode算法笔记)