DP刷题练习(一)

简介: 本文是关于动态规划(DP)算法的刷题练习,由作者blue整理于2024年8月5日,内容基于代码随想录的学习笔记。文章通过多个经典LeetCode题目详细讲解了动态规划的应用,包括斐波那契数、爬楼梯、最小花费爬楼梯、不同路径、整数拆分、不同的二叉搜索树以及分割等和子集等问题。每道题均配有清晰的C++代码实现与思路解析,涵盖状态定义、递推公式、初始化及遍历顺序等动态规划核心步骤。此外,还额外扩展了01背包问题的三种实现方式(二维数组、滚动数组与一维数组),深入探讨了优化技巧与遍历顺序的重要性。适合初学者系统学习动态规划算法并提升解题能力。

DP刷题练习(一)

作者:blue

时间:2024.8.5

文章内容学习自代码随想录,感谢carl!!!!

509. 斐波那契数 - 力扣(LeetCode)

int fib(int n) {
   
        int dp[31];//dp[i]表示第i个数的值
        dp[0]=0,dp[1]=1;
        for(int i=2;i<=n;i++) dp[i]=dp[i-1]+dp[i-2];
        return dp[n];
    }

70. 爬楼梯 - 力扣(LeetCode)

int climbStairs(int n) {
   
        int dp[46];//dp[i]表示,爬到第i层楼梯,有dp[i]种方案
        dp[1]=1,dp[2]=2;
        for(int i=3;i<=n;i++) dp[i]=dp[i-1]+dp[i-2];
        return dp[n];
    }
//递推公式怎么来?
//你想上到第10层,那你前一步是什么?
//肯定你是从第9层爬一步或者是从第8层爬两步上来,那么爬上第10层的方案就是,爬上第9层的方案+爬上第8层的方案
//∴dp[i]=dp[i-1]+dp[i-2],将最初始的dp[1]=1,dp[2]=2;

746. 使用最小花费爬楼梯 - 力扣(LeetCode)

int minCostClimbingStairs(vector<int>& cost) {
   
        int n=cost.size();
        int dp[n+1];//dp[i]表示,爬到第i层的最小花费
        dp[0]=0,dp[1]=0;//如果是爬到0/1就不用钱,因为0/1都可以作为初始点
        for(int i=2;i<=n;i++){
   
            dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[n];//顶楼是n
    }

//要注意题目的要求,题目是说,你一次花费当下这一层,注意是当层!的钱,你就可以向上爬1/2层
//所以你爬到第i层,可以是从i-1层来花费cost[i-1],也可以是从i-2层来花费cost[i-2],二者取最小值就是爬上第i层的最小花费
//所以递推公式就是 dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
//要注意dp数组的初始化,你可以选择从第0层和第1层开始爬,所以爬到第0/1层的花费都是0

62. 不同路径 - 力扣(LeetCode)

 int uniquePaths(int m, int n) {
   
        int dp[m][n];//dp[i][j]表示从起点(0,0)走到(i,j)的不同路径数
        dp[0][0]=1;
        for(int i=1;i<=n-1;i++) dp[0][i]=1;
        for(int i=1;i<=m-1;i++) dp[i][0]=1;
        for(int i=1;i<=m-1;i++)
        {
   
            for(int j=1;j<=n-1;j++) dp[i][j]=dp[i-1][j]+dp[i][j-1];
        }
        return dp[m-1][n-1];
    }
//动态规划五步曲
//dp含义:dp[i][j]表示从起点(0,0)走到(i,j)的不同路径数

//递推公式;dp[i][j]=dp[i-1][j]+dp[i][j-1],为什么是这样,你想,你每一步只能从上方一格,或者是左方一格走来
//那么这一格的方案数,不就是上方一格的dp[i-1][j]加上左方一格的dp[i][j-1]

//初始化:你想第一行往右的这一排,是不是只能从第一格往右走走到,那他们的路径数不就只能是从左往右吗?所以初始化为1
//同理你你一列往下这一排只能从第一格往下走走到,那他们的路径数不就只能是从上往下吗?所以初始化为1
//那对于(0,0)呢?也是只能是1,你不走就已经到达终点不也是一种方案吗?

//遍历顺序:自不必多说,先知道前面的才能知道后面的,所以是从前往后遍历

63. 不同路径 II - 力扣(LeetCode)

int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
   
        int m=obstacleGrid.size();int n=obstacleGrid[0].size();//获取行和列的长度
        if(obstacleGrid[0][0]==1||obstacleGrid[m-1][n-1]==1) return 0;//如果障碍物在起点或者终点,那路径数无论如何都是0
        int dp[m+10][n+10];
        dp[0][0]=1;
        int i;
        for(i=1;i<=n-1;i++){
   //行初始化
            if(obstacleGrid[0][i]==0) dp[0][i]=1;
            else break;
        }
        for(int j=i;j<=n-1;j++) dp[0][j]=0;
//////////////////////////////////////////////////////
         for(i=1;i<=m-1;i++){
   //列初始化
            if(obstacleGrid[i][0]==0) dp[i][0]=1;
            else break;
        }
        for(int j=i;j<=m-1;j++) dp[j][0]=0;
/////////////////////////////////////////////////////
        for(int j=1;j<=m-1;j++)
        {
   
            for(int k=1;k<=n-1;k++){
   
                if(obstacleGrid[j][k]==0) dp[j][k]=dp[j-1][k]+dp[j][k-1];
                else dp[j][k]=0;
            }
        }
        return dp[m-1][n-1];
    }


//别看本题代码写的如此多,其实并不复杂,我们一步一步来分析
//还是动规五步曲,在这里我设obs为障碍物数组
//dp含义:dp[i][j]表示从(0,0)->(i,j)的不同路径数

//递推公式:①obs[i][j]==0时 dp[i][j]=dp[i-1][j]+dp[i][j-1]
//         ②obs[i][j]==1时 dp[i][j]=0

//初始化:以上两步和上道题大同小异,本题初始化才是最讲究的部分
//首先如果终点或者起点就已经是障碍物了,那么肯定出发不了,肯定到达不了,路径数就是0,直接返回0即可
//其次第一行和第一列依然是最特殊的,在没有遇到障碍物之前,第一行/列上的这些点都是可达的,直接初始化dp[0][i]/dp[i][0]=1
//但是一旦遇到了障碍物,那么障碍物后面点都是不可达的,为什么?不要忘记了题目说的,你只能向右或者向下走!所以第一行只能从右来
//你一旦遇到障碍物,后面就不能到达了,同理,第一列也一样
//所以一旦遇到障碍物,我们就初始化障碍物的位置及其后面的位置的方案数为0!!!

//遍历顺序:自然也是从前往后,因为只有前面的先知道了,后面的才能知道!!!!!

343. 整数拆分 - 力扣(LeetCode)

数的拆分是dp中常见的类型

1.dp数组的含义:dp[i]表示,将i拆分,得到的最大值为dp[i]

2.递推公式:我们固定j(从1开始枚举到i-1),那么针对每一种情况,我们都可以将其拆分为2个数,或者是n个数的乘积

为什么这样分呢?因为拆成两个数是最基本的,n个数可以递归得到

int a=j*(i-j);//拆两个数
int b=j*dp[i-j];//拆两个数以上,递归思想!
//所以我的递推公式就是
dp[i]=max(dp[i],max(a,b));
//为什么要将dp[i]也放进来比较,因为我们是枚举j,也就是枚举拆分的第一位数!这有多种情况,而我们要的是最大的那个!

3.初始化:

0与1不必说,不用拆分,自然初始化为0

dp[2]=1,的话无法用之前的基础步得到,所以2也得初始化

//3可以拆成,1*2,1*dp[2],2*1,2*dp[1] 所以3可以由之前的情况得到故3不用初始化

dp[0]=0,dp[1]=0;//无法拆分
 dp[2]=1;//只能拆1*1

4.遍历顺序:自是从前往后,先知道前面的才能知道后面的

 int integerBreak(int n) {
   
        int dp[n+10];
        memset(dp,0,sizeof(dp));//注意这里先给所有赋值为0,因为我们一开始用dp[i]去比较,dp[i]中是没有值的,这样会报错
        dp[0]=0,dp[1]=0;//无法拆分
        dp[2]=1;//只能拆1*1
        for(int i=3;i<=n;i++)
        {
   
            for(int j=1;j<i;j++)
            {
   
                int a=j*(i-j);//拆两个数
                int b=j*dp[i-j];//拆两个数以上,递归思想!
                dp[i]=max(dp[i],max(a,b));
            }
        }
        return dp[n];
    }

小优化:

我们将数拆分,然后要使拆分后这些数的乘积最大,那么我们就要尽量使这些数相等,所以你拆一半就够了,后面你拆数就是向着大小差异的极端去的,所以可以这样优化

for(int j=1;j<=i/2;j++)
int integerBreak(int n) {
   
        int dp[n+10];
        memset(dp,0,sizeof(dp));
        dp[0]=0,dp[1]=0;//无法拆分
        dp[2]=1;//只能拆1*1
        for(int i=3;i<=n;i++)
        {
   
            for(int j=1;j<=i/2;j++)
            {
   
                int a=j*(i-j);//拆两个数
                int b=j*dp[i-j];//拆两个数以上,递归思想!
                dp[i]=max(dp[i],max(a,b));
            }
        }
        return dp[n];
    }

96. 不同的二叉搜索树 - 力扣(LeetCode)

首先何为二叉搜索树,节点的要求是针对于顶点来说,顶点左侧要小于顶点,顶点的右侧要大于顶点,就是要满足左小右大

1.dp数组的含义:dp[i],表示节点数目为i时,共有dp[i]种不同的二叉搜索树

2.递推公式:我们先用i枚举节点个数,用j表示j节点作为二叉搜索树的顶点为j,那么想想看,我们如何计算以j为顶点的二叉搜索树的种数呢?

是不是左子树的种数*右子树的种数,那么左子树有几种?是不是dp[j-1]种,你就想一串数,1,2,3,4,5此时j=4,那么比4小的,不就只有3个节点,而比4大的,不就是节点总数i,减掉j,5-4=1

然后j,有i种情况,就是说每个节点都可以作为顶点,所以都要把这i种情况累加起来

于是状态转移方程:dp[i]+=dp[j-1]*dp[i-j]

3.初始化:dp[0]=1,为什么?空树也是二叉搜索树!!

dp[1]要不要初始化?dp[1]+=dp[0]*dp[0]=1,符合现实情况,可以由递推公式得到,所以不用初始化

int numTrees(int n) {
   
        int dp[n+10];//表示i个节点,有dp[i]种不同的二叉搜索树
        memset(dp,0,sizeof(dp));//因为一会要自加,所以先给初值赋0
        dp[0]=1;//空树也是二叉搜索树
        for(int i=1;i<=n;i++){
   
            for(int j=1;j<=i;j++){
   
                dp[i]+=dp[j-1]*dp[i-j];//dp[j-1]左子树总数
            }                          //dp[i-j]右子树总数
        }
        return dp[n];
    }

416. 分割等和子集 - 力扣(LeetCode)这个背包能不能装满???

class Solution {
   
public:
    bool canPartition(vector<int>& nums) {
   
        int n=0;//背包容量
        int sum=0;
        int m=nums.size();//物品数量
        for(int i=0;i<m;i++) sum+=nums[i];
        n=sum/2;
        if(sum-n!=n) return false;//特判,不可能等分的情况
        int dp[m][n+1];//dp[i][j]针对前(0-i)个物品用容量j的背包来存放能否放满
        //初始化
        for(int i=0;i<m;i++) dp[i][0]=0;//容量为0什么都放不了
        for(int i=0;i<=n;i++){
   //第0个物品放置情况
            if(i>=nums[0]) dp[0][i]=nums[0];
            else dp[0][i]=0;
        }

        for(int i=1;i<m;i++){
   //物品
            for(int j=1;j<=n;j++){
   //容量
                if(j>=nums[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i]);
                else dp[i][j]=dp[i-1][j];
            }
        }

        if(dp[m-1][n]==n) return true;
        else return false;
    }
};

学有余力,手撕01背包:P1048 [NOIP2005 普及组] 采药 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

0/1背包要注意,所有的物品只能选择一次!!!

二维数组版本:

#include <iostream>
using namespace std;
int dp[110][1010];//dp[i][j]表示在j时间内任采(0,i)株草药可以获得的最大价值 
int t[110],m[110];
int main()
{
   
    int T,M;
    cin>>T>>M;
    for(int i=0;i<M;i++) cin>>t[i]>>m[i];

    //初始化:非常重要!!! 
    for(int i=0;i<M;i++) dp[i][0]=0;//0秒什么都放不了 

    for(int i=0;i<=T;i++){
    //注意时间可以到达第T秒 
        if(i>=t[0]) dp[0][i]=m[0]; //看下第一个物品能不能放 
        else dp[0][i]=0;
    }

    for(int i=1;i<M;i++){
    
        for(int j=1;j<=T;j++){
    //注意时间可以到达第T秒 
            if(j>=t[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-t[i]]+m[i]);//能放,看下放值不值 
            else dp[i][j]=dp[i-1][j];//没法放 
        }
    }

    cout<<dp[M-1][T];//注意时间可以到达第T秒 

    return 0;    
}

交替滚动版本:

经过观察可以发现,dp[i][j]的更新只与dp[i-1]的状态有关,那么我们只保留两行就够了,每一次根据第一行来更新第二行的信息,然后再把第二行替换为第一行。

#include <iostream>
using namespace std;
int dp[2][1010];//dp[0/1][j]表示在j时间内可以获得的最大价值
//0表示旧数据,1表示新数据 
int t[110],m[110];
int main()
{
   
    int T,M;
    cin>>T>>M;
    for(int i=0;i<M;i++) cin>>t[i]>>m[i];

    //初始化:非常重要!!! 

    for(int i=0;i<=T;i++){
    //注意时间可以到达第T秒 
        if(i>=t[0]) dp[0][i]=m[0]; //看下第0个物品能不能放 
        else dp[0][i]=0;
    }

    int now=0,old=1;//我这样写是为了使代码具有一般性 

    for(int i=1;i<M;i++){
    
        swap(old,now);//新老交替
        for(int j=1;j<=T;j++){
    //注意时间可以到达第T秒 
            if(j>=t[i]) dp[now][j]=max(dp[old][j],dp[old][j-t[i]]+m[i]);//能放,看下放值不值 
            else dp[now][j]=dp[old][j];//没法放 
        }
    }

    cout<<dp[now][T];//注意时间可以到达第T秒 

    return 0;    
}

一维数组版本:

/*这里的遍历顺序非常有考究,首先为什么时间是从右往左遍历呢?
dp[i][j]的更新只与dp[i-1][j]和dp[i-1][j-weight_i]左上角这两个数据有关,而与右边的数据无关,
那么从右向左遍历,遍历时左边的数据还是上一行的数据没有更新,这样我们就可以实现自我滚动,为什么j到t[i]就可停下来,因为只有j>=t[i]时才有可能交换,而j<t[i]时保留原来的值就可以了
为什么,要先物品后时间呢?
如果你先时间后物品,你看你每次dp[j]里的值,不就永远只有1个物品吗?你每次上个物品的值都被这次的替换了,脱离了我们
dp[j]的含义!!!!*/
#include <iostream>
using namespace std;
int dp[1010];//dp[j]表示在j时间内可以获得的最大价值

int t[110],m[110];
int main()
{
   
    int T,M;
    cin>>T>>M;
    for(int i=0;i<M;i++) cin>>t[i]>>m[i];

    //初始化:非常重要!!! 

    for(int i=0;i<=T;i++){
    //注意时间可以到达第T秒 
        if(i>=t[0]) dp[i]=m[0]; //看下第0个物品能不能放 
        else dp[i]=0;
    }


    for(int i=1;i<M;i++){
    //先物品后时间
        for(int j=T;j>=t[i];j--){
    //注意时间可以到达第T秒 
            dp[j]=max(dp[j],dp[j-t[i]]+m[i]);
        }
    }

    cout<<dp[T];//注意时间可以到达第T秒 

    return 0;    
}
目录
相关文章
|
5月前
|
缓存 监控 Cloud Native
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化
本文深入解析了Java Solon v3.2.0框架的实战应用,聚焦高并发与低内存消耗场景。通过响应式编程、云原生支持、内存优化等特性,结合API网关、数据库操作及分布式缓存实例,展示其在秒杀系统中的性能优势。文章还提供了Docker部署、监控方案及实际效果数据,助力开发者构建高效稳定的应用系统。代码示例详尽,适合希望提升系统性能的Java开发者参考。
269 4
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化
|
SQL 流计算
对于Flink CDC,当源表的数据被删除后,可以通过以下方法在结果表中同步删除
对于Flink CDC,当源表的数据被删除后,可以通过以下方法在结果表中同步删除
1484 1
|
4月前
|
存储 弹性计算 固态存储
阿里云服务器配置费用整理,支持一万人CPU内存、公网带宽和存储IO性能全解析
要支撑1万人在线流量,需选择阿里云企业级ECS服务器,如通用型g系列、高主频型hf系列或通用算力型u1实例,配置如16核64G及以上,搭配高带宽与SSD/ESSD云盘,费用约数千元每月。
443 0
|
9月前
|
机器学习/深度学习 存储 人工智能
什么是 ERP?
企业资源计划(ERP,全称Enterprise Resource Planning)是一种集成化的管理软件系统,旨在通过信息技术手段整合企业的各个业务流程和资源管理,从而提高企业的运营效率和管理水平。ERP系统涵盖了财务、物流、人力资源、生产管理等多个核心模块,帮助企业实现资源的优化配置和业务流程的自动化。 ERP与财务管理的区别
511 5
|
6月前
|
Java 数据库 网络架构
菜鸟之路Day36一一Web开发综合案例(部门管理)
本文详细记录了基于Spring Boot的Web开发综合案例——部门管理功能的实现过程。从环境搭建到功能开发,涵盖数据库表设计、Spring Boot项目创建、依赖引入、配置文件设置以及Mapper、Service、Controller的基础结构构建。文章重点讲解了查询、删除、新增和修改部门信息的业务逻辑实现,遵循RESTful规范设计接口,并通过统一响应结果类`Result`优化前后端交互体验。借助Spring的IoC容器管理与MyBatis的SQL映射,实现了高效的数据操作与业务处理,最终完成部门管理的全功能开发。
221 12
|
6月前
|
SQL 存储 关系型数据库
菜鸟之路Day29一一MySQL之DDL
本文《菜鸟之路Day29——MySQL之DDL》由作者blue于2025年5月2日撰写,主要介绍了MySQL中的数据定义语言(DDL)。文章详细讲解了DDL在数据库和表操作中的应用,包括数据库的查询、创建、使用与删除,以及表的创建、修改与删除。同时,文章还深入探讨了字段约束(如主键、外键、非空等)、常见数据类型(数值、字符串、日期时间类型)及表结构的查询与调整方法。通过示例代码,读者可以更好地理解并实践MySQL中DDL的相关操作。
241 11
|
5月前
|
SQL 监控 Java
菜鸟之路Day40一一事物管理&AOP
本文主要介绍了事物管理和AOP(面向切面编程)的相关知识。在事物管理部分,详细讲解了SQL中的事物控制语句以及Spring框架中的事物注解@Transactional的使用方法和属性配置,结合删除部门及员工的实际案例说明了事物传播行为的应用场景。AOP部分首先概述了其概念和优势,如代码复用与关注点分离,并通过计算service层方法运行时间的实例展示了AOP的实现方式。接着深入探讨了AOP的核心概念,包括切面、连接点、切入点和通知类型,最后通过一个综合案例——操作日志记录到数据库中,演示了如何利用自定义注解和AOP技术完成统一功能的添加。
176 40
|
5月前
|
JSON 数据格式 开发者
淘宝天猫图片搜索商品接口(附代码示例)
拍立淘图片搜索接口支持开发者通过上传图片或提供图片URL,在淘宝、天猫平台搜索相似商品,适用于商品识别、比价等场景。接口采用POST(上传图片)或GET(图片URL)请求方式,返回JSON格式数据,包含商品ID、标题、价格、卖家信息、销量及图片URL等详情,参数可指定搜索关键词、类目、结果数量等,默认返回20条。
|
5月前
|
算法 IDE 开发工具
蓝桥杯备赛经验帖
本文是作者blue分享的蓝桥杯备赛经验帖,旨在帮助刚接触算法竞赛的新手。文章从个人参赛经历出发,详细介绍了蓝桥杯的OI赛制特点、比赛流程以及备赛建议。作者强调了重点掌握基础数论、DFS、数组操作、二分法、动态规划等知识,并建议多参与线上赛,熟悉输入输出规则,同时避免盲目刷题或过度依赖力扣。通过参赛,作者不仅提升了编码能力,还结识了优秀的朋友,认识到自身差距,激励自己不断进步。此经验适合新手参考,大佬可略过。
466 4
|
7月前
|
存储 网络协议 API
Cpp网络编程Winsock API
本文详细介绍了使用Winsock API进行C++网络编程的过程,通过具体实例实现了一个基于TCP协议的C/S架构通信demo。文章从服务端与客户端两方面展开,涵盖网络库初始化、套接字创建、绑定IP与端口、监听与连接、数据收发到关闭连接等关键步骤。重点解析了`WSAStartup`、`socket`、`bind`、`listen`、`accept`、`connect`、`send`和`recv`等函数的使用方法及注意事项,并对比了标准库与Winsock库在链接时的区别。适合初学者了解Winsock网络编程基础。
337 35