重叠子问题
的状况的最优解
时有效。子问题
。结果都逐渐被计算并被保存
,从简单的问题
直到整个问题
都被解决。保存递归时
的结果,于是不会在解决一样的问题时花费时间 · · · · · ·动态规划只能应用于有最优 子结构
的问题。最优子结构的意思是局部最优解能决定全局最优解
(对有些问题这个要求并不能彻底知足,故有时须要引入必定的近似)。算法
简单地说,问题可以分解成子问题来解决
。数组
通俗一点来说,动态规划和其它遍历算法(如深/广度优先搜索)都是将原问题拆成多个子问题而后求解
,他们之间最本质的区别是,动态规划保存子问题的解,避免重复计算
。markdown
解决动态规划问题的关键是找到状态转移方程
,这样咱们能够通计算和储存子问题的解来求解最终问题
。spa
同时,咱们也能够对动态规划进行空间压缩
,起到节省空间消耗的效果。code
在一些状况下,动态规划能够当作是带有状态记录(memoization)的优先搜索
。orm
动态规划是自下而上的
,即先解决子问题,再解决父问题;递归
而用带有状态记录的优先搜索
是自上而下
的,即从父问题搜索到子问题,若重复搜索到同一个子问题则进行状态记录,防止重复计算。字符串
若是题目需求的是最终状态,那么使用动态搜索比较方便;it
若是题目须要输出全部的路径,那么使用带有状态记录的优先搜索会比较方便。io
对问题求解的时候,老是作出在当前看来是最好的作法
适用贪心算法的场景:问题可以分解成子问题来解决,子问题的最优解能递推到最终问题的最优解
。这种子问题最优解成为最优子结构
回溯法(backtracking)是优先搜索的一种特殊状况,又称为试探法
,经常使用于须要记录节点状态
的深度优先搜索。一般来讲,排列、组合、选择类问题使用回溯法比较方便。 顾名思义,回溯法的核心是回溯
。在搜索到某一节点的时候,若是咱们发现目前的节点(及其子节点)并非需求目标时
,咱们回退到原来的节点
继续搜索,而且把在目前节点修改的状态 还原
。
这样的好处是咱们能够始终只对图的总状态进行修改,而非每次遍历时新建一个图来储存 状态。
在具体的写法上,它与普通的深度优先搜索同样,都有 [修改当前节点状态]→[递归子节 点]
的步骤,只是多了回溯的步骤,变成了 [修改当前节点状态]→[递归子节点]→[回改当前节点 状态]
。
回溯法。有两个小诀窍,一是按引用传状态
,二是全部的状态修 改在递归完成后回改
。 回溯法修改通常有两种状况,一种是修改最后一位输出,好比排列组合;一种是修改访问标 记,好比矩阵里搜字符串。
重复计算
来得到最优解假设你正在爬楼梯。须要 n 阶你才能到达楼顶。
每次你能够爬 1 或 2 个台阶。你有多少种不一样的方法能够爬到楼顶呢?
注意:给定 n 是一个正整数。
这是十分经典的斐波那契数列题。定义一个数组 dp,dp[i] 表示走到第 i 阶的方法数,走到第 i 阶的 方法数即为走到第 i-1 阶的方法数加上走到第 i-2 阶的方法数。这样咱们就获得了状态转移方程 dp[i] = dp[i-1] + dp[i-2]。注意边界条件的处理。
const climbStairs = function(n) {
if(n <= 2) return n;
const dp = [1, 2];
for(let i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n-1];
}
// O(n) 空间复杂度
复制代码
动态规划进行空间压缩 dp[i] 只与 dp[i-1] 和 dp[i-2] 有关,所以能够只用两个变量来存储 dp[i-1] 和 dp[i-2]
const climbStairs = function(n){
if(n <= 2) return n;
let pre1 = 1, pre2 = 2, cur;
for(let i = 2; i < n; i++) {
cur = pre1 + pre2;
pre1 = pre2;
pre2 = cur;
}
return cur;
}
复制代码
递归写法:(能够无视,空间复杂度最高)
const climbStairs = function(n){
if(n <= 2) return n;
return climbStairs(n - 1) + climbStairs(n-2);
}
复制代码
超时