动态规划 适用于 有限枚举+最值。
求解步骤:
正推 或 逆推:
1. 枚举
2. 边界
3. 最优子结构
4. 状态转移方程
题例
青蛙跳台阶
从青蛙跳台阶开始。
逆推 比较好理解,而且 计算机递归很擅长做逆向的事情,但递归 方法栈调用易耗时,可以通过引入Map<n,f(n)> 的方式;或者 递归 转 for 的方式,设置for 初始值,更新for 状态。
* 一次可以跳上1级台阶,也可以跳上2级台阶。n 级的台阶总共有多少种跳法。 * 正推: * n , 方案, 多少种跳法 * n=0,不跳,1种? * n=1,1级, 1 * n=2, 1+1/2, 2 * n=3,1+1+1/1+2/2+1, 3 * n=4,1+1+1+1/1+1+2/1+2+1/2+1+1/2+2, 5 * n=5,1+1+1+1+1/1+1+1+2/1+2+1+1/1+2+2/2+1+1+1/2+1+2/2+2+1/, 7 * <p> * 逆推: * 10级,先跳9级,再跳1级;先跳8级,再跳2级;先跳7级,再跳 1级/2级?* 9 级,先跳8级,再跳1级;先跳7级,再跳2级; * 2级,先跳2级,无;先跳1级,再跳1级; * 1级,先跳1级,无; * f(n)=f(n-1)+f(n-2), n>2 * <p>
代码细节:
值得注意的是,
Map.get(?),?是 n还是 n-1/n-2呢?看图1
for 的初始和边界是什么呢?看for 值作用于谁,看 for ++与条件顺序
图1
// Map<n,f(n)>
Map<Integer, Integer> resultMap = new HashMap<>();
/**
* 逆推-递归
* @param n
* @return
*/
public int climbStairs(int n) {
if (n <= 1) {
resultMap.put(0, 1);
resultMap.put(1, 1);
return 1;
}
if (n == 2) {
resultMap.put(2, 2);
return 2;
}
// 因为是倒序,先看 n 被计算过
// n 不被计算,自然 n-1, n-2 从来没被计算过,直接递归
if (resultMap.containsKey(n)) {
return resultMap.get(n);
}
// 简化成两行代码
resultMap.put(n, (climbStairs(n - 1) + climbStairs(n - 2)) %1000000007);
return resultMap.get(n);
}
/**
* 正推-for
* @param n
* @return
*/
public int climbStairsForwardReasoning(int n) {
if (n <= 1) {
return 1;
}
if (n == 2) {
return 2;
}
int f1=1,f2=2;
int result=-1;
// 3级开始
// 二次开始,先num++ , 再 num<=n
for (int num=3;num<=n;num++){
result=f1+f2;
// 更新
f1=f2;
f2=result;
}
return result;
}
最长严格递增子序列
这个题的关键在于,找到两个概念。
同时,可以认识到,数学本身是嵌套,多条件限制关系,但我们定义的时候,总是展开来定义,这样好理解,那定义如何映射到 题解里,又或者,定义 如何 映射到 代码 中,是一个有意思的事。
从本题来看,定义 映射到 代码 中 的内容有:
1. 求最值,定义一个变量/一个已有变量,不断更新这个变量值
下标 i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
原始数组 | 10 | 9 | 2 | 5 | 3 | 7 | 101 | 18 |
i结尾的 最长递增子序列 | 1 | 1 | 1 | 2 | 2 | 3 | 4 | 3 |
数组中的最长递增子序列= max(lens[x]), x=[0,n) | 10 ; 1 | 10/9 ; 1 | 10/9/2 ; 1 | 2,5 ; 2 | 2,5/2,3 ; 2 | 2,5,7/2,3,7 ; 3 | 2,5,7,101/2,3,7,101 ; 4 | 2,5,7,101/2,3,7,101 ; 4 |
两个概念: 以i结尾的 最长递增子序列/ 最长递增子序列 | 设:lens[i]: i结尾 的最长递增子序列 当:j in [0,i-1], nums[i]>nums[j] lens[i]=max(lens[j] )+1 =max(lens[i],lens[j] +1) | lens[i] 结尾 | lens[i] 结尾 | lens[i] 结尾 | lens[i] 结尾/ max(lens[j]) | lens[i] 结尾 | lens[i] 结尾 | max(lens[j]) |
public int findNumberOfLIS(int[] nums) {
// 以 lens[i] 结尾 的 最大子序列
int[] lens = new int[nums.length];
lens[0] = 1;
// 最终结果:[0,nums.length) 最长递增子序列
int finalMaxLen = 1;
for (int i = 1; i < nums.length; i++) {
// 初值
lens[i] = 1;
// 两两比较 需要加一层 for
for (int j = 0; j < i; j++) {
// 求 lens[i]
if (nums[i] > nums[j]) {
// 自我更新,少用一个变量来表达
lens[i] = Math.max(lens[i], (lens[j] + 1));
}
// 记录结果
finalMaxLen = Math.max(finalMaxLen, lens[i]);
}
}
return finalMaxLen;
}
剪绳子
变量大法,指数大法好,O(1) 复杂度,写法还简单。
2 <= 总长n <= 58 | 2 | 8 | 10 | 17 | ||
每段m>0,int | 1,1;1 | 4,4;16 3,5;15 2,6;12 2,3,3;18 | 5,5;25 2,3,5;30 2,3,2,3;36 | 8;9;72 2,3,3, 3,3,3;18*27 | 思路1:最小因数分割 例子中,因数都是2,3 且 3多 对于17来说,先二分,二分后的两个数 结果乘积,就是17的结果 | |
最短思路:尽量3分,算指数 三分有余数做枚举 |
public int cuttingRope(int n) {
// 边界 使用变量,少用枚举,减少代码量
if (n<=3){
return n-1;
}
// 计算外提,好看
int a= n%3,b=n/3;
// /3
if(a==0){
return (int) Math.pow(3,b);
}else if (a==1){
// 将一个 1+3 转换成 一个 2+2,这个有意思
return (int)Math.pow(3,b-1)*4;
}
// 不需要加else,因为: 1. 前种直接返回 2. 这是最后一种情况
// 多*2
return (int)Math.pow(3,b)*2;
}
尽量三分的数学验证:
① 当所有绳段长度相等时,乘积最大。
算数几何不等式:
② 最优的绳段长度为 3 。
列式 求最值:
当然,动态规划自我更新的写法,漂亮极了。
完美表达 动态规划 求极值 的想法。
public int cuttingRopeDynamic(int n) {
//dp[i] i 的 最大乘积
int[]dp=new int[n+1];
dp[1]=1;
dp[2]=1;
for(int i=3;i<=n;i++){
// j<=i-j, 等式 当且仅当 i=2j时成立,表达 二分
// j 从 1 开始(最小1段),计算 j * i-j
for(int j=1;j<=i-j;j++){
dp[i]=Math.max(dp[i],
Math.max(j, dp[j]) * Math.max(i-j,dp[i-j])
);
}
}
return dp[n];
}
皮一下:青蛙跳台阶是我毕业笔试的第一道算法题。看出那个面试官事情比较急,看完我的笔试就没有耐心了,那次的面试体验就很滑稽。