文章首发于:github.com/USTB-musion…前端
如今竞争愈来愈激烈,以往前端算法面试只问问排序的日子一去不复返了。如今大厂喜欢问一些进阶性的算法问题,好比今天要聊的面试中常常出现但理解起来有些困难的一种算法思想——「动态规划」。git
先看下几个常见的面试题:github
上面这些问题是很是常见的动态规划的题目,你能够先思考一下如何回答上边的问题🤔,而后带着答案来阅览接下来的内容。面试
动态规划,英文是Dynamic Programming,简称DP,擅长解决“多阶段决策问题”,利用各个阶段阶段的递推关系,逐个肯定每一个阶段的最优决策,并最终获得原问题的最优决策。算法
先来看一道面试题:假如楼梯有n个台阶,每次能够走1个或2个台阶,请问走完这n个台阶有几种走法❓具体如何分析这道题目,能够看下笔者前段时间写的文章:聊一聊前端算法面试——递归数组
function climbStairs(n) {
if (n == 1) return 1
if (n == 2) return 2
return climbStairs(n-1) + climbStairs(n-2)
}
复制代码
暴力递归这种方法通俗易懂,可是很是低效,咱们能够来看下它的递归树:缓存
这个递归树怎么理解?这是一种自顶向下的方法,咱们想求出f(10),得先求出子问题f(9)和f(8),而且知足f(10)=f(9)+f(8),同理可得f(9)=f(8)+f(7),f(8)=f(7)+f(6),······f(3)=f(2)+f(1)。最后遇到f(2)或者f(1)时,一颗完整的递归树就出来了,这其实就是一个二叉树。bash
递归算法的时间复杂度怎么求?子问题个数乘与解决一个子问题所须要的时间数据结构
从上图中能够看出,随着问题规模的增加,这是一个指数级别的算法,时间复杂度为O(2^n)。从上图f(10)为例,暴力递归有大量的子问题被重复计算。f(7)被计算了2次,f(6)被计算了4次,而上层的每一次计算更是把底层的f(1)和f(2)都计算了,能够看出这是一种及其低效的作法。那有木有什么改进的方法呢❓post
既然暴力递归低效的根本缘由是有大量的子问题被重复计算,那能不能把这些子问题缓存起来呢?把这些子问题放在特定的数据结构里,当计算某个子问题时,先去这个数据结构里查一下,若是原来有缓存,则直接返回。若是原来没有缓存,则把这个子问题缓存起来,方便下次使用。这样就能优化暴力递归低效的缘由了。
var calculated = []
function climbStairs(n) {
if(n == 1) {
return 1
}else if (n == 2) {
return 2
}else {
if(!calculated[n-1]){
calculated[n-1] = climbStairs(n-1)
}
if(!calculated[n-2]){
calculated[n-2] = climbStairs(n-2)
}
return calculated[n-1] + calculated[n-2]
}
}
复制代码
咱们来看一下时间复杂度为多少?经过memorize操做,把巨大的递归树进行“剪枝”操做,把须要重复计算的子问题都缓存起来,没有冗余的计算,时间复杂度和问题规模成正比,即为O(n)。
动态规划须要知足3个条件:最优子问题,边界条件和状态转移方程
f(10)=f(9)+f(8),就是f(10)问题的最优子问题,若是求出f(9)和f(8)的最优子问题,那么就是f(10)的最优子问题了
动态规划是自顶向下的设计思想,以爬楼梯为例,最后分解到底层的边界条件就是f(1)=1,f(2)=2。
其实,动态规划最难的步骤就是写出状态转移方程,那么如何来写出状态转移方程呢?状态转移方程能够理解是描述数学问题的数学方程式,对于爬楼梯问题来讲,能够发现其状态转移方程为 f[i]=f[i-1]+f[i-2],从最开始的1和2个台阶两个状态开始,自底向上进行求解:
function climbStairs(n) {
var val = [];
for ( var i = 0; i <= n ; ++i) {
val[i] = 0
}
if (n <= 2) {
return n
} else {
val[1] = 1
val[2] = 2
for (var i = 3; i <= n; ++i) {
val[i] = val[i-1] + val[i-2]
}
return val[n]
}
}
console.log(climbStairs(10)) // 55
复制代码
以下图所示:一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。如今考虑网格中有障碍物。那么从左上角到右下角将会有多少条不一样的路径❓
这是一道leetcode的原题,若是你在面试中遇到这道题,该怎么应答呢?还记得上面说的动态规划三要素吗❓
动态规划是自底向上的思想,可能跟大部分人的思路是相反的。如上图所示,咱们想达到终点F(7*3),无非有两种状况,一种是F(7*2)向下走一步,一种是F(6*3)向右走一步。因此咱们能够得出打到终点的最优子问题是F(7*2)和F(6*3)。
根据最优子问题的分析,容易想出状态转移方程为F(m*n) = F((m-1)*n) + F(m*(n-1))。
var uniquePathsWithObstacles = function(arr) {
// arr为二维数组,m为行,n为列
let n = arr.length, m = arr[0].length;
let temp = [];
// 初始化将格子填充为0
for (let i = 0; i < n; i++) {
temp[i] = Array(m).fill(0)
}
// 若是起始或终止目标有障碍物,则直接返回0
if (arr[0][0] == 1 || arr[n - 1][m - 1] == 1) {
return 0
}
// 遍历二维数组的列数
for (i = 0; i < n; i++) {
// 遍历二维数组的行数
for (let j = 0; j < m; j++) {
if (i == 0 && j == 0) {
temp[i][j] = 1;
// 第一种边界状况:1行n列
} else if (i == 0) {
if (arr[i][j] != 1 && temp[i][j - 1] != 0) {
temp[i][j] = 1;
} else {
temp[i][j] = 0;
}
// 第二种边界状况: m行1列
} else if (j == 0) {
if (arr[i][j] != 1 && temp[i - 1][j] != 0) {
temp[i][j] = 1;
} else {
temp[i][j] = 0;
}
} else if (arr[i][j] != 1) {
// 若是不是上述的两种边界状况,终止条件的到达方式是i-1,j和i,j-1的和
temp[i][j] = temp[i - 1][j] + temp[i][j - 1]
}
}
}
return temp[n - 1][m - 1]
};
console.log(uniquePathsWithObstacles([[0,0,0],[0,1,0],[0,0,0]])) // 2
复制代码
在M件物品里取出若干件放在大小为W的背包里,每件物品的体积为W1,W2,W3····Wn,与这些物品对应的价值分别对应为P1,P2,P3·····Pn,如何求出这个背包能装的最大价值❓
function beibao(M, W, arrP, arrW) {
var result = []
for (var i = 0; i <= M; i++) {
result[i] = []
for (var j = 0; j <= W; j++) {
if ( i == 0) {
result[i][j] = 0
} else if ( arrW[i-1] > j) {
result[i][j] = result[i-1][j]
} else {
result[i][j] = Math.max(arrP[i-1] + result[i-1][j - arrW[i-1]], result[i-1][j])
}
}
}
return result[i-1][j-1]
}
var M = 5; // 物体个数
var W = 16; // 背包总容量
var arrP = [4,5,10,11,13]; // 物体价值
var arrW = [3,4,7,8,9]; // 物体个数
console.log(beibao(M, W, arrP, arrW)); // 23
复制代码
动态规划适合解决重叠子问题和最优子结构性质的问题,三要素为「最优子问题」,「边界条件」和「状态转移方程」,其中解决动态规划这类问题的关键在于写出「状态转移方程」,而写出状态转移方程法的思路为: