1. 动态规划
自底向上思想:我们直接从最底下,最简单,问题规模最小的 f(1) 和 f(2) 开始往上推,直到推到我们想要的答案 f(20),这就是动态规划的思路,这也是为什么动态规划一般都脱离了递归,而是由循环迭代完成计算
最优子结构:原问题的最优解,可以通过子问题的最优解获得
重叠子问题:消除了递归中出现的重叠子问题的重复计算
2. 动态规划的三大步骤
第一步:定义数组元素的含义
第二步:找出数组元素之间的关系式,也就是递推公式或者说状态转移方程
第三步:找出初始值
有了初始值,并且有了数组元素之间的关系式,那么我们就可以得到 dp[n] 的值了,而 dp[n] 的含义是由你来定义的,你想求什么,就定义它是什么,这样这道题也就解出来了
3. 递归题
3.1 小青蛙跳台阶
题目:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
分析过程:
第一步:dp[n]的含义,想求什么就定义它是什么。所以这里定义跳n级台阶有dp[n]种跳法
第二步:思考对于这道题,由于情况可以选择跳一级,也可以选择跳两级,所以青蛙到达第 n 级的台阶有两种方式:
一种是从第 n-1 级跳上来
一种是从第 n-2 级跳上来
由于我们是要算所有可能的跳法的,所以有 dp[n] = dp[n-1] + dp[n-2]
试想10级台阶跳法是dp[10],10级台阶两种情况,一种是第8级台阶跳上去也就是dp[8];另一种是从第9级台阶跳上去也就是dp[9]。跳法状态转移方程dp[10] = dp[9] + dp[8]
第三步:找初始条件。当 n = 1 时,dp[1] = dp[0] + dp[-1],而我们是数组是不允许下标为负数的,所以对于 dp[1],我们必须要直接给出它的数值,相当于初始值,显然,dp[1] = 1。一样,dp[0] = 0.(因为 0 个台阶,那肯定是 0 种跳法了)。于是得出初始值:
dp[0] = 0. dp[1] = 1. 即 n <= 2 时,dp[n] = n
// 代码
function Fun(n){
let dp = []
// 初始值
dp[1] = 1
dp[2] = 2
// 数组元素关系式
for (let i=2; i<= n; ++i){
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
}
3.2 二维数组DP
题目:一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。问总共有多少条不同的路径?
分析过程:
第一步:定义到达 m x n网格位置,也就是右下角,需要dp[i][j]条路径
第二步:找数组元素关系式,试想到达m*n右下角位置,有两条路可以到达,一种是从dp[i-1][j]向下走一步到达,另一种是从dp[i][j-1]向右走一步到达;所以dp[i][j] = dp[i-1][j] + dp[i][j-1]
第三步:找初始条件;当 dp[i] [j] 中,如果 i 或者 j 有一个为 0,那么还能使用关系式吗?答是不能的,因为这个时候把 i - 1 或者 j - 1,就变成负数了,数组就会出问题了
dp[0] [0….n-1] = 1; // 相当于最上面一行,机器人只能一直往左走
dp[0…m-1] [0] = 1; // 相当于最左面一列,机器人只能一直往下走
int Fun(m, n){
if (m <= 0 || n <= 0) {
return 0;
}
int dp[][] = new int[m][n];
// 初始化
for(int i = 0; i < m; i++){
dp[i][0] = 1;
}
for(int i = 0; i < n; i++){
dp[0][i] = 1;
}
// 推导出 dp[m-1][n-1]
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}