符合人类思维的动态规划

4,701 阅读7分钟

焦虑唠嗑

首先声明一下,我没有卖焦虑,是我自己焦虑了。其次,要感谢我的师傅,西湖区最帅.....

好了,言归正传,在leetCode评论区你都可以看到 lucifer,简称路西法大佬;他的题解才是符合人类思维的思考方式,思考的点都是很深入浅出的。

为什么要刷算法呢?因为现在大前端时代,而前端开发确实是比其它领域要稍微简单一点的,注意,我并没有说前端领域简单,是入门相对简单。入门简单,就意味着入门的人会非常的多,那怎么在众多的初级前端中脱颖而出呢?我选择了刷算法,刷算法有以下几点好处。

  1. 在遇到需要算法的业务需求时候,可以完全不虚。
  2. 可以锻炼自己的逻辑思维,锻炼内功。
  3. 可以更快的学习一些新技术。

到底什么是动态规划

你说的我都会,我也能看得懂,为什么一说/看就会,一写就费呢?

因为大多数题解,甚至是leetcode评论区的题解,只会告诉答案,不会告诉你思考答案的方式。或者有些会告诉你怎么思考,但是他们都是经验丰富的dp选手,思考方式完全不适合新手,一个动态转移方程莫名其妙的就出来了???满脸的黑人问号,我曾经也是这样走过来的,再次感谢路西法大佬的指点。

到底我们要以什么方式来学习动态规划呢?我的建议是硬着头皮先刷点简单的,层层递进,再刷点困难的;刷题的过程中千万不能浮躁,不要为了AC而AC,而是要通过自己耐心的观察,抽象,练习,归纳总结;递归直至理解dp,熟悉dp。真的没有什么快捷的方式,如果有,都是骗人的。这里,结合实战来带大家过一遍,用符合人类的思考方式完全扒开动态规划的裤子,学习(gandiao)动态规划。

如果你不熟悉动态规划,或者完全不了解,我建议你先看看我上一篇 动态规划 的文章。

53.最大子序和

好了,我们直接上菜,先来看看 题目 ;

            

什么样的题目适合用动态规划?

不要多想,我们先以符合人类最简单的思维方式暴力求解,再根据状态树考虑以下两点。

那这题怎么暴力呢?直接枚举nums,以nums[i]为起点,不断的加到最后一位,加的过程中维护一个最大值即可,我写下代码。千万不要看不起暴力求解,是dp的突破口!


我们来看下这个暴力的状态树,我只画出前面最长的两个分支,其它自行脑补。

       

分析一下这个暴力状态树两个分支,很明显答案都是是 [4,-1,2,1] ,后一个分支比前一个分支少一个-2,也就是问题的规模变小了,答案依然是最优的,这就存在最优子结构

我们在算第二个分支的时候,其实前面第一个分支已经算过了,这就是重复子问题

一般这种最值型,都比较适合dp来求解;如何能快速的用dp求解,就靠你自己去攒经验了。

定义状态是动态规划的定海神针

状态定义的对不对,直接是决定了你的dp方程式对不对,从而决定了你dp的方式对不对;

【定义状态】之前,我们先要搞清楚两个东西,一个是【状态】,一个是【选择】

  • 状态:一般直接是题目给定的条件,本题给定的条件就是nums,那每个状态就是nums[i]
  • 选择:对于每个状态,有几种选择?本题的nums[i]就有两种情况,要么选择nums[i]做为结果,要么就不选择,如果不选择,因为要求连续,就是另起炉灶嘛。

注意:这里的【状态】【定义状态】是两个东西,【状态】是题目给出的条件,会影响结果的条件,而定义【定义状态】是为了明确dp方程式的含义,以便后面根据状态的变化,利用数学归纳法得出状态转移方程,说白了就是找规律。

举个例子,x + y = z,你必须要明确你的x,y,z是啥,你才能写出这样的状态转移方程;

到这里,我们明确了【状态】【选择】,而定义好转移方程的状态;我们必须要有以下两个意识。

  1. 化成子问题(问题规模变小)去想,去思考。对应这题,我们不妨把数组逐渐变小了想。
  2. 最后一步是什么,也就是最后一个解;(最优策略中使用的最后一个选择)

以题目为例,nums = [-2,1,-3,4,-1,2,1,-5,4],最后一步是什么?最优策略中使用的最后一个选择是什么?很明显这道题的最后一个选择是nums[6] = 1,如果【不选择】1,上面我们说过了,就是另起炉灶;如果选择1,答案就是最终的 [4,-1,2,1];

根据以上的两点,结合明确的两个东西,一个是【状态】,另一个【选择】

  1. 状态:很明显就是nums[i],每个数就是一个状态
  2. 状态选择:对于每个状态nums[i],我们有两个选择,要么是选择nums[i],要么是不选择

到这里,状态的定义我们就可以很明确的得出来了。

dp[i] = x,表示以 nums[i] 结尾的最大子序和为x; i < nums.length

状态转移方程

一定要想清楚状态的定义,状态的定义直接是决定了你的转移方程对不对,dp的姿势对不对;

根据上面状态的定义分析,我们来找下规律(数学归纳法);nums[i] 是一个个的状态;对于每个状态,我们可以选择,或者不选择,如果选择以nums[i]为结果,那答案就是 nums[i] + dp[i-1],因为要连续,所有得加上前面的;如果不选择,就是另起炉灶,以nums[i]开头;枚举所有的状态,取两种选择的最大值,不就是答案了吗?这就是状态转移方程了;

dp[i] = max(nums[i],dp[i - 1] + nums[i]),i > 1

初始条件和边界

因为我们枚举所有的状态,取两种选择的最大值就是答案,所以边界就是数组长度;初始值是什么呢?很明显就是数组本身,即dp[i] = nums[i];

代码

一定要明确上面的状态定义,转移方程,边界和初始值才开始写代码,有一点不明白都不能写代码,不然基本一写就费。想清楚了写代码也要非常细心。


152.乘积最大子数组

我们用同样的套路解决乘积最大子数组


暴力求解就不解释了,同上;

先来明确 【状态】 和 【选择】,这题同样的,状态就是一个个 nums[i],而对于每个 nums[i] 状态,同样用是选或不选两个选择;但是这题有一个比较隐晦的条件,需要考虑进去,就是两个数相乘:

  • 如果是最大值(假设正数)乘以一个负数,就是最小值
  • 如果最小值(假设负数)乘以一个负数,就是最大值

那我们在枚举所有的状态时候,根据这两个条件,不断的维护最大最小值,遇到负数就乘以最小值,遇到正数就是乘以最大值即可。结果只和最大最小值有关系,那我们枚举状态不断更新这两个值就可以求出答案了,其实如果是遇到0,情况也是一样的;

状态的定义:

  • imin = min(min(nums[i] * imax, nums[i] * imin), nums[i]) 
  • imax = max(max(nums[i] * imax, nums[i] * temp), nums[i])
tips:这里要注意,temp = 上次的 imin。

初始条件就是第一个数nums[i]

这里直接给出代码了。QAQ


总结

我觉的dp是最能体现代码功底的,为什么呢?因为它难。

一定要多练习,看懂了只是我懂了,你要真懂必须多练;

另外推荐几个我觉得写得还不错的文章和视频,没有打广告QAQ

九章dp视频

一个还不错的文章

一个很强的B站博主