引用 leetcode 的一段话,我认为它讲很权威,我将结合实战带你学习动态规划。java
看得很懵吧?懵就对了,我当初接触动态规划的时候,也懵了好久。可是,只有咱们搞清楚如下几个问题,动态规划其实也不是那么的难。(三维四维DP难到怀疑人生QAQ)git
仍是有点懵?懵就对了,我详细解释一下。算法
对于状态的定义,其实就是找题目给定的条件,限制的条件。就拿经典的 爬楼梯 来举例。typescript
首先咱们收集题目限制的条件,一个是每次只能够爬1个台阶,或者2个台阶。另一个是须要n阶到达楼顶;那么咱们状态的定义确定只和这两个条件有关系。这不是废话嘛,这真不是废话,有时候解题的关键点之一就是在于找准状态的定义;数组
那这题的状态的定义该怎么定义呢?很明显就是爬n个台阶到达楼顶的不一样方法。bash
即 dp = 爬到楼顶的不一样方法markdown
转移方程,也是dp的难点之一,仍是继续以爬楼梯为例;其实dp最难也就是前两点了;把状态的定义和状态转移方程肯定以后,dp也就迎刃而解了。oop
其实状态转移方程也就是数学概括法,听着很高大上吧?其实它就是找规律而已。学习
关键是怎么找规律呢?授之以鱼不如授之以渔。编码
直接根据求解的问题,拆分红子问题,规模更小的问题来思考;好比爬楼梯,爬n台阶很懵吧?我相信没有接触过dp的同窗都会很懵,别怕,咱们从规模小的问题来思考,获得结果,从而递推出规律,也就是状态转移方程;
仔细观察上面的数据,由问题的规模不断的变大,结果也会逐渐的变大,那它们有什么规律呢?细心观察的小伙伴确定会发现 f(n) = f(n-1) + f(n-2),这不就是最熟悉的斐波那契数列问题了吗?到这里,状态转移方程就是 f(n) = f(n-1) + f(n-2) { n > 2 }
咱们把状态的定义和状态转移方程肯定以后,初始条件就很简单了,就直接观察状态转移方程知足条件的起始值,其实就是 n > 2,当知足这个条件的时候,公式才成立,不知足公式的条件,就是初始条件或者边界,也就是当 n <= 2 的结果,就是初始条件;咱们很容易就能够想出来 f(1) = 1, f(2) = 2;有时候相对难一点的dp,须要利用状态转移方程来肯定,后面解释。
得出dp三个须要肯定的条件以后,咱们就能够根据这三个条件来写代码了
var climbStairs = function(n) { // 状态:dp = 爬到楼顶的不一样方法 // 边界: fn(1) = 1,fn(2) = 2 // 动态方程: fn(n) = fn(n-1) + fn(n-2) if(n < 3) return n let fn_1 = 1 let fn_2 = 2 let res = 0 for(let i = 3; i <= n; i++){ res = fn_1 + fn_2 fn_1 = fn_2 fn_2 = res } return res };复制代码
当你彻底弄懂了股票系列问题,你才算得上真正的入门动态规划问题。在此以前,有个大神的文章写得很是好,可是是java版本的,我在学习的过程当中也发现了一点小错误,挣扎了好久,弄明白以后写下此文,对动态规划学习作一个总结。我但愿你能先认真的过一边原文,原文很长,你必须耐心的看懂里面讲的问题,无需纠结里面的细节,下面我将带领你解决这些细枝末节的东西,真正的入门动态规划。原文连接
我假设你已经看过原文,大概弄懂了做者讲什么,先来复习看这张状态图,有的小伙伴确定会问?他是怎么获得这张状态定义图的?上面爬楼梯我说过了,是根据题目的限制条件,抽取出来的。
以上两点就是最关键的状态定义,同时结合能够交易的次数k,和第i天的股票价格,状态的具体定义能够这么来:
很显然,咱们想求的最终答案是 dp[n - 1][K][0],即最后一天,最多容许 K 次交易,最多得到多少利润。为何不是 dp[n - 1][K][1]?由于 [1] 表明手上还持有股票,[0] 表示手上的股票已经卖出去了,很显而后者获得的利润必定大于前者。
最关键的步骤,也是难点之一,可是对于状态转移方程,咱们能够根据状态的定义,转变得出,仔细观察上面的状态转换图,买卖股票的操做,咱们能够得出持有股票,或未持有股票的两个状态转移。若是还不明白,回头看买卖的状态图。
根据这个状态的转换,咱们就能够得出状态转移方程,也就是数学概括法得出通用公式,[ 这里要注意卖出股票会获取利润,买入股票须要支付成本的问题 ]。为何 k-1 呢?由于当咱们买入一次股票以后交易次数就要减 1。
到这里,状态转移方程就写出来了,难点在于考虑买卖股票的状态图,根据状态机得出转移方程,不过,我认为这都是熟练度的问题,你只要把dp的本质理清楚,剩下的就是多练了。
股票问题的初始条件和边界,会有点隐晦,不像爬楼梯那么直白明了。我上面也说过,比较难的咱们能够经过状态转移方程直接代入进去,而后得出,咱们来尝试一下。
综上所述,咱们就能够得出初始条件和边界值了,即当 i - 1 == -1的时候
当 k= 0 的时候,也就是还未交易,原理也是同样的
const maxProfit = function(k, prices) { // 交易天数 let n = prices.length; // 最大交易次数,k不影响状态转移方程,此处去掉 let maxTime = k; if(n == 0){ return 0; } // 初始化三维数组 // 若是当题 k 不影响状态转移方程,此处初始化去掉 let dp = Array.from(new Array(n),() => new Array(maxTime+1)); for(let i = 0;i < n;i++){ for(let r = 0;r <= maxTime;r++){ dp[i][r] = new Array(2); } } // 若是当题k不影响状态转移方程,则只需二维数组 // let dp = Array.from(new Array(n),() => new Array(2)); // 枚举递推 for(let i = 0;i < n;i++){ // 若是当题k不影响状态转移方程,内循环去掉 for(let k = maxTime;k >= 1;k--){ if(i == 0){ // 边界条件处理 continue; } // 递推公式,上面分析的状态转移方程 dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) } } // 返回结果 return dp[n-1][maxTime][0]; // 若是当题k不影响状态转移方程返回此结果 // return dp[n-1][0]; };复制代码
122.买卖股票的最佳时机 II
188.买卖股票的最佳时机 IV
dp问题须要多练,简单的一维二维还好,难度不大,其中三维思惟就很考验熟练度和数学建模的抽象能力了,不得不认可有些是天赋型选手,咱们普通人,多练就好。
其实练算法最大的好处,就是提高编码能力。我在半年以前一点都不懂算法的,写业务也会偶尔卡壳,以前遇到一个排列组合的业务问题也彻底懵逼。在通过半年的思惟提高之后,业务代码我彻底能够切菜式的完成,并且代码写得也比以前要好不少。对于困难一点,复杂一点的组件封装,也可以封装得很好。甚至,能够本身封装一个UI库。因此,算法真的颇有用,扎实编码功底的最佳选择。
另外,推荐阅读大神的算法小抄,连接。