动态规划(Dynamic Programming, DP)
- 在查找有很多
重叠子问题的情况的最优解时有效。 - 它将问题重新组合成
子问题。 - 为了避免多次解决这些子问题,它们的
结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。 - 因此,动态规划
保存递归时的结果,因而不会在解决同样的问题时花费时间 · · · · · ·
-
动态规划只能
应用于有最优 子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。 -
简单地说,
问题能够分解成子问题来解决。 -
通俗一点来讲,动态规划和其它遍历算法(如深/广度优先搜索)都是将
原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存子问题的解,避免重复计算。 -
解决动态规划问题的关键是找到
状态转移方程,这样我们可以通计算和储存子问题的解来求解最终问题。 -
同时,我们也可以对动态规划进行
空间压缩,起到节省空间消耗的效果。 -
在一些情况下,动态规划可以看成是
带有状态记录(memoization)的优先搜索。 -
动态规划是自下而上的,即先解决子问题,再解决父问题; -
而用带有
状态记录的优先搜索是自上而下的,即从父问题搜索到子问题,若重复搜索到同一个子问题则进行状态记录,防止重复计算。 -
如果题目需求的是最终状态,那么使用动态搜索比较方便;
-
如果题目需要输出所有的路径,那么使用带有状态记录的优先搜索会比较方便。
贪心算法
-
对问题求解的时候,总是做出在当前看来是最好的做法
-
适用贪心算法的场景:问题能够分解成子问题来解决,
子问题的最优解能递推到最终问题的最优解。这种子问题最优解成为最优子结构
回溯法
回溯法(backtracking)是优先搜索的一种特殊情况,又称为试探法,常用于需要记录节点状态的深度优先搜索。通常来说,排列、组合、选择类问题使用回溯法比较方便。
顾名思义,回溯法的核心是回溯。在搜索到某一节点的时候,如果我们发现目前的节点(及其子节点)并不是需求目标时,我们回退到原来的节点继续搜索,并且把在目前节点修改的状态 还原。
这样的好处是我们可以始终只对图的总状态进行修改,而非每次遍历时新建一个图来储存 状态。
在具体的写法上,它与普通的深度优先搜索一样,都有 [修改当前节点状态]→[递归子节 点] 的步骤,只是多了回溯的步骤,变成了 [修改当前节点状态]→[递归子节点]→[回改当前节点 状态]。
回溯法。有两个小诀窍,一是按引用传状态,二是所有的状态修 改在递归完成后回改。
回溯法修改一般有两种情况,一种是修改最后一位输出,比如排列组合;一种是修改访问标
记,比如矩阵里搜字符串。
贪心算法 vs 动态规划
- 贪心算法与动态规划的不同在于它对每个子问题的解决方案都作出选择,不能回退
- 动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能
- 而回溯算法就是大量的
重复计算来获得最优解
动态规划 -- Leetcode 70. 爬楼梯
假设你正在爬楼梯。需要 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);
}
超时