一、前言
按照蓝桥杯的考点,我将DP放在最后来复习(也就是今天,明天则进行简单的IDE熟悉即可了)。对于DP,相信大家也早有听闻,它不是一两天能够提升起来的,而且也需要天赋,因此在此我就简单进行总结一下,培养一下这方面的感觉,希望省赛场上遇到此类题能够有下手方向,最后祝大家蓝桥杯旗开得胜。
二、具体过程
(1)为什么需要DP算法?
- DP一般采用使用dp数组存储解,因此在求最优解时能够优化时间复杂度。(即使不能够找到状态方程,也得学会采用空间换时间的思想去优化时间,多骗一点测试用例)
- 算法时间效率高,代码量少,多元性强,主要考察思维能力、建模抽象能力、灵活度。
(2)什么情况使用DP
- 最优子结构
- 子问题重叠
- 无后效性
(3)DP的标准步骤
- 确定初始状态(从小问题出发,看0,1项情况)
- 确立状态方程,如何去存储?
- 递推,寻找n与n-1项的关系,建立状态方程
- 终止状态,获取最优解
(4)提升DP的一般步骤(长远来看,短期提升很困难)
- 分析简单的递归(斐波那,汉诺塔,爬楼梯等)
- 开始刷二叉树(弄清楚先中后,层次遍历),N叉树
- 开始刷DP较为有难度的题(最长公共子序列,购买股票,背包问题等)
- 进阶,在LeetCode上面寻找中等难度甚至困难的题进行刷。
参考书籍:labuladong算法书、算法导论、LeetCode。
看书+刷题(这篇DP题型分类很细)相结合
(5)本次DP简单相应题型总结
蓝桥杯近两次DP题:数字三角形,子串排序、装饰珠。都是有难度的题,考场遇见此类题建议放到最后再动手,先保证基础编程题拿分拿满。
- 从斐波那数看DP
由于斐波那数,我们都知道他的递推公式,因此对于初学者的我来理解DP更为友好,谈到斐波那求解,我们最原始的就直接递归(递推),但是发现稍大了就会超时,因为在递推的过程中就是一颗二叉树在递进,有很多重复部分。于是我们就可以想到采用数组进行存重复部分,于是有了记忆优化。但是发现这样还是时间复杂度降下来了,但是空间复杂度上去了,于是开始想如何降低空间呢?因为我们只需要最后的最优解,所以无需关心前面的情况,因此就可以想到只需一个空间即可存储,采用迭代的思想进行一步步更新。(虽然迭代没有那么明显的使用递推公式,状态方程了,但是根本还是n-1到n的递推,只是表现方式不同罢了,因此最关键的就是从小问题出发寻找递推公式)
//斐波那处理类
// 斐波那数
// https://leetcode-cn.com/problems/fei-bo-na-qi-shu-lie-lcof/submissions/
//法1:最原始的方法直接递归求解
// //到40求解时就跑不动了
int method1(int n){
if(n<2){
return n;
}else{
return method1(n-1)+method1(n-2);
}
}
static int []cache= new int[1000];
static {
Arrays.fill(cache,-1);
}
// 法2:记忆优化
int method2(int n){
if(n<2){
cache[n]=n;
return n;
}else{
if(cache[n-1]!=-1&&cache[n-2]!=-1){
return cache[n-1]+cache[n-2];
}else if(cache[n-2]!=-1){
return cache[n-2]+method1(n-1);
}else if(cache[n-1]!=-1){
return cache[n-1]+method1(n-2);
}
return method1(n-1)+method1(n-2);
}
}
// 法3:动规划方法,使用数组进行存储就无需再重复递归
// 动态方程
int method3(int n){
if(n<2){
cache[n]=n;
}else {
cache[0]=0;
cache[1]=1;
for(int i =2;i<=n;i++){
cache[i]=cache[i-1]+cache[i-2];
}
// cache[n]=cache[n-1]+cache[n-2];
}
return cache[n];
}
//空间继续优化,,迭代思想
int method4(int n){
int a=0,b=1,sum;
for(int i=0;i<n;i++){
sum = a+b;// 如果需要进行数据处理,取余则从此处开始取余这样也肯定让后面的数小于该数,
a=b;//a进一步迭代到b(即第二数,第一个数废弃)
b=sum;//b迭代到第三个数
}
return a;// 返回第一个数,看起点 n=0不用迭代就为本身,n=1时迭代一次到b也就是第二个数
}
void test(){
for(int i=10;i<1000;i*=2){
long start = System.currentTimeMillis();
int val = method4(i);
long end = System.currentTimeMillis();
System.out.println(i+"所用时间为:"+(end-start)+";结果为"+val);
}
}
public static void main(String[]args){
TreeLeetCodePart1 treeLeetCodePart1 = new TreeLeetCodePart1();
treeLeetCodePart1.test();
}
利用反射和多线程(单纯为了熟悉Java知识)进行跑一下测试对比。
public class ReflectUtil {
static int executeMethod(int kind,int n){
try{
Method method =TreeLeetCodePart1.class.getDeclaredMethod("method"+kind, int.class);
// Object o = new Object();
Object rs = method.invoke( TreeLeetCodePart1.class.newInstance(),n);
System.out.println(rs);
return (int) rs;
}catch (Exception e){
e.printStackTrace();
}
return -1;
}
public static void main(String[]args){
// executeMethod(1,20);
}
}
public class TestFab1 implements Runnable{
private int method;//方法选择,其实直接判断也行,但为了应用反射,我便写了一个简单的反射调用
public TestFab1(int method) {
this.method = method;
}
@Override
public void run() {
//System.out.println("这里是");
TreeLeetCodePart1 treeLeetCodePart1 = new TreeLeetCodePart1();
for(int i=10;i<1000;i*=2){
long start = System.currentTimeMillis();
int val = ReflectUtil.executeMethod(method,i);//treeLeetCodePart1.method4(i)
long end = System.currentTimeMillis();
System.out.println("方法"+method+"求解"+i+"所用时间为:"+(end-start)+";结果为"+val);
}
}
}
public class Main {
public static void main(String[]args){
TestFab1 testFab1 = new TestFab1(1);
// testFab1.run();
TestFab1 testFab2 = new TestFab1(2);
// testFab2.run();
TestFab1 testFab3 = new TestFab1(3);
// testFab3.run();
TestFab1 testFab4 = new TestFab1(4);
// testFab4.run();
Thread thread = new Thread(testFab1);
thread.start();
Thread thread2 = new Thread(testFab2);
thread2.start();
Thread thread3 = new Thread(testFab3);
thread3.start();
Thread thread4 = new Thread(testFab4);
thread4.start();
}
- 子序列问题
package lq.questions.consolidate.dp;
import java.util.Arrays;
import java.util.Random;
/**
* @AUTHOR LYF
* @DATE 2021/4/16
* @VERSION 1.0
* @DESC
*/
public class SimpleDp {
// 最大子段和
// 总体思路
// 1.确定起点(从小问题进行出发考虑)
// 2.确定最优解是?,如何存储?如何递推(状态转移方程),从小问题类推到n
// 3.确定终点,递推结束
// 4.获取最优解,在DP存储中获最优
// 最大子段和
// 1 -1 3 -1 3 -2 -4
// 暴力方式: n!+(n-1)!+..+1
// dp[]存储以i结尾最大值,当dp[i]为负数,开始转移到该数
// 可以得到该状态方程为 dp[i+1]=dp[i]>0?dp[i]+arr[i+1]:arr[i];// 若前面为负转移到从本身开始
void maxSub(){
int []arr = new int[10];
int[]dp = new int[10];
int []status = {-1,1};
for(int i =0;i<10;i++){
Random random = new Random();
arr[i]=random.nextInt(20)*status[random.nextInt(2)];
System.out.print(arr[i]+"->");
}
System.out.println();
dp[0]=arr[0];
for(int j =1;j<10;j++){
dp[j]=dp[j-1]>0?dp[j-1]+arr[j]:arr[j];
}
for(int i =0;i<10;i++){
System.out.print(dp[i]+">");
}
Arrays.sort(dp);
System.out.println("最大值为:"+dp[dp.length-1]);
}
// 最长不降子序列
// 1 3 2 4 5 3 1 0 3 4
// https://blog.csdn.net/liu16659/article/details/104091629
// 1.按照最大子段思路,dp[i]存储在i最长的不降个数
// 2.确定状态转移,,如何递推? 由于不连续因此,需要在确定dp[i]时,需要遍历i之前符合要求的然后再选取最大的值进行存储
// dp[0]=arr[0] ,dp[i]=max(dp,0,i-1)&&arr[i]>=.. // 如果使用list可以使用stream进行条件筛选再进行排序,但是复杂度会多排序,
// 3.最优解 max dp
// 时间复杂度n^2
// 若暴力需要 Cn 1, Cn 2+...Cn n +加上遍历
void maxSub2(){
int N = 20;
int []arr = new int[N];
// 模拟数据
for(int i =0;i<N;i++){
arr[i]=new Random().nextInt(20);
System.out.print(arr[i]+"->");
}
int []dp = new int[N];
dp[0]=1;
// 确定DP
for(int i =1;i<N;i++){
int maxDpVal = 1;//如该数小于前面所有数则只能为该数起点 个数则为1
for(int j=0;j<i;j++){
if(arr[i]>=arr[j]&&dp[j]+1>maxDpVal){// 满足要求,可以考虑是否从该处对接,此处可以进行记录
maxDpVal = dp[j]+1;
}
}
dp[i]=maxDpVal;
}
Arrays.sort(dp);
System.out.println();
System.out.println("最优解:"+dp[dp.length-1]);
}
// 最长公共子序列
// https://www.cnblogs.com/fengziwei/p/7827959.html
// 由于是两个字符串比较,因此需要二维数组进行存储DP
// 行和列分别代表一个字符串
void maxSub3(){
String str1=" fjlsdjfklajsdfj";
String str2=" ofyljfsreoewirwq";//需要进行空位
int[][]dp = new int[str1.length()][str2.length()];
//dp[0][0]=0;// 空位
String str = "";
for(int i =0;i<str1.length();i++)
{
Arrays.fill(dp[i],0);
}
// 比较暴力的DP,存储全部
for(int i =1;i<str1.length();i++){
for(int j = 1;j<str2.length();j++){
if(str1.charAt(i)==str2.charAt(j)){
dp[i][j]=dp[i-1][j-1]+1;
// 有发生变化转移的,记录路径
// str=str+str1.charAt(i)+"";
}else{
dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j]);
}
// if(i==1){//初始
// if(str1.charAt(i)==str2.charAt(j)){
// dp[i][j]=1;
// }
// }else{//递推
//
// int max=dp[i-1][1];//上一层最大值
//
// for(int k=2;k<str2.length();k++){// 时间复杂度n^3
// if(dp[i-1][k]>max){
// max = dp[i-1][k];
// }
// }
// // 如匹配相等,则dp[i][j]在此之前的最大值+1
// // 若不匹配则等于该值
// dp[i][j]=str1.charAt(i)==str2.charAt(j)?max+1:max;
// }
}
}
// 最后一行最大值则是最优解
Arrays.sort(dp[str1.length()-1]);
System.out.println("++++++++++");
for(int i =0;i<str1.length();i++){
for(int j=0;j<str2.length();j++){
System.out.print(dp[i][j]+" ");
}
System.out.println();
}
// int min = Math.min(str1.length(),str2.length());
// for(int i=1;i<min;i++){
// if(dp[i][i]>dp[i-1][i-1]){
// str=str+str2.charAt(i)+"";
// }
// }
for(int i =1;i<str2.length();i++){
if(dp[str1.length()-1][i]>dp[str1.length()-1][i-1]){
str=str+str2.charAt(i);
}
}
System.out.println("最优解:"+dp[str1.length()-1][str2.length()-1]+";"+str);
}
// 最大子矩阵和
public static void main(String[]args){
SimpleDp dp =new SimpleDp();
dp.maxSub3();
}
}
- 数字三角形
// 数字三角形
// 求最大和,且左右相差不超过1
/**
*
* 3
* 1 2
* 4 5 6
* 2 3 4 1
*
*/
// 如果没有相差限制,直接倒退贪心即可
// 但如果有限制则
private Scanner scanner = new Scanner(System.in);
private int n =scanner.nextInt();
private int[][] map = new int[n][n];
public void inputData(){
for(int i=0;i<n;i++){
for(int j =0;j<i+1;j++){
map[i][j]=scanner.nextInt();
}
}
System.out.println("=========TEST==========");
for(int i=0;i<n;i++){
for(int j =0;j<i+1;j++){
System.out.print(map[i][j]+" ");
}
System.out.println();
}
}
// 未进行限制(贪心即可)
public void test1(){
for(int i=n-2;i>=0;i--){//从倒数第二开始递推,选择最大的
for(int j=0;j<i+1;j++){
map[i][j]+=Math.max(map[i+1][j],map[i+1][j+1]);
}
}
System.out.println("RESULT:"+map[0][0]);
}
// 进行相差限制小于等1=>
// 偶数层 :向左和右一样多 奇数层: 向左或右为n/2,另外为n/2+1
// dp[n]==每层最大
// 最原始的思考,遍历所有并记录满足情况的,再选取最大的
// dp:动态规划,
//注意,这不是普通的数塔问题,因为向左走的次数与向右走的次数差值不超过1,所以当到达最后一层时
// ,一定是落在中间位置,如果层数是奇数,那么最后一定落在最后一层的第个元素上,如果层数是偶数,
// 最后一定是落在第或第个元素上。所以dp只要从最后一层的中间开始向上递推就可以了。
//作者:yo1ooo
//链接:https://www.jianshu.com/p/c20b6b9a178a
//来源:简书
// 相差的可以从最后的推出。最左边节点即全部往左走,往右走一步,则+1
// 使用map同时代表DP方程记录
public void test2(){
// 讨论奇偶数,确定start,end
int start,end;// n层范围
if(n%2==0){
start=n/2-1;// 0坐标开始
end = n/2;
}else{
start = end = n/2;
}
int start0,end0=end;// n-1层范围
for(int i =n-2;i>=0;i--){
start0=start-1<0?0:start-1;
//end0=end+1>n-2?n-2:end+1;
end0=end>n-2?n-2:end;
for(int j = start0;j<=end0;j++){
if(j<start0||j>end0){
continue;
}
if(j==start0){//左边
map[i][j]+=map[i+1][j+1];
start--;
}else if(j==end0){
map[i][j]+=map[i+1][j];
//end++;//无需加
}else{
map[i][j]+=Math.max(map[i+1][j],map[i+1][j+1]);
}
}
}
System.out.println("RESULT:"+map[0][0]);
}
- 购买股票以及打劫家舍