上一篇讲述了动态规划入门级题目,代码都是没有优化的,如果没有看过的读者也没关系,在下面会贴出这两道题目的所有代码,包括没有优化的和优化之后的。感兴趣的读者可以先去看一下上一篇的题目,都是EASY级别的题目。
小白学习动态规划:入门篇
今天主要是对上一篇博客的两道题目进行优化,对于绝大多数利用动态规划的算法题作优化时,个人认为最重要的优化方法就是:画DP的图!找出状态之间的依赖关系,将其它没有依赖的状态废弃掉。
如果你不懂没有关系,可以看以下例子,很好理解~
优化类型一:一维降变量
LeetCode70. 爬楼梯
问题描述:假设你需要爬楼梯,需要爬n阶才能到达楼顶,每次可以爬1或2阶,由多少种不同的方法可以爬到楼顶?
未优化前的代码:
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n];
for (int i = 0; i < dp.length; i++) {
if(i == 0){ //第1阶
dp[i] = 1;
}else if(i == 1){ //第2阶
dp[i] = 2;
}else{ //第3阶及以上
dp[i] = dp[i-1]+dp[i-2];
}
}
return dp[n-1];
}
}
首先,我们先分析dp的填充过程,观察有什么值是会在某个时间段后作废(一直没有使用到)
从上面几幅图可以很清晰地观察到,从求第四阶楼梯的爬楼梯方法数之后,每求下一阶楼梯的方法数时,就会有多一个值被废弃掉,而求某个状态的值(爬楼梯的方法数)只与它的第(n-2)个状态和第(n-1)个状态[n >= 3]有依赖关系,所以在第(n-2)个状态以前的值就被废弃掉了。
所以,这个定义一维DP数组在某种程度上是浪费了内存空间的。
未优化前的状态转移方程:
优化的过程可以观察下图:
颜色的含义(帮助你理解整个过程):
蓝色:初始化的状态值
黄色:得出下一个状态的状态值
红色:将当前状态的第(n-1)个状态转化为第(n-2)个状态
绿色:将第n个状态转化为第(n-1)个状态
动态规划过程:
求出一个新的状态时,将当前状态的第(n-1)状态更新为第(n-2)个状态,将当前状态更新为第(n-1)个状态,继续求下一个新的状态,直到循环结束。
优化后的代码:
class Solution {
public int climbStairs(int n) {
if(n <= 2){return n;}
int dp0 = 1;
int dp1 = 2;
for(int i = 3; i <= n; i++){
int result = dp0 + dp1;
dp0 = dp1;
dp1 = result;
}
return dp1;
}
}
优化类型二:二维降一维
LeetCode62. 不同路径
问题描述:一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
说明:m 和 n 的值均不超过 100。
未优化前的代码:
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++){
for (int j = 0; j < n; j++){
//上边界
if(i == 0 && j >= 0){dp[i][j] = 1;continue;}
//左边界
if(j == 0 && i >= 0){dp[i][j] = 1;continue;}
//其它情况
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
观察未优化前的DP矩阵:
状态转移方程:
当填充第二行时,情况是这样的:
当填充第三行时,情况是这样的:
**细心的我们会发现:**第一行数据已经没有用处了,它的存在与否不影响我们求第三行的数据。所以,我们只需要保存上一行数据,就可以得出下一行的数据,并且每求出下一行的一个数据时,都可以舍弃掉上一行的那一个数据,所以只需要一维数组保存一行数据就可以求出下一行的数据。
如果你不是很懂,那么看下面几幅图就会秒懂~
注意左边数组行下标一直为0,表示该数组本质上只有一行
原本二维dp数组的状态转移方程为:
在图中从位置上表现出来的结果是正确的,但是!!!重点来了!
注意看!我们会发现状态转移方程转化为:
DP的空间复杂度由O(m * n) 降低为O(m)
最后数组就会呈现出下图得到状态:
优化后的一维dp代码:
class Solution {
public int uniquePaths(int m, int n) {
if(m <= 1 || n <= 1){return 1;}
int[] dp = new int[n];
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(j == 0){dp[j] = 1;continue;}
dp[j] = dp[j-1] + dp[j];
}
}
return dp[dp.length - 1];
}
}
总结
这两道题目虽然简单,但是它们是动态规划入门的必做题目中的两道,掌握这两道题目的思想和优化技巧是非常重要的,优化DP的核心在于:画图
掌握画图这一种优化方法,寻找值的依赖关系,观察能否将空间复杂度降低,DP的核心本质上是用空间换取时间的算法,在面试中会遇到许多面试官提出优化方法的场景,掌握这一种技巧,刷多一些相关的题目,自然会熟能生巧!