阿伟在搞蓝桥杯的时候,属实是被动态规划按在地上摩擦、吊在树上打,看的懂答案、但是做的时候老是白给,于是我赶紧去找大佬的文章去读一下(可以看我下面的参考资料),下面是我自己的一些见解,希望在自己成长的同时,可以帮助到有需要的人。该文章用leetcode《53. 最大子序和》作为开始,leetcode《5. 最长回文子串》进阶和强化,leetcode《887. 鸡蛋掉落》做魔鬼训练,实际代码以及代码模板均用伪代码,后面有题目类型总结(看来源),建议点个小星星作为收藏,方便练习
白话解动态规划
1. 动态规划究竟是要干什么
动态规划就是暴力解决问题(穷举法),但是动态规划是练过军体拳的、一拳一个嘤嘤怪(穷举的优化),一般的穷举法是乱拳打老师傅、一大通王八拳完事(直接暴力)。动态规划的核心思想就是用一个备忘录将子问题的最优解全部叠加起来,将一个个小的问题进行叠加变成大问题,叠到最后就是整个问题的最优解。说的通俗一点就是:大事化小,小事化了,一般而言,他解决的求最值问题具有以下三个特征:
- 暴力穷举的时候,存在重叠子问题:通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,具有天然剪枝的功能,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。比较人话一点:上一个解决的子问题,可以用作下一个子问题的参考答案(又不是西方国家抗疫,抄都不会抄)。有的直接就抄,所以用一个备忘录来记录下解决过的东西,再遇到直接抄就完事了,从而减低了时间复杂度。
- 子问题存在最优子结构:每一个小的部分都可以找到各自在这个阶段的最优解,最后每一个小的子问题的最优解叠加起来起到质变得到最终解。
- 存在状态转移方程:上一个子问题可以通过某种方式(加上一些东西)变成下一个子问题的答案。
如果将已经学过的东西进行纵向对比的话,我觉得所谓的动态规划特别像高中数学的送(na)分(ming)题——等差数列里面通过A1和d确定An的值
其实动态规划的代码并不长,正所谓浓缩的就是精华,但是他的状态转移方程构建和重叠子问题的消除就很难,没什么关系,下面会有三个思考步骤,帮助构建状态转移方程。在这里,我不会去弄教科书里面什么自顶向下的办法之类的,那种花里胡哨的,我是怎么实用怎么来,在熟悉了动态规划的思考方式之后,我会再回到概念上加深理解。
2. 动态规划的思考步骤
其实leetcode题目medium难度已经是面试正常题目的难度了,hard难度的话都是面试中比较难的题目,所以medium难度多做一道作为训练,hard只有一道,想多做的话建议自己看后面的有生之年系列去练习
其实动态规划的三个步骤和“等差数列里面通过A1和d确定An的值”的求解思路,区别不大,思想都是找到“d”和“a0”,确定ak。(下标不好打,看间隔)
- 定义子问题的最优解(ak-2与ak-1)的含义(我是谁),动态规划毕竟不是等差数列,ak-2与ak-1并不是一个具体的数字,在解题中需要确定ak-2与ak-1是什么形式,是一个最长距离还是一个true or false 的flag,那么才好放到备忘录中消除重叠子问题,也方便确定最后解的形式。
- 找到“ak+1 = ak + d”这个方程式,确定子问题之间的递推关系(我要到哪里去),放在等差数列中就是找d,这就需要一点数学知识,稳住,这是最难的一部分了,但是稳住别慌,问题不大在涉及到递归的状态转移方程:有一个用于递归的思维框架(这个我是看Github项目《labuladong/fucking-algorithm》知道的,在解决确实好用):
明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」-> 明确 base case。 - 找到初始值a0(我从哪里来),由零开始一层层往上走。
有了初始值,并且有了数组元素之间的关系式,那么我们就可以得到 dp[n] 的值了,而 dp[n] 的含义是由你来定义的,你想求什么,就定义它是什么,这样,这道题也就解出来了。
那么好,我们用例题进行讲解,因为确实干写干读实在太过于枯燥了。
3. 最大子序列和(入门难度,了解思维方式)
来源:leetcode《53. 最大子序和》
3.1 题目描述
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
很巧的是这题有视频讲解,我可能讲的不太好,可以看这个视频:
官方题解,那里就有,一点开就可以了,b站链接:53. 最大子序和 Maximum Subarray 【LeetCode 力扣官方题解】
3.2 那么好,这里用我的三个步骤进行分析
来,拿出我们的小纸纸,对着例题来进行分析。
1.定义子问题的最优解(ak-2与ak-1)的含义。(我是谁)
题目问的是:给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。那么在这里直接找第k个数所对应的最大子序列之和不就完事了吗,用例子算一下先,嗯,怎么:
4 -1 2 1 -5找到的最大子序列是4 -1 2 1 ,4 -1 2 1 -5 4 找的还是 4 -1 2 1
这样子问题就重复了,想一想先,为什么重复,重复就是因为我停下来之后还使劲往前蹦跶,做别人的事情,那不是累死自己吗,那如果我直接在4 -1 2 1 -5 4的4那里刹住车,我也不往前面跳,我就踏踏实实的管住我门前的三亩地(也就是只管2 1 -5 4又或者是-1 2 1 -5 4,而不是4 -1 2 1)那不就ok了,如果每个数都管好自己的三亩地,并且把这个最大值记录下来,就不需要再往前跳(重复计算),我就可以少走一点路,趴在床上刷手机不妙吗,最后我再把这些记录来个全部比较大小不就完事了吗
所以:找到当前位置第k个数结尾所对应的最大连续子序列之和,最后直接返回a0,a1……an之间最大值ak。
比较动态的过程可以看上面视频。
2.找到“ak+1 = ak+ d”这个方程式,确定子问题之间的递推关系。(我要到哪里去)
来看一下ak与ak-1
ak = 当前位置第k个数结尾所对应的最大连续子序列之和(nums[i]……nums[k])
ak+1 = 当前位置第k个数结尾所对应的最大连续子序列之和(nums[i]……nums[k],nums[k+1])
那么可以看到,两者之间的差别就在多了个nums[k+1],而ak的设定是当前位置第k个数结尾所对应的最大连续子序列之和,那么就比较num[k+1]单独成为一段与ak+nums[k+1]连接成段之间的大小,选取两者之间最大的那一个作为a[k]
所以得出状态转移方程:a[k+1] = max(ak + nums[k+1],nums[k+1])。
3.找到初始值a0,由零开始一层层往上走。(我从哪里来)
这个就简单了,初始值a0 = nums[0]
3.3 那么好,经过三个思维步骤确定了状态转移方程,就可以直接打伪代码了
1. MAX-SUB-ARRAY(nums)
2. maxAnswer = nums[0] //k = 0的情况
3.
4. //当前位置第k个数结尾所对应的最大连续子序列之和(nums[0]……nums[k])
5. pre = 0
6.
7. for i = 0 to the length of the nums,i++
8. pre = max(pre+nums[i],nums[i])
9. maxAnswer = max(maxAnswer,pre)
10.