DAY44:动态规划(四)整数拆分(递归+DP递推都可以做,注意区别和理解)

news2024/12/26 15:20:31

文章目录

    • 343.整数拆分
    • 思路1:递归法(最直观的想法)
      • 递归思路
      • 普通递归写法
        • 注意点:max的嵌套
        • 普通递归存在的问题
      • 记忆化搜索+递归写法
        • 时间复杂度
      • 递归解法总结
    • 思路2:动态规划(注意递推的理解)
      • 确认DP数组含义
      • 递推公式
      • 初始化
      • 遍历顺序
      • DP写法完整版
        • dp[i]求最大值为什么还要再写一遍
        • 时间复杂度
    • 总结:递归和DP递推的区别
      • 动态规划和递归
      • 状态转移方程的进一步理解
      • 递归法与递推的区别

343.整数拆分

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积

示例 1:

输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

示例 2:

输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

提示:

  • 2 <= n <= 58

思路1:递归法(最直观的想法)

这个问题的目标是找出一个整数n的最大乘积,通过将其拆分为至少两个正整数的和。也就是说,可以出现多个正整数的情况。

递归思路

最直观的思路,我们要找的是一个整数n的最大乘积,可以将这个整数拆分为i和n-i(1 <= i < n)。对于每个i,我们可以选择将n-i继续拆分,也可以选择不再拆分。如果只拆成两个,就是i*(n-i)。如果拆成多个,就是i*dfs(n-i)

所以对于每个i,我们需要比较i*(n-i)i*dfs(n-i)的值,取所有结果中的最大值

普通递归写法

  • 注意找result的最大值,max的比较里一定要加上result本身,要存储for循环遍历中所有的最大值
  • 注意(n-i)这样的情况要加括号,不然会发生逻辑错误
class Solution {
public:
    int dfs(int n){
        //递归终止条件:输入为1,也就是已经拆到1了,直接返回
        if(n==1) return 1;
        int result=0;
        for(int i=1;i<n;i++){
            //(n-i)加括号
            result = max(result,max(i*(n-i),i*dfs(n-i)));//找到result的最大值,i*n-i是拆两个,i*dfs(n-i)是拆多个
        }
        return result;
    }
    int integerBreak(int n) {
		return dfs(n);
    }
};

注意点:max的嵌套

max()函数在c++标准库中定义的版本只接受两个参数。也就是说max()函数在每次调用时只比较两个值,因此,如果要比较三个或更多的值,需要将max()函数嵌套在一起使用。例如下面:

result = max(result,max(i*n-i,i*dfs(n-i));

max的三个元素比较的嵌套,嵌套任意两个都可以。无论如何嵌套max()函数,只要确保所有的数值都被比较,就能得到相同的结果

result = max(i*n-i,max(result,i*dfs(n-i)));//这样写也可以正确运行,也是比较这三个数字

普通递归存在的问题

在这里插入图片描述
用上面的写法来写,小用例可以通过,但是较大的用例会发生超时。

在这里插入图片描述
这是因为普通递归的思路,我们会多次计算相同值的最大乘积。例如下图情况:

  • n/2之后的情况,i(n-i)的情况是重复的
  • dfs内部拆分的时候,类似5 4这样比较小的数字,会被多次拆分,例如我们要拆分9的时候,dfs(5)会拆一次,拆分8的时候,dfs(5)还是会再拆一次(图中绿色横线表示)。比较小的数字,每次拆分大的数字都会重新拆分同样的拆分操作就会在多次递归调用中重复进行,导致大量的重复计算

在这里插入图片描述
即使我们令i<=2/n做了优化,避免了后面一半的重复,依然会发生超时。

class Solution {
public:
    int dfs(int n){
        //递归终止条件:输入为1,也就是已经拆到1了,直接返回
        if(n==1) return 1;
        int result=0;
        for(int i=1;i<=n/2;i++){
            result = max(result,max(i*(n-i),i*dfs(n-i)));//找到result的最大值,i*n-i是拆两个,i*dfs(n-i)是拆多个
        }
        return result;
    }
    int integerBreak(int n) {
		return dfs(n);
    }
};

在这里插入图片描述

记忆化搜索+递归写法

为优化时间复杂度,我们采用记忆化搜索来防止同一个数字被重复拆分

记忆化搜索原理是使用一个数组memo,用来保存每个整数的dfs拆分结果。当我们要计算一个数的dfs(n)(也就是这个数字拆分后的最大乘积)时,首先检查memo数组,如果已经计算过,就直接返回,否则计算出结果后保存在memo数组中。

这么做可以防止类似dfs(5)在多个较大整数拆分的时候被重新拆的情况。

  • 如果memo[n]已经有数值了,不用拆分,直接返回当前的记忆数值(历史拆分结果)
  • memo[n]没有数值,才进行拆分的逻辑
class Solution {
public:
    //创建记忆数组,初始化为0
    vector<int>memo;
    
    int dfs(int n){
        //递归终止条件:已经for结束存在memo里面了
        //如果memo[n]已经有数值了,不用拆分,直接返回当前的数值(历史拆分结果)
        if(memo[n]!=0) return memo[n];
        int result=0;
        for(int i=1;i<=n/2;i++){
            result = max(result,max(i*(n-i),i*dfs(n-i)));//找到result的最大值,i*n-i是拆两个,i*dfs(n-i)是拆多个
        }
        memo[n]=result;
        return result;
    }
    int integerBreak(int n) {
        //memo初始化
        memo = vector<int>(n+1,0);//注意memo的重新赋值
		return dfs(n);
    }
};

进行了记忆化搜索的优化,递归写法就能通过这道题了。

在这里插入图片描述

时间复杂度

时间复杂度:O(n²)。递归树的深度为n,每一层最多需要做n次操作(在for循环中比较和更新结果),所以时间复杂度为O(n²)。

空间复杂度:O(n)。我们使用了一个大小为n的一维数组memo来存储已经计算过的结果,所以空间复杂度为O(n)。

递归解法总结

递归做法更加直观也更好理解,核心就是result = max(result, i*(n-i), i*dfs(n-i))枚举从1–n的所有i和n-i的乘积结果,找到最大值。(实际上枚举1–n/2即可,i和n-i后半段是重复的)

但是这种做法,dfs(n-i)的拆分里会有大量的重复拆分,例如dfs(5)这个数值,遍历到dfs(9),dfs(8),dfs(7)的时候都会再拆一遍,造成大量时间消耗,会超时。

因此需要靠记忆化搜索进行防止重复拆分的操作,用memo数组存下每一次拆分的最终结果dfs(n)。递归之前检查如果memo数组有这个dfs(n),直接返回memo存的结果。

dfs(n)的含义就是n拆分后得到的因子乘积最大值结果。

思路2:动态规划(注意递推的理解)

动态规划的思想,是当前状态全部由之前的状态推出来。也就是说,如果要获得最大乘积,我们需要让i(1<i<n)范围内的所有数字,都满足乘积最大化的需求

确认DP数组含义

DP解法中,dp[i]的含义和递归的dfs(n)是一样的,都是表示数字i/n当前拆分乘积的最大值。i表示从1开始到n所有的数字。

递推公式

递推公式的思想和递归也是相同的。从1遍历j,有两种渠道得到dp[i]。一个是j * (i - j) 直接相乘。一个是j * dp[i - j],相当于是拆分(i - j)。

因此递推公式为:dp[i]=max(dp[i],j*(i-j),j*dp[i-j])。这个递推公式的含义是,对于i<n范围的所有i,都要满足得到的dp[i]是乘积最大值这样的话, 遍历到i=n的时候,dp[n]才能是最大值

初始化

从dp[i]的定义来说,dp[0],dp[1] 是没有意义的数值,是不需要初始化的。因为0和1没有能够拆分相加=i的因子。而且n是正整数,不需要考虑n=0的情况。题目提示里给出了n>=2。

因此,我们只需要初始化dp[2]=1,因为i=2的时候,最大乘积为1(2拆成1+1)。

遍历顺序

这里的遍历顺序仍然是正序,因为需要基于前面的状态来计算当前的状态。

让所有的i都满足输出dp[i]是最大乘积,遍历逻辑如下:

//i初始值是2,第一个从3开始
for(int i=3;i<=n;i++){
    //i的因子从1开始
    for(int j=1;j<=i-1;j++){
        //和递归写法很像,都是取i情况下的最大值
        dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]));
    }
}
//结果就是dp[n]
return dp[n];

DP写法完整版

class Solution {
public:
    int integerBreak(int n) {
        //DP数组建立,注意数组本身容量赋值
        vector<int>dp(n+1,0);
        //初始化
        dp[2]=1;
        for(int i=3;i<=n;i++){
            for(int j=1;j<=i-1;j++){
                dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]));
            }
        }
        return dp[n];
    }
};

dp[i]求最大值为什么还要再写一遍

因为在递推公式推导的过程中,每次计算dp[i],需要取所有dp[i]中的最大值。

和下面引入result的写法是一样的。result完全可以用dp[i]来代替

class Solution {
public:
    int integerBreak(int n) {
        //DP数组建立,注意数组本身容量赋值
        vector<int>dp(n+1,0);
        //初始化
        dp[2]=1;
        for(int i=3;i<=n;i++){
            //记录dp[i]最大值
            int result=0;
            for(int j=1;j<=i-1;j++){
                result=max(result,max(j*(i-j),j*dp[i-j]));
            }
            //最大值存在dp[i]里
            dp[i]=result;
        }
        return dp[n];
    }
};

时间复杂度

时间复杂度:O(n²)。因为有两层for循环,每一层最多需要做n次操作(比较和更新dp[i]),所以时间复杂度为O(n²)。

空间复杂度:O(n)。我们使用了一个大小为n的一维数组dp来存储所有状态的结果,所以空间复杂度为O(n)。

总的来说,这两种方法的时间复杂度和空间复杂度都相同,但实际运行效率可能因为具体实现(例如递归的开销、记忆化搜索的查找开销等)和语言特性等因素有所不同。

总结:递归和DP递推的区别

本题中,递归解法和DP解法的核心思路是一样的,都是找到一个整数可以拆分为的两个数的最大乘积,这两个数可以是一个具体的整数和一个可能继续拆分的整数。

不同的是,递归解法是从上到下,对每个整数,都尝试所有可能的拆分方式,然后选择最大的乘积;而DP解法是从下到上,对每个整数,根据已经计算出的较小整数的最大乘积,来推导出当前整数的最大乘积

动态规划和递归

动态规划(DP)是一种以空间换时间的算法设计技术,通过存储和重用过去的结果,避免了重复计算,从而提高了算法的效率。DP是一种特殊的递推,通常我们会找到一个或一组状态转移方程,用于描述问题之间的关系,然后从基本情况(初始状态)开始,按照这个(这些)方程递推出所有的状态

递归是一种算法设计技术,它通过将问题分解为更小的子问题来解决问题,而这些子问题与原问题具有相同的形式。通常,递归需要一个或多个基本情况来停止递归。当递归处理的子问题有重叠时(即在递归过程中多次求解相同的子问题),使用递归可能会导致效率低下。这时就可以使用动态规划或记忆化搜索来改进

状态转移方程的进一步理解

动态规划的状态转移方程,就是在找一个适用于从小到大所有元素的公式,这个公式可以把一个大问题转化为几个小问题的关系

在整数拆分这个问题中,状态转移方程为:dp[i] = max(j*(i-j), j*dp[i-j]),这个方程表示,对于一个正整数i,我们要么选择将它拆分为j和i-j(不再拆分i-j),要么选择将它拆分为j和以某种方式继续拆分i-j得到的最大乘积。然后我们遍历所有的j(1 <= j <= i-1),找到能使这个乘积最大的j,即为dp[i]的值。这样我们就从小到大,依次求出了所有整数的最大乘积

递归法与递推的区别

DP的从下到上推算,就是从小到大的每一个i,都要推出最大的i的结果,这样到n的时候dp[n]也是最大的。

而递归从上到下,就是从最大的n开始分割,然后递归的寻找所有可能的拆分方式,也就是从大到小一直枚举所有的可能的乘积情况。然后在这些情况中选择乘积最大的。在这个过程中,由于涉及到重复的子问题,所以我们使用记忆化搜索,也就是用一个数组来保存已经计算过的结果,避免了重复计算。这是一种从上到下的方法,我们是从最大的问题开始,然后逐渐深入到更小的子问题

两者的区别在于求解的顺序和重复子问题的处理方式,但是解决的问题和达到的目的是相同的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/740982.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Transformer 模型详解

Transformer模型 https://blog.csdn.net/m0_67084346/article/details/128138486 https://blog.csdn.net/benzhujie1245com/article/details/117173090 2017 年&#xff0c;Google 在论文 Attention is All you need 中提出了 Transformer 模型&#xff0c;其使用 Self-Atten…

一个SpringMVC的小项目

一个图书管理小项目&#xff1a; 定义对应的表结构&#xff0c;为了学习所以才添加大量的 SQL 规则&#xff0c;要记得针对货币的处理方案 create table if not exists tbl_books( id bigint primary key auto_increment,book_name varchar(32) not null,book_price numeric(8…

专业的PDF文件压缩工具推荐,让你的PDF文件轻松压缩

​在参加专业的比赛时&#xff0c;就需要用到pdf文件&#xff0c;如果pdf文件过大操作和分享起来就特别不方便&#xff0c;其实可以使用专业的pdf文件压缩工具来处理。今天就分享一款pdf在线压缩工具&#xff0c;通过浏览器就可以快速完成pdf压缩&#xff08;https://www.yasuo…

SQL22 统计每个学校的答过题的用户的平均答题数

SELECT university,COUNT(qt.question_id)/COUNT(distinct(qt.device_id)) avg_answer_cnt FROM question_practice_detail qt LEFT JOIN user_profile ut ON qt.device_idut.device_id GROUP BY university

使用Word轻松实现PDF转Word

以前WPS还能通过每天打卡白嫖会员&#xff0c;最近不行了&#xff0c;害&#xff0c;羊毛没了 现在重新回归Word&#xff0c;利用Word就可以将PDF转化为Word 一、通过Word新建一个Word文档并打开 二、点击 文件 —> 打开 三、浏览&#xff0c;找到要转的PDF 四、点击确定&…

基础篇--初识STM32

初识STM32 STM32是什么 ST&#xff1a;意法半导体 M&#xff1a;MCU/MPU32:32位 ST累计推出了&#xff1a;5大类、18个系列、1000多个型号的Cortex内核微控制器 STM32芯片分类 ST中文社区网&#xff1a;https://www.stmcu.org.cn/ ST官网&#xff1a;https://www.st.com …

4.5Java EEMyBatis缓存机制

一、 一级缓存 MyBatis的一级缓存级别 MyBatis的一级缓存是SqlSession级别的缓存。如果同一个SqlSession对象多次执行完全相同的SQL语句时&#xff0c;在第一次执行完成后&#xff0c;MyBatis会将查询结果写入到一级缓存中&#xff0c;此后&#xff0c;如果程序没有执行插入、…

Mysql (insert,update操作)

1.创建表&#xff1a; 创建员工表employee&#xff0c;字段如下&#xff1a; id&#xff08;员工编号&#xff09;&#xff0c;name&#xff08;员工名字&#xff09;&#xff0c;gender&#xff08;员工性别&#xff09;&#xff0c;salary&#xff08;员工薪资&#xff09; …

脚踏Java知识点

对上节Java的基础语法续讲 三元运算符和if语句格式的区别 语法格式&#xff1a; 三元运算符的语法格式是&#xff1a;(condition) ? expression1 : expression2&#xff1b; if语句的语法格式是&#xff1a; if (condition) { // 执行 expression1 } else { // 执行 express…

Stage模型HarmonyOS服务卡片开发整体说明

服务卡片&#xff08;以下简称“卡片”&#xff09;是一种界面展示形式&#xff0c;可以将应用的重要信息或操作前置到卡片&#xff0c;以达到服务直达、减少体验层级的目的。卡片常用于嵌入到其他应用&#xff08;当前卡片使用方只支持系统应用&#xff0c;如桌面&#xff09;…

cyclo(Ser-Ser),23409-30-5,环(L-丝氨酰基-L-丝氨酰),具有明确的生物活性

​资料编辑|陕西新研博美生物科技有限公司小编MISSwu​ 产品描述&#xff1a; cyclo(Ser-Ser)&#xff08;CAS号&#xff1a;23409-30-5&#xff09;&#xff0c;环二肽(2,5-哌嗪二酮)是Z小的环肽&#xff0c;许多天然环二肽化合物都具有明确的生物活性&#xff0c;例如作为抗…

什么是矢量数据库?

我们正处于人工智能革命之中。它颠覆了它所接触的任何行业&#xff0c;承诺了伟大的创新 – 但它也带来了新的挑战。对于涉及大型语言模型、生成式 AI 和语义搜索的应用程序&#xff0c;高效的数据处理变得比以往任何时候都更加重要。 所有这些新应用程序都依赖于向量嵌入&…

【Android Framework系列】5章 AMS启动流程

1 AMS简介 AMS&#xff08;Activity Manager Service&#xff09;是Android中最核心的服务&#xff0c;管理着四大组件的启动、切换、调度及应用进程的管理和调度等工作。AndroidQ将Activity移到了ActivityTaskManagerService中&#xff0c;但也和AMS相关联。 AMS通过使用一些…

3.元素的显示与隐藏

类似网站广告, 当我们点击关闭就不见了, 但是我们重新刷新页面, 会重新出现! 本质:让一个元素在页面中隐藏或者显示出来。 display显示隐藏&#xff0c;不保留原来的位置visibility 显示隐藏&#xff0c;保留原来的位置overflow 溢出显示隐藏&#xff0c;只对溢出的部分进行处…

1000道网络安全必备面试题合集,秋招金九银十必看!!!

前言 以下为网络安全各个方向涉及的面试题&#xff0c;星数越多代表问题出现的几率越大&#xff0c;祝各位都能找到满意的工作。 注&#xff1a;本套面试题&#xff0c;已整理成pdf文档&#xff0c;但内容还在持续更新中&#xff0c;因为无论如何都不可能覆盖所有的面试问题&a…

Ctfshow web入门 PHP特性篇 web89-web151 全

web入门 PHP特性篇的wp都一把梭哈在这里啦~ 有点多&#xff0c;师傅们可以收藏下来慢慢看&#xff0c;写的应该挺全面的叭… 有错误敬请斧正&#xff01; CTFshow PHP web89 看题目&#xff0c;有个flag.php文件。题目要求get有个num&#xff0c;是数字但是不包含0-9。 intv…

【opencv之cv::Mat数据深拷贝和浅拷贝探讨】

cv::Mat数据深拷贝和浅拷贝 cv::Mat 拷贝方法实验测试1.matA matSrc2.matB(matSrc)3.matC matSrc.clone()4.matSrc.copyTo(matD) 很多时候写程序除了一个强大的架构&#xff0c;细节也很重要&#xff0c;俗话说的话细节决定成败嘛&#xff0c;在使用cv::Mat做图片处理的时候发…

C#(五十六)之线程池

线程池&#xff1a; 线程池是一种多线程的形式&#xff0c;其中的任务被添加到队列中&#xff0c;并在创建线程时自动启动。 ThreadPool类&#xff1a;以下都是静态方法&#xff1a;&#xff08;不需要new的&#xff09; GetAvailableThreads剩余空闲线程数 GetMaxThreads最…

生成token的两种方式

方式一&#xff1a;自定义工具类 手动编写代码&#xff0c;写两个方法&#xff0c;一个生成&#xff0c;一个解析&#xff1b; 第一步&#xff1a;导入依赖 <!--JWT令牌依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjw…

i2c_tool的使用

文章目录 前言一、交叉编译i2c_tool二、板子上使用i2c_tool三、为什么不需要编写驱动也能够访问到对应设备四、命令行使用i2_tool操作AP3216模块五、使用i2c_tool代码操作IIC设备六、相关函数讲解1.open_i2c_dev2.int set_slave_addr 七、具体代码编写总结 前言 本篇文章将带大…