在一周之前,我就开始决定啃动态规划这块硬骨头了。还记得看过y总的动态规划视频,当时给我留下了它很难的感觉。我也知道这个过程应该很漫长,但我坚持了一周多几天了。因此,我想要总结一下这相关内容,也算是为我之后的解相关题目提供更多的思想吧。
动态规划是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题
的方法。 动态规划常常适用于有重叠子问题和最优子结构性质的问题
,动态规划方法所耗时间往往远少于朴素解法。
动态规划的问题一般形式就是求最值问题
还有就是一定具有最优子结构
,但也并非不是求最值的就不可以使用动态规划解决。而动态规划的主要思想是就是穷举
,但这个穷举并非是暴力穷举,它是有技巧的穷举
。那么解题的关键是什么呢?解题的关键就是状态的表示(dp数组的含义)
和状态的转移计算(状态转移方程)
(相信看过y总的课的人都知道)。
前提:
我们知道了该题可以使用动态规划解决
。
状态
(其实就是确定相应的dp数组的维数和维数相应的含义)和相应的dp数组的含义
。选择
(也就是什么可以使得状态发生转变
,那么就是列出相关的状态转移方程
)。base case
(也就是一开始就可以确定的,它也是后面状态转移的基础
)。我们根据动态规划的解题步骤,可以看出解这类题目需要确定相应的base case
,然后根据这base case
并且全程坚信dp数组的含义(和递归相信函数的含义一样)
,然后根据这两前提进行状态转移
。所以他就和数学中的数学归纳法相似
,我们先假定其成立
,然后在每一步都遵守题目要求
,那么我们根据归纳所求出的肯定就是符合题目要求的
。然后根据dp数组的含义就可以求出相应的题目所要求的东西
。
AC代码:
class Solution {
public int waysToStep(int n) {
int m = 1000000007;
int[] dp = new int[n + 1];
dp[1] = 1;
if(n == 1) return dp[1];
dp[2] = 2;
if(n == 2) return dp[2];
dp[3] = 4;
if(n == 3) return dp[3];
// 三步问题其实和两步问题一样
// 我们只需要从相应的位置转化而来,如果是还差三步,我们将它拆分为1 + 2 或者 2 + 1的话,其实和还剩两步过去 和 还剩一步过去重复了,所以会多算,因为是算方式数,所以只需将其相加不需要再添加任何的常数。
for(int i = 4; i <= n; i ++){
dp[i] = ((dp[i - 3] + dp[i - 2]) % m + dp[i - 1]) % m;
}
return dp[n];
}
}
1. 状态和dp数组含义
状态就是第几阶台阶,那么这里的状态就是台阶数
,一维的
。dp数组的含义:dp[i] 的数值表示的是上到第i阶台阶的方式数
。一定要相信这个定义。
2. base case
base case 就是一开始就能确定的,即上到第一阶的方式数为1
,上到第二阶台阶的方式数为2
,上到第三阶台阶的方式数为4
。
3. 状态转移(计算)
选择是什么?选择就是小孩可以一次性上一阶、二阶、三阶
。那么第i阶台阶可以由 i - 1、i - 2、i - 3 阶台阶过来
。所以有dp[i] = ((dp[i - 3] + dp[i - 2]) % m + dp[i - 1]) % m
,因为最后结果需要取模,所以我们在两数相加之后就需要取模
。
4. 最后的返回值
根据dp数组的含义:dp[i] 的数值表示的是上到第i阶台阶的方式数
,那么所求的就是到第n阶台阶的方式数,就是dp[n]
。
class Solution {
public int maxSubArray(int[] nums) {
//
int[] dp = new int[nums.length];
dp[0] = nums[0];
if(nums.length == 0) return dp[0];
int res = dp[0];
for(int i = 1; i < dp.length; i ++){
dp[i] = Math.max(dp[i - 1] + nums[i],nums[i]);
res = Math.max(res,dp[i]);
}
return res;
}
}
1. 状态和dp数组含义
状态就是以第几个数结尾的连续子数组,一维的
。dp数组的含义:dp[i] 值表示的是以第i个数为结尾的连续子数组的和的最大值
。
2. base case
以第一个数结尾的子数组的和的最大值
就是相应数组中的第一个数的值
。
3. 状态转移(计算)
这道题的选择是这个数是否可以加入前面的那个子数组
,那么就是要比较相应的加入后的和与其独立出来(就是其一个数为一个子数组)的子数组的和
。dp[i] = Math.max(dp[i - 1] + nums[i],nums[i])
;
4. 最后的返回值
因为要求的是最大值
,那么我们需要遍历一边dp数组求最大值
。
AC代码:
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
// dp[i][j] 表示的是处于 到达第i行第j列上的格子所需的路径种数
if(m == 1 && n == 1) return 1;
// 刚开始的时候,dp[0][0] = 0;
dp[0][0] = 0;
// 在处于第一行的位置 除了dp[0][0] 之外的 都是1
for(int i = 1; i < m; i ++) dp[i][0] = 1;
// 在处于第一列的位置 除了dp[0][0] 之外的 都是1
for(int j = 1; j < n; j ++) dp[0][j] = 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];
}
}
1. 状态和dp数组含义
状态就是相应的行和列,二维的
。dp数组的含义:dp[i][j] 值表示的是到达第i行第j列上的格子所需的路径种数
。
2. base case
以刚开始的时候,到达第0行第0列的路径种数为0,因为它是我们的起点
。
3. 状态转移(计算)
这道题的选择是机器人每次可以向下或者向右移动一步
,那么到达第i行第j列可以从第i行第j - 1列过来,也可以从第i - 1行第j列转移过来
。dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
;
4. 最后的返回值
根据dp数组的含义:dp[i][j] 值表示的是到达第i行第j列上的格子所需的路径种数
,所以最后所求的是到达第 m - 1行第 n - 1列的路径种数就是dp[m - 1][n - 1]
。
class Solution {
public int[] countBits(int n) {
int[] dp = new int[n + 1];
dp[0] = 0;
if(n == 0) return new int[]{dp[0]};
dp[1] = 1;
if(n == 1) return new int[]{dp[0],dp[1]};
dp[2] = 1;
if(n == 2) return new int[]{dp[0],dp[1],dp[2]};
int highBit = 2;
for(int i = 3; i <= n; i ++){
// 关键就是找那个小于i且是2的倍数的最大数
if(highBit * 2 == i){
highBit = highBit * 2;
dp[i] = 1;
continue;
}
dp[i] = dp[i - highBit] + 1;
}
return dp;
}
}
1. 状态和dp数组含义
状态就是相应数字的数值,一维的
。dp数组的含义:dp[i]值表示的是数字的数值为i时,该数字二进制表示中的1的个数
。
2. base case
以刚开始的时候,数值为0是dp[0] = 0,数值为1时dp[1] = 1,数值为2时dp[2] = 1
。
3. 状态转移(计算)
这道题的关键就是求出小于且最接近i数值的且为2的倍数的数(即highBit)
,那么i数值的1的个数只会比i - highBit数值中的个数多1
。dp[i] = dp[i - highBit] + 1
;
AC代码:
class Solution {
//自己写出的第一道记忆化搜索
// num[i] 表示的是长度为i的区间的所能构成的二叉搜索树的种类数
int[] num = new int[20];
public int numTrees(int n) {
return getNum(1,n);
}
public int getNum(int start,int end){
// 长度为1的种类数为1
if(start > end || start == end){
num[1] = 1;
return 1;
}
//长度为2的 种类数为2
if(start + 1 == end){
num[2] = 2;
return 2;
}
// 备忘录优化
if(num[end - start + 1] != 0)
{
//System.out.println(num[end - start + 1]);
return num[end -start + 1];
}
int sum = 0;
// 如果没有相应的的备忘就需要添加备忘
for(int i = start; i <= end; i ++){
int j = getNum(start,i - 1);
int k = getNum(i + 1, end);
// 求总和
sum += k * j;
}
// 记录下来
num[end - start + 1] = sum;//刚开始有点小坑
return sum;
}
}
1. 状态和dp数组含义
状态就是区间内部长度,一维的
。dp数组的含义:dp[i]值表示的是区间长度为i的所能形成的二叉搜索树的种数
。
2. base case
以区间长度为1或者为负数时,相应的种类数为1;区间长度为2时,相应的种类数为2
。
3. 状态转移(计算)
这道题的选择是我们选举区间的那一个数为根节点
,因为区间的每一个点都可能作为根节点
,所以我们在递归过程中进行枚举,那么分割后在进行求左边和右边的区间的所能形成的二叉搜索树的种类
,这个就交给递归进行完成
,那么以该节点为根节点的二叉搜索树的种类数就是左右所求出的种类数相乘
,在进行求和;
4. 备忘录解决重叠子问题
因为我们求一个区间长度更大的所能形成的二叉搜索树的种数时是将它进行分割
,那么必然会缩小区间
,也就是小区间的值会反复使用到
,我们会对其进行记录
,从而减少了小区间的反复计算
,从而使得效率提高。
经过了一周多的时间,我将动态规划的简单题刷完了,并且中等题刷了将近十道,并且动态规划的好多题型都还没有熟练掌握,接下来我会继续努力。其实动态规划的套路在状态表示和状态计算,难也难在状态表示和状态计算,其中这需要大量的习题积累
。后续动态规划的文章我也会持续更新,又要去深造了
。如果觉得对你有帮助的友友,请留下你的三连。