动态规划入门及经典基础算法(js实现)
一、思想与性质
- 动态规划的核心思想是把原问题分解成子问题进行求解,也就是分治的思想。
那么什么问题适合用动态规划呢?我们通过一个现实中的例子,来理解这个问题。大家可能在公司里面都有一定的组织架构,可能有高级经理、经理、总监、组长然后才是小开发,今天我们通过这个例子,来讲讲什么问题适合使用动态规划。又到了一年一度的考核季,公司要挑选出三个最优秀的员工。一般高级经理会跟手下的经理说,你去把你们那边最优秀的3个人报给我,经理又跟总监说你把你们那边最优秀的人报给我,经理又跟组长说,你把你们组最优秀的三个人报给我,这个其实就动态规划的思想!
首先是重叠子问题,不同的问题,可能都要求1个相同问题的解。
其次是最优子结构,最优解肯定是有最优的子解转移推导而来,子解必定也是子问题的最优解。
第三是无后效性,这个问题可能比较难理解,也就是求出来的子问题并不会因为后面求出来的改变。
二、动画规划过程
- 划分状态,即划分子问题,例如上面的例子,我们可以认为每个组下面、每个部门、每个中心下面最优秀的3个人,都是全公司最优秀的3个人的子问题
- 状态表示,即如何让计算机理解子问题。上述例子,我们可以实用f[i][3]表示第i个人,他手下最优秀的3个人是谁。
- 状态转移,即父问题是如何由子问题推导出来的。上述例子,每个人大Leader下面最优秀的人等于他下面的小Leader中最优秀的人中最优秀的几个。
- 确定边界,确定初始状态是什么?最小的子问题?最终状态又是什么。例如上述问题,最小的子问题就是每个小组长下面最优秀的人,最终状态是整个企业,初始状态为每个领导下面都没有最优名单,但是小组长下面拥有每个人的评分。
三、经典模型
1.线性模型
斐波那楔数列:每个数的值都是一个状态,可以用F[i]表示表示第i个数的值是多少,每个数都是由F[i-1]+F[i-2]转移而来
最长上升自序列(LIS):有一串序列,要求找出它的一串子序列,这串子序列可以不连续,但必须满足它是严格的单调递増的且为最长的。把这个长度输出。示例:1 7 3 5 9 4 8 结果为4
线性模式还可以拓展成二维问题,例如背包问题,用f[i][j]表示前i个物品,凑成大小为j的背包,最大的价值是多少。
这类问题非常的多,但是思路都是这样,无非就是从左往右,从上到下,从低维到高维进行转移。
2.区间模型
- 对于每个问题,都是由子区间推导过来的,我们称之为区间模型,下面是一个例子:
我们有一个连续的序列,每个序列上面都是一个数字c[i],每次我们都能够消灭一个连续的回文子序列,消灭之后左右会合并,成为一个新序列,问最少需要多少次才能够把整个序列消灭掉。回文就是从左到有从右到左读到的序列都是一样的。题目比较抽象,我们通过一些例子来说明这个问题吧?例如一开始的序列是1 4 4 2 3 2 1,那么我们最少需要2次,先消灭掉4 4 , 然后再消灭调1 2 3 2 1.第二个例子是 1 2 3 4 5 5 3 1,我们先消灭掉2 然后再消灭掉4, 最后消灭 1 3 5 5 3 1, 需要3次。
我们经常用f[i][j]来表示消灭i,j区间需要的代价
3.树状模型
我们在数据结构树上面进行最求最优解、最大值等问题,上述我们讲的这个绩效考核就是一个树状模型。
四、实现的方法
一个是自底向上,另外一个是自顶向下。无论是何种方式,我们都要明确动态规划的过程,把状态表示、状态转移、边界都考虑好。
1、自底向上
简单来说就是根据初始状态,逐步推导到最终状态,而这个转移的过程,必定是一个拓扑序。
自底向上一般用来解决什么问题呢?那就是可以轻松确定拓扑序的问题,例如线性模型,都是从左往右进行转移,区间模型,一般都是从小区间推导到大区间。自底向上的一个经典实现是斐波那楔数列的递推实现,即F[i] = F[i - 1] + F[i - 2] 。
2、自顶向下
也就是从最终状态出发,如果遇到一个子问题还未求解,那么就先求解子问题。如果子问题已经求解,那么直接使用子问题的解,所以自顶向下动态规划又有一个形象生动的名字,叫做记忆化搜索,一般我们采用递归的方式进行求解。
自顶向下,我们一般用在树上面,因为我们根据父亲结点,很容易找到所有的子问题,也就是所有的子结点,而自底向上的话,我们要去统计这个结点的所有兄弟结点是否已经实现。会稍微复杂一点,而且比较难理解。
五、经典入门算法及代码实现
1、爬楼梯
LeetCode #70 : 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到n楼呢?
动态规划过程:
1、划分状态:将爬n阶楼梯的问题划分为n个子问题,由于“ 每次你可以爬 1 或 2 个台阶“,所以第i个台阶的结果可能是由第i-1个台阶爬1个台阶或者是由第i-2个台阶爬2个台阶所得
2、状态表示:将i层阶梯的结果记作f(i),i-1层阶梯则为f(i-1),i-2层阶梯为f(i-2)
3、状态转移:f(i) = f(i−1) + f(i−2) ,它意味着爬到第 xx 级台阶的方案数是爬到第 x - 1x−1 级台阶的方案数和爬到第 x - 2x−2 级台阶的方案数的和
4、确定边界:从第 00 级爬到第 00 级我们可以看作只有一种方案,即f(0)=1;从第 00 级到第 11 级也只有一种方案,即爬一级,f(1) = 1。根据转移方程得到 f(2) = 2 ,f(3) = 3 ,f(4) = 5 …
这是一个线性模型(斐波那楔数列),采用自底向上从0到n递推最后的结果,以下为代码实现:
/**
1. @param {number} n
2. @return {number}
*/
var climbStairs = function(n) {
if(n<=2)
return n;
let arr = [1,2];
for(let i = 2;i<n;i++){
arr[i] = arr[i-1] + arr[i-2];
}
return arr[n-1];
};
LeetCode 面试题 08.01. 三步问题
三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。
以下为代码实现:
/**
* @param {number} n
* @return {number}
*/
var waysToStep = function(n) {
let arr = [1,2,4],t = 1000000007;
if(n<=3)
return arr[n-1];
for(let i = 3;i<n;i++){
arr[i] = (arr[i-1] + arr[i-2] + arr[i-3] )%t;
}
return arr[n-1] ;
};
2、连续子数组的最大和
LeetCode 剑指 Offer 42:输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)
自底向上,代码实现:
/**
* @param {number[]} nums
* @return {number}
*/
var maxSubArray = function(nums) {
for(let i = 1;i<nums.length;i++){
nums[i] = Math.max(nums[i],nums[i]+nums[i-1]);
}
return Math.max(...nums)
};
3、使用最小花费爬楼梯
/**
* @param {number[]} cost
* @return {number}
*/
var minCostClimbingStairs = function(cost) {
let f1 = 0,f2 = 0;
for(let i = cost.length-1;i>=0;i--){
let f0 = cost[i] + Math.min(f1,f2);
f2 = f1;
f1 = f0;
}
return Math.min(f1,f2) ;
};
4、按摩师
/**
* @param {number[]} nums
* @return {number}
*/
var massage = function(nums) {
let cur = 0,pre = 0;
for(let i =nums.length-1;i>=0;i--){
let t = Math.max(cur,pre+nums[i]);
pre = cur;
cur = t;
}
};
5、打家劫舍
/**
* @param {number[]} nums
* @return {number}
*/
var rob = function(nums) {
let cur = 0,pre = 0;
for(let i = nums.length-1;i>=0;i--){
let temp = Math.max(pre+nums[i],cur);
pre = cur;
cur = temp;
}
return Math.max(cur,pre)
};
6、买卖股票的最佳时机
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function(prices) {
if(prices.length<=1)
return 0;
let min = prices[0];
let res = 0;
for(let i = 1;i<prices.length;i++){
if(prices[i]<min){
min = prices[i];
continue;
}
res = res>(prices[i]-min)?res:prices[i]-min;
}
return res;
};