动态规划算法(3)(不同方案数问题+拆分问题)

news2024/9/23 9:23:15

文章目录

  • 不同路径
  • 不同路径II
  • 整数拆分
  • 不同的二叉搜索树

动态规划解题五步走:

  1. 确定dp数组以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

不同路径

力扣传送门:
https://leetcode.cn/problems/unique-paths/description/

  1. 确定dp数组以及下标的含义:
    在本题中dp是一个二维数组,因为这是一个二位的地图,使用dp[i][j]表示在地图原点到 (i,j)这个位置的路径数量
    因此我们到达终点的方案数就是dp[i][j]的值,即 (i,j)等于终点处的(i,j)

  2. 确定递归公式
    由题意得在地图上某个点,我们只能向右或者向下走一步,因此要想到达 (i,j)这个位置,我们可以从(i-1,j)即从上往下走,或者从(i,j-1)即从左往右走。这两种方案来到达,因此便可以推导出我们的递推公式:dp[i][j]=dp[i-1][j]+dp[i][j-1]

注意我们的dp[i][j]存储的走到这个位置的方案数量,因此从上面来的方案数 + 从左边来的方案数 = 我们当前的点的方案数。

  1. dp数组如何初始化
    可以发现,我们的地图第一行或者第一列上的每一个点都只能有一种方案走到,即一直往右走,或者一直往下走,因此我们的 dp[0][j]应该初始化为1(只有一种方案),dp[i][0]同理也应该初始化为1。

  2. 确定遍历的顺序
    我们要计算 (i,j)处的方案数,因此必须先求得它上面的和左边的方案数,它们两者之和即是(i,j)的方案数,因此要从左往右,从上往下计算

  3. 举例推导dp数组
    我们来用图描绘一下这个过程:
    在这里插入图片描述
    所以我们到达终点(i,j)的方案数就是(i-1,j)的方案数 + (i,j-1)的方案数。

代码如下:

class Solution {
public:
    int uniquePaths(int m, int n) {
    	//dp数组创建
        vector<vector<int>> dp(m,vector<int>(n));
        for (int i=0;i<m;i++)
        {
            for (int j=0;j<n;j++)
            {
            	//边界:第一列和第一行只能有一种方案
                if (i==0 || j==0)
                {
                    dp[i][j]=1;
                }
                //其他的位置可以有多种情况
                else
                {
                    dp[i][j]=dp[i-1][j]+dp[i][j-1];
                }
            }
        }
        //返回终点处的方案数
        return dp[m-1][n-1];
    }
};

不同路径II

力扣传送门:
https://leetcode.cn/problems/unique-paths-ii/description/

这道题相比第一题多了一个障碍物的概念

障碍物代表着什么?

如果(i,j)是一个障碍物,则表示在dp[i][j]中此位置的数值是0.

如果dp[i][j]的数值是0,则对于其下一个点来说,例如(i,j+1)就只能由(i-1,j+1)得到,因为(i,j)的dp数值是0,是一个障碍物。

如图: dp数组随着地图的变化依次计算每一位置的数值,最后的结果保存的就是dp数组的最后一个数据。

在这里插入图片描述

代码如下:

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m=obstacleGrid.size();
        int n=obstacleGrid[0].size();
        vector<vector<int>> dp(m,vector<int>(n));
        for (int i=0;i<n && obstacleGrid[0][i]==0;i++)
        {
            dp[0][i]=1;
        }
        for (int i=0;i<m && obstacleGrid[i][0]==0;i++)
        {
            dp[i][0]=1;
        }
        for (int i=1;i<m;i++)
        {
            for (int j=1;j<n;j++)
            {
                if (obstacleGrid[i][j]==0)
                {
                     dp[i][j]=dp[i-1][j]+dp[i][j-1];
                }
                else
                {
                    dp[i][j]=0;
                }
            }
        }
        return dp[m-1][n-1];
    }
};

时间复杂度:O(nm) 空间复杂度:O(n)


有人就说了,这和上面的那道题不是基本一致的吗,为何要重复在贴一遍呢。
这道题我之所以要在这里在写一遍,是为了引出动态规划的一个非常重要的优化思路:【滚动数组思想

普通的动态规划是以大量的空间来换取时间的,但是有没有什么办法使得对空间的利用能够减小,而且又能利用动态规划的思想来解决问题呢?

使用优化的动态规划,可以让我们的空间复杂度达到O(1)。

注意看这道题: 我们在dp计算地图的每一个点的数值的时候,我们没有必要创建一个和整个地图一样大的dp数组,而且我们的返回值只有(m-1,n-1)终点这一个dp数组的值,其他的值全部都是多余的,我们只需要一个一维保存当前的行的值即可,然后依次计算按每一行,进行dp数组的更新。

总结一句话就是:我们使用一个dp[j]保存每一行各列的计算结果,然后随着地图的行的更新,我们的dp数组也会更新为每一行数值,直到到达最后一行,最后一个数值即是我们的终点值。

图解:一维进行每一行的各列的计算,如果出现了障碍,则此点置0,否则就加上一个点的数据得到当前点的数据。
在这里插入图片描述

代码如下:

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int m=obstacleGrid.size();
        int n=obstacleGrid[0].size();
        //只使用一个一维dp数组,保存的是每一行的值
        vector<int> dp(n);
        //dp数组首元素的初始化,第一个是不是正常的路
        dp[0]=(obstacleGrid[0][0]==0);//遍历整个地图
        for (int i=0;i<m;i++)
        {
            for (int j=0;j<n;j++)
            {
            	//如果地图的值为1,则是障碍物,则把dp数组对应的此点置0,即无法通过
                if (obstacleGrid[i][j]==1)
                {
                    dp[j]=0;
                    //直接跳到下一列
                    continue;
                }
                //如果地图《上一个位置》的值为0,则表示此位置可以由上一个位置得到,加上左边的位置即可得到当前的位置
                if (j-1>=0 && obstacleGrid[i][j-1]==0)
                {
                    dp[j]+=dp[j-1];
                }
            }
        }
        //最后存储的即是终点。
        return dp.back();
    }
};

时间复杂度:O(nm) 空间复杂度:O(1)


整数拆分

力扣传送门:
https://leetcode.cn/problems/integer-break/

假设我们有一个数字:n(n>=2),那么这个n可以被拆分成 x,则另一半就是 n-x。此时我们面临两种选择:

  1. 我们不需要再分了, x * (n-x) 就是我们拆分的个数相乘的最大值 ,此时 k = 2
  2. 我们还可以再分(n-x)还可以继续当作一个新的 n,然后再次回到一开始,把他拆分成x和(n-x)… 那么我们就可以推断出来。这一步的最大值就是我们之前进行相同的操作所得到的最大值,此时 k > 2

由此可见,寻找拆分的整数的最大值其实就是寻找之前的某个拆分的最大值,满足动态规划的思想。

我们按照动态规划五步来进行推理:

  1. 确定dp数组以及下标的含义。
    创建dp[n]一维数组,dp[n]存储的是第n个数的拆分乘积最大值。

  2. 确定递推公式
    经过我们刚才分析的两步可以得出:
    当前拆分某一个数字的所有可能的情况:max( x * (n-x),x * dp[n-x] ),注意我们要取得这两种情况的最大值。
    当前的数字拆分的最大值: dp[n] = max(dp[n],max( x * (n-x),x * dp[n-x]))

  3. dp数组如何初始化
    由于题目规定了 n>= 2 ,因此我们的准确的初始化从下标为2开始,因为0 1在此题中无效,而且在实际上我们也不需要他们,因此我们只需要:dp[2] = 1,这是公认的,数字2只有一种情况。

  4. 确定遍历顺序
    由于大数的最大值可以由小数的最大值得到,我们依次从小数到大数遍历。

  5. 推导dp数组:
    依次拆分数字x: 1 2 3…一直到n的一半(因为后面的跟前面的是对称的),得到的另一半分别是: n-1 n-2 n-3,然后根据dp公式依次寻找他们的最大值。
    在这里插入图片描述

class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n+1);
        dp[2]=1;
        // i 相等于我们公式的 n
        for (int i=3;i<=n;i++)
        {
        	// j 相等于我们公式的 x,另一半则是(i - j)
            for (int j=1;j<=i/2;j++)
            {
                dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]));
            }
        }
        //dp里存储的就是最大值
        return dp[n];
    }
};

时间复杂度:O(n^2)空间复杂度:O(n)

对于动态规划的优化,我们不再讨论。


不同的二叉搜索树

力扣传送门:
https://leetcode.cn/problems/unique-binary-search-trees/

这道题表面上没有什么特别的思路,我们可以从画图来找找动态规划的规律

节点为1的情况:只有一个二叉搜索树
在这里插入图片描述
节点为2的情况:可以有两种二叉搜索树。节点1为根节点,节点2为根节点
在这里插入图片描述
节点为3的情况:可以有五种二叉搜索树。节点1,节点2,节点3都可以为根节点
在这里插入图片描述

我们来观察n=3的情况:
节点1为根节点,与n=2的节点1为根节点的结构一致! 都是左子树为空,右子树不为空。
节点3为根节点,与n=2的节点2为根节点的结构一致! 都是左子树不为空,右子树不为空。
节点为2为根节点,与n=1的节点1为根节点的结构一致 都是左右子树为空,或者说同时具有左右子树,因为每一个节点又都是一颗新的树。

发现到这里,其实我们就找到了重叠子问题了,其实也就是发现可以通过dp[1] 和 dp[2] 来推导出来dp[3]的某种方式。

我们不妨大胆假设:
n=3时 生成的二叉搜索树的数量 = 节点1为根节点生成的二叉搜索树的数量 + 节点2为根节点生成的二叉搜索树的数量 + 节点3为根节点生成的二叉搜索树的数量

节点1为根节点生成的二叉搜索树的数量 = 左子树有0个元素的子树的数量 * 右子树有2个元素的子树的数量。
节点2为根节点生成的二叉搜索树的数量 = 左子树有1个元素的子树的数量 * 右子树有1个元素的子树的数量。
节点3为根节点生成的二叉搜索树的数量 = 左子树有2个元素的子树的数量 * 右子树有0个元素的子树的数量。

这是n=3的准确的情况,我们把这个数字变到 n试一下:


我们也许会发现规律:当 n=n时:
生成的二叉搜索树的数量 = 节点1为根节点生成的二叉搜索树的数量 + 节点2为根节点生成的二叉搜索树的数量 + 节点3为根节点生成的二叉搜索树的数量 + 节点4… +节点5 … + 节点n为根节点生成的二叉搜索树的数量

节点1为根节点生成的二叉搜索树的数量 = 左子树有0个元素的子树的数量 * 右子树有 n-1 个元素的子树的数量。
节点2为根节点生成的二叉搜索树的数量 = 左子树有1个元素的子树的数量 * 右子树有 n-2 个元素的子树的数量。
节点3为根节点生成的二叉搜索树的数量 = 左子树有2个元素的子树的数量 * 右子树有 n-3 个元素的子树的数量。

节点4为根节点生成的二叉搜索树的数量 = 左子树有3个元素的子树的数量 * 右子树有 n-4 个元素的子树的数量。
节点5为根节点生成的二叉搜索树的数量 = 左子树有4个元素的子树的数量 * 右子树有 n-5 个元素的子树的数量。



节点n为根节点生成的二叉搜索树的数量 = 左子树有 n-1 个元素的子树的数量 * 右子树有 0 个元素的子树的数量。

动态规划递推公式:dp[i] = dp[j-1] * dp[i-j]

由此,我们正式进入动态规划的总结阶段:

  1. 确定dp数组以及下标所表示的含义
    dp[i]表示n == i时,生成的二叉搜索树的数量。

  2. 确定递推公式
    dp[i]=dp[i-1] * dp[i-j]

  3. dp数组的初始化
    我们初始化 dp[0] =1 ,因为我们首先要求得dp[1]=1 ;因此dp[1] = dp[0] * dp[0]它必须是1,因此dp[0]必须初始化为1.

  4. dp数组如何遍历
    依次填充dp每一个数字为根节点的生成的二叉搜索树的个数,在其中再次求解子问题。

  5. 举例推导dp公式

代码示例:

class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n+1);
        //dp初始化为1
        dp[0]=1;
        //依次填充每一个dp[i]
        for (int i=1;i<=n;i++)
        {
        	//每一个dp[i]都是由 dp[j-1] * dp[i-j] 各自相加得到
            for (int j=1;j<=i;j++)
            {
                dp[i]+=dp[j-1]*dp[i-j];
            }
        }
        //第n个dp
        return dp[n];
    }
};

时间复杂度:O(n^2) 空间复杂度:O(n)

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

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

相关文章

[附源码]JAVA毕业设计酒店订房系统(系统+LW)

[附源码]JAVA毕业设计酒店订房系统&#xff08;系统LW&#xff09; 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&…

基于opencv答题卡识别基本处理_1

文章目录1.读取图片2.图片预处理2.1 原图转换为灰度图2.2 高斯滤波处理&#xff0c;去除噪声点2.3 增强亮度2.4 自适应二值化2.5 图片可视化3. 添加边框3.1 使用copyMakeBorder添加边框3.2 可视化图片查看效果3.3 手动截取答题卡区域1.读取图片 img cv2.imread(images/5.png)…

Nx C++程序使用spdlog库进行日志存储

1 spdlog简介 spdlog是一个开源的日志库&#xff0c;在github上有。代码见这里&#xff0c;文档这里 C语言的&#xff0c;支持Linux、windows等系统。 csdn上也有许多介绍&#xff0c;这里列举两个&#xff1a;1、2 2 使用 2.1下载编译链接 有多种使用方式&#xff0c;这里…

(三)沟通管理风险管理采购管理@相关方管理

沟通管理目录概述需求&#xff1a;设计思路实现思路分析1.沟通管理绩效报告提供资源2.管理沟通3.监督沟通风险管理规划风险管理识别风险定性风险分析&#xff1a;定量分析风险规划风险应对实施分享应对监督风险采购管理&#xff1a;12.1 规划采购的管理12.2 实施采购控制采购相…

ResNet网络的改进版:ResNeXt

之前的文章讲过ResNet网络的基本架构&#xff0c;其本质就是让网络的学习目的从学习转为学习&#xff0c;也就是学习输入和输出之间的残差信息&#xff0c;从而缓解了梯度消失和网络退化问题。 本文讲下ResNet网络的改进版&#xff1a;ResNeXt。 架构 下面是ResNet和ResNeXt的架…

9.2、面向对象高级特性(类方法和静态方法、property类属性、单例模式)

文章目录类方法和静态方法property类属性单例模式基于装饰器实现使用_ _ new _ _方法实现面向对象总结类方法和静态方法 类里面定义的方法称为实例方法&#xff0c;python解释器会自动将对象&#xff08;或实例&#xff09;传入方法【pycharm中会自动将self传入&#xff0c;se…

【机器学习】支持向量机【下】软间隔与核函数

有任何的书写错误、排版错误、概念错误等&#xff0c;希望大家包含指正。 在阅读本篇之前建议先学习&#xff1a; 【机器学习】拉格朗日对偶性 【机器学习】核函数 由于字数限制&#xff0c;分成两篇博客。 【机器学习】支持向量机【上】硬间隔 【机器学习】支持向量机【下】…

研发效能工程实践-利用Superset快速打造大数据BI平台

大数据BI平台自研之殇 随着互联网发展&#xff0c;现在随便哪个公司都手握大量数据。如何利用这些数据为公司商业带来价值&#xff0c;触使各个公司投入大量人力财力去做商业智能。 早期的BI可能就是公司Leader叫开发小哥写几句SQL导出数据&#xff0c;然后导入到Excel里绘制几…

echarts:nuxt项目使用echarts

一、项目环境 nuxt 2.X vue2.X vuex webpack 二、安装 yarn add echarts 三、使用 3.1、plugins目录下创建echarts.js import Vue from vue import * as echarts from echarts // 引入echarts Vue.prototype.$echarts echarts // 引入组件&#xff08;将echarts注册为全…

认证服务-----技术点及亮点

大技术 Nacos做注册中心 把新建的微服务注册到Nacos上去 两个步骤 在配置文件中配置应用名称、nacos的发现注册ip地址&#xff0c;端口号在启动类上用EnableDiscoveryClient注解开启注册功能 使用Redis存验证码信息 加入依赖配置地址和端口号即可 直接注入StringRedisTempla…

HTML静态网页作业——关于我的家乡介绍安庆景点

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

Armbian搭建本地Gitea服务器

Armbian搭建本地Gitea服务器 1 安装Docker Docker 是一个用于开发、发布和运行应用程序的开放平台。 Docker 是一个开源的应用容器引擎&#xff0c;Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器上&…

R语言中的prophet预测时间序列数据模型

本文 将针对R进行的几次建模练习的结果&#xff0c;以魁北克数据为依据&#xff0c;分为13年的训练和1年的测试。prophet与基本线性模型&#xff08;lm&#xff09;&#xff0c;一般加性模型&#xff08;gam&#xff09;和随机森林&#xff08;randomForest&#xff09;进行了比…

ES6:ES6 的新增语法

什么是 ES6 ? ES 的全称是 ECMAScript , 它是由 ECMA 国际标准化组织,制定的一项脚本语言的标准化规范。 年份 版本 2015年6月 ES2015 2016年6月 ES2016 2017年6月 ES2017 2018年6月 ES2018 … … ES6 实际上是一个泛指&#xff0c;泛指 ES2015 及后续的版本。 …

基于最大熵图像插值Maximum Entropy插值算法的图像超分辨重构研究-附Matlab代码

⭕⭕ 目 录 ⭕⭕✳️ 一、引言✳️ 二、图像复原基本原理✳️ 三、最大熵图像插值原理✳️ 四、实验验证✳️ 五、参考文献✳️ 六、Matlab程序获取与验证✳️ 一、引言 图像是一种表达信息的形式&#xff0c;其中&#xff0c;数字图像反馈的信息更加丰富。 在获取图像的过程中…

基于N32G45的按键驱动

基于N32G45的按键驱动 1.N32G45简介 N32G45系列集成了最新一代嵌入式ARM Cortex™-M4F处理器&#xff0c;在Cortex™-M3内核的基础上强化了运算能力、新增加了浮点运算处理单元&#xff08;FPU&#xff09;、DSP和并行计算指令&#xff0c;提供1.25DMIPS/MHz的优异性能。同时其…

JAVA复习【11】单列集合Collection:ArrayList、 LinkedList、HashSet、TreeSet学习与使用

1.首先思考一个问题&#xff1a;为什么要有集合&#xff1f; 我们也知道&#xff0c;数组可以保存多个对象&#xff0c;但是在某些情况下无法确定到底需要保存多少个对象&#xff0c;此时数组不再适用没因为数组的长度不可变&#xff0c;例如&#xff0c;要保存一个学校的学生信…

移动WEB开发之rem布局--less基础

维护 css 的弊端 CSS 是一门非程序式语言&#xff0c;没有变量、函数、SCOPE&#xff08;作用域&#xff09;等概念。 CSS 需要书写大量看似没有逻辑的代码&#xff0c;CSS 冗余度是比较高的。 不方便维护及扩展&#xff0c;不利于复用。 CSS 没有很好的计算能力 非前端开…

前馈神经网络与支持向量机实战 --- 手写数字识别

前馈神经网络与支持向量机实战 — 手写数字识别 文章目录前馈神经网络与支持向量机实战 --- 手写数字识别一、前馈神经网络介绍二、支持向量机介绍三、数据集说明四、环境准备五、实验要求六、Python代码tutorial_minst_fnn-keras.py&#xff1a;使用TensorFlow的Sequential实现…

Linux开发常用ps命令选项详解

【摘要】本文介绍了在Linux应用/内核开发调试中&#xff0c;经常需要用到的两个选项组合&#xff0c;当然&#xff0c;如果你需要查看更多更详尽的选项说明&#xff0c;可以参考man说明文档&#xff0c;即命令行下输入man ps进行查看。 aux选项组合 使用场景&#xff1a;更多…