本文正在参加「金石计划」
前言
在做题的过程中一定经常听说 dp dp,那么什么是 dp。dp 是 Dynamic Programming 动态规划的简称,用于解决一个问题有很多重叠的子问题,简单的来说,就是一个问题可以有两个小的问题组成,这比较类似于分治的思想,举一个最简单的例子--斐波那契数列
斐波那契数列为 0,1,1,2,3,5,8... 等等,不难发现,在这个数列当中,第n的数是由前两个数相加得到的,这就是典型的动态规划思路,f(n) = f(n-1) + f(n-2)
简单介绍
动态规划的题型有特别多,比如说 背包问题,子序列问题,股票问题,这在leetcode上有非常多的经典题目,但是动态规划始终逃不过的几个点就是:
-
你需要去定义动态规划保存状态的变量,
-
确定每一个状态之间的关系,
-
状态变量的初始值是什么,
-
如何去遍历状态转化的过程,
-
推导最后的状态值
剑指 Offer 10- I. 斐波那契数列
经典题目
斐波那契数列 : 0 1 1 2 3 5 8
f(n) = f(n-1) + f(n-2)
求第 n 个斐波那契数是多少
然后我们根据几个步骤来完成这道递归题目:
- 定义动态规划保存状态的变量
在动态规划当中,一半保存状态的变量都是使用数组,当然这不是一定的,在这道题当中,我们就可以定义 dp数组,并且 dp[i] 代表第 i 个斐波那契数的值,dp[0] 代表第 0 个数,也就是 0,dp[1] 为 1,dp[2] 为 1,以此类推。
- 确定每一个状态之间的关系,
也就是确定递推公式,这道题的递推公式题目已经告诉我们了,就是 f(n) = f(n-1) + f(n-2)
- 状态变量的初始值是什么
根据斐波那契数列的前几个值,以及递推公式 f(n) = f(n-1) + f(n-2) ,我们可以发现,前两个数由于不存在-1和-2的位置,所以是没有办法通过公式推导出来的,这时候就需要我们初始化数组的时候赋予初始值
let dp = [0,1]
-
如何去遍历状态转化的过程 按照递推公式,由第三个数开始往下进行计算遍历,当前的数是由前两个数相加得到的。
-
推导最后的状态值
当输入需要的第 n 个 斐波那契数列的值得时候,我们就可以通过 递推公式 进行拆分,拆成一个个小的值相加。
为什么我们需要dp数组?从题目给的递推公式来看,似乎是一个递归就能够解决的问题,也确实,递归公式可以这样写。
function climbStairs1(n) {
if(n==0) {
return 0
}
if(n==1) {
return 1
}
return climbStairs1(n-1) + climbStairs1(n-2)
};
但是这样的递归树状图为
我们会发现,很多个斐波那契数都是会重复计算的,比方说 f(2) 它是由 f(1) + f(0) 得到的,但是在递归的过程中每次碰到 f(2) 都会去递归成 f(1) + f(0),那么我们是不是可以用一个数组,把当前的斐波那契数保存下来,在下次碰到的时候,直接先判断数组中是否含有,没有再去递归获取,这样就能够大大减少递归树的分支,这也就是dp数组的作用--保存递归结果
如此一来,递归图就变成了
很明显大幅减少了计算的过程。
所以利用dp数组,我们能够得到题解:
function climbStairs2(n) {
let map = new Map()
// 新建表存放数据
const dfs = (n) => {
if(n==0) {
return 1
}
if(n==1) {
return 2
}
if(map.get(n)) {
// 表中已经存在,直接返回
return map.get(n)
}else {
// 表中不存在进行计算
const t = dfs(n-1) + dfs(n-2)
map.set(n,t)
return t
}
}
return dfs(n)
};