动态规划课堂5-----子序列问题(动态规划 + 哈希表)

news2024/11/15 17:50:36

目录

引言:

例题1:最长递增子序列

例题2:最长定差子序列

例题3:最长的斐波那契子序列的长度

例题4:最长等差数列

例题5:等差数列划分II-子序列

结语:


引言:

要想解决子序列问题那么就要理解子序列和子数组的区别,二者的定义如下。

子序列:是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

子数组:是数组中的一个连续部分[6,2,2,7] 是数组 [0,3,1,6,2,2,7] 的子数组。

本节和之前的分析思路一样还是考虑好1. 状态表示,2.状态转移方程,3.初始化,4.填表顺序,5.返回值。希望友友们看完本章后,自己理解一下子数组问题和子序列问题的差别。

例题1:最长递增子序列

链接:最长递增子序列

题目简介:

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

解法(动态规划):

 1. 状态表示:

这里和子数组的表示方法倒是差不多。

dp[i] 表示:以i 位置元素为结尾的所有⼦序列中,最长递增子序列的长度。

 2.状态转移方程:

推状态转移方程时可以画图帮助我们理解,下面这些情况可以大致分为两种,一种就只有一个i还有一种i会跟在i - 1,2,3的某一个后面(子序列)。

对于dp[i] ,我们可以根据子序列的构成⽅式,进⾏分类讨论:

(1)子序列长度为1 :只能自己玩了,此时dp[i] = 1 。

(2)子序列长度大于1 : nums[i] 可以跟在前面任何⼀个数后面形成子序列。 设前面的某⼀个数的下标为j ,其中0 。只要nums[j] < nums[i] , i 位置元素跟在j 元素后⾯就可以形成递增序列,长度为dp[j] + 1 。因此,我们仅需找到满足要求的最大的dp[j] + 1 即可。

综上, dp[i] = max(dp[j] + 1, dp[i]) ,其中0 <= j <= i - 1 && nums[j] < nums[i]。

 3.初始化:

在求长度之类的dp问题一般可以直接把dp表都初始化成1,因为在我们的状态表示中长度至少为1.因此可以将dp 表内所有元素初始化为1 。

 4.填表顺序:

从左往右

 5.返回值: 

由于不知道最长递增子序列以谁结尾,因此返回dp 表里面的最大值。

代码如下:

class Solution {
    public int lengthOfLIS(int[] nums) {
        //1.创建 dp 表
        //2.初始化
        //3.填表
        //4.返回值
        int n = nums.length;
        int[] dp = new int[n];
        for(int i = 0;i < n;i++){
            dp[i] = 1;
        }
        int max = dp[0];
        for(int i = 1;i < n;i++){
            for(int j = i - 1;j >= 0;j--){
                if(nums[i] > nums[j]){
                    dp[i] = Math.max(dp[i],dp[j] + 1);
                }
            }
            max = Math.max(max,dp[i]);
        }
        return max;
    }
}

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

空间复杂度:O(n)

接下来几题会用到动态规划 + 哈希表

例题2:最长定差子序列

链接:最长定差子序列

题目简介:

给你一个整数数组 arr 和一个整数 difference,请你找出并返回 arr 中最长等差子序列的长度,该子序列中相邻元素之间的差等于 difference 。

子序列 是指在不改变其余元素顺序的情况下,通过删除一些元素或不删除任何元素而从 arr 派生出来的序列。

 解法(动态规划):

这道题和最长递增子序列有⼀些相似,但仔细读题就会发现,本题的arr.lenght⾼达10^5 ,使⽤O(N^2) 的lcs 模型⼀定会超时。那么,它有什么信息是不同于最长递增子序列呢?是定差。之前,我们只知道要递增,不知道前⼀个数应当是多少;现在我们可以计算出前⼀个数是多少了,就可以⽤数值来定义dp 数组的值,并形成状态转移。这样,就把已有信息有效地利用了起来。

 1. 状态表示:

dp[i] 表示:以i 位置的元素为结尾所有的子序列中,最长的等差子序列的长度。

 2.状态转移方程:

对于dp[i] ,上⼀个定差⼦序列的取值定为arr[i] - difference 。只要找到以上⼀个数字为结尾的定差⼦序列⻓度的dp[arr[i] - difference] ,然后加上1 ,就是以i为结尾的定差⼦序列的⻓度。

这里要考虑一个问题:如果在i前面有多个等于arr[i] - difference的dp值要取哪一个呢?

 其实取最后一个即可,因为最后一个肯定大于等于前面几个的长度。

因此,这⾥可以选择使⽤哈希表做优化。我们可以把【元素, dp[j]】绑定,放进哈希表(会覆盖)中。甚⾄不⽤创建dp 数组,直接在哈希表中做动态规划。

 3.初始化:

刚开始的时候,需要把第⼀个元素放进哈希表中, hash[arr[0]] = 1。

 4.填表顺序:

从左往右

 5.返回值:

返回整个dp 表中的最⼤值

代码如下:

这里之所以不用 hash[arr[0]] = 1,是因为在下面put的写法中已经包含了。

class Solution {
    public int longestSubsequence(int[] arr, int difference) {
        //在哈希表里面做动态规划
        Map<Integer,Integer> map = new HashMap<>();
        int ret = 1;
        for(int x:arr){
            map.put(x,map.getOrDefault(x - difference,0) + 1);
            ret = Math.max(ret,map.get(x));
        }
        return ret;
    }
}

 时间复杂度:O(n) 

 空间复杂度:O(n)

例题3:最长的斐波那契子序列的长度

链接:最长的斐波那契子序列的长度

题目简介:

如果序列 X_1, X_2, ..., X_n 满足下列条件,就说它是 斐波那契式 的:

  • n >= 3
  • 对于所有 i + 2 <= n,都有 X_i + X_{i+1} = X_{i+2}

给定一个严格递增的正整数数组形成序列 arr ,找到 arr 中最长的斐波那契式的子序列的长度。如果一个不存在,返回  0 。

(回想一下,子序列是从原序列 arr 中派生出来的,它从 arr 中删掉任意数量的元素(也可以不删),而不改变其余元素的顺序。例如, [3, 5, 8] 是 [3, 4, 5, 6, 7, 8] 的一个子序列)

 解法(动态规划):

 1. 状态表示:

 2.状态转移方程:

设nums[i] = b, nums[j] = c ,那么这个序列的前⼀个元素就是a = c - b 。我们根据a 的情况讨论:

(1)a 存在,下标为k ,并且a < b :此时我们需要以k 位置以及i 位置元素为结尾的最⻓斐波那契⼦序列的⻓度,然后再加上j位置的元素即可。于是dp[i][j] = dp[k][i] + 1。

(2)a 存在,但是b < a < c :此时只能两个元素自己玩了, dp[i][j] = 2 。

(3)a 不存在:此时依旧只能两个元素自己玩了, dp[i][j] = 2 。

优化点:我们发现,在状态转移⽅程中,我们需要确定a 元素的下标。因此我们可以在dp 之前,将所有的元素+下标绑定在⼀起,放到哈希表中。

 3.初始化:

可以将表⾥⾯的值都初始化为2

 4.填表顺序:

先固定最后⼀个数,然后枚举倒数第二个数。由于j > i 故表如下:

 5.返回值:

因此返回dp 表中的最大值但是最大值可能小于3 ,小于3的话说明不存在。因此需要判断⼀下。

具体代码如下:

class Solution {
    public int lenLongestFibSubseq(int[] arr) {
        //1.创建 dp 表
        //2.初始化
        //3.填表
        //4.返回值
        Map<Integer,Integer> map = new HashMap<>();
        int n = arr.length;
        for(int i = 0;i < n;i++){
            map.put(arr[i],i);
        }
        int[][] dp = new int[n][n];
        for(int i = 0;i < n;i++){
            for(int j = 0;j < n;j++){
                dp[i][j] = 2;
            }
        }
        int ret = 2;
        for(int j = 2;j < n;j++){
            for(int i = 1;i < j;i++){
                int a = arr[j] - arr[i];
                if(a < arr[i] && map.containsKey(a)){
                    dp[i][j] = dp[map.get(a)][i] + 1;
                }
                ret = Math.max(ret,dp[i][j]);
            }
        }
        return ret < 3 ? 0 : ret;
    }
}

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

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

例题4:最长等差数列

链接:最长等差数列

题目简介:

给你一个整数数组 nums,返回 nums 中最长等差子序列的长度

回想一下,nums 的子序列是一个列表 nums[i1], nums[i2], ..., nums[ik] ,且 0 <= i1 < i2 < ... < ik <= nums.length - 1。并且如果 seq[i+1] - seq[i]0 <= i < seq.length - 1) 的值都相同,那么序列 seq 是等差的。

 解法(动态规划):

 1. 状态表示:

和上一题一样,一维的dp表不能解决问题,dp[i][j] 表示:以i 位置以及j位置的元素为结尾的所有的子序列中,最长的等差序列的长度。规定⼀下i < j 。

 2.状态转移方程:

设nums[i] = b, nums[j] = c ,那么这个序列的前⼀个元素就是a = 2 * b - c 。我们根据a的情况讨论:这里和例题3的分析差不多就直接给图了。

优化点:我们发现,在状态转移⽅程中,我们需要确定a 元素的下标。因此我们可以将所有的元素+ 下标绑定在⼀起,放到哈希表中,这里有两种策略:

(1)在dp 之前,放⼊哈希表中。这是可以的,但是需要将下标形成⼀个数组放进哈希表中。这样 时间复杂度较高,我帮⼤家试过了,超时。

(2)⼀边dp ,⼀边保存。这种方式,我们仅需保存最近的元素的下标,不用保存下标数组。但是 ⽤这种⽅法的话,我们在遍历顺序那里,先固定倒数第⼆个数(i),再遍历倒数第⼀个数(j)。这样就可以在i 使用完时候,将nums[i] 扔到哈希表中。✅

 3.初始化:

将所有位置初始化为2。

 4.填表顺序:

因为这里要保证去相同a下标k的最大值。

下图为固定倒数第一个数(j),枚举倒数第二个数。这样就不能保证跟新dp表时用到的a为在i前面的。红色部分为a可能出现的地方。

所以我们采用先固定倒数第⼆个数,然后枚举倒数第⼀个数如下图,这样a就只能在i的前面。

 5.返回值:

返回dp 表中的最大值

代码如下:

class Solution {
    public int longestArithSeqLength(int[] nums) {
        //1.创建 dp 表
        //2.初始化
        //3.填表
        //4.返回值
        Map<Integer,Integer> map = new HashMap<>();
        int n = nums.length;
        map.put(nums[0],0);
        int[][] dp = new int[n][n];
        for(int i = 0;i < n;i++){
            Arrays.fill(dp[i],2);
        }
        int ret = 2;
        for(int i = 1;i < n;i++){
            for(int j = i + 1;j < n;j++){
                int a = 2 * nums[i] - nums[j];
                if(map.containsKey(a)){
                    dp[i][j] = dp[map.get(a)][i] + 1;
                    ret = Math.max(ret,dp[i][j]);
                }
            }
            map.put(nums[i],i);
        }
        return ret;

    }
}

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

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

例题5:等差数列划分II-子序列

链接:等差数列划分II-子序列

题目简介:

给你一个整数数组 nums ,返回 nums 中所有 等差子序列 的数目。

如果一个序列中 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该序列为等差序列。

  • 例如,[1, 3, 5, 7, 9][7, 7, 7, 7] 和 [3, -1, -5, -9] 都是等差序列。
  • 再例如,[1, 1, 2, 5, 7] 不是等差序列。

数组中的子序列是从数组中删除一些元素(也可能不删除)得到的一个序列。

  • 例如,[2,5,10] 是 [1,2,1,2,4,1,5,10] 的一个子序列。

题目数据保证答案是一个 32-bit 整数。

  解法(动态规划):

 1. 状态表示:

dp[i][j] 表⽰:以i 位置以及j 位置的元素为结尾的所有的⼦序列中,等差子序列的个数。规定⼀下i < j 。这一类问题基本都这样。

 2.状态转移方程:

设nums[i] = b, nums[j] = c ,那么这个序列的前⼀个元素就是a = 2 * b - c 。我们根据a的情况讨论:(还是这张图非常重要)

(1)a 存在,下标为k ,并且a < b :此时我们知道以k 元素以及i 元素结尾的等差序列的数dp[k][i] ,在这些⼦序列的后⾯加上j 位置的元素依旧是等差序列。但是这⾥会多出来⼀个以k, i, j 位置的元素组成的新的等差序列,因此dp[i][j] = dp[k][i] + 1。

(2)因为a 可能有很多个,我们需要全部累加起来。

综上, dp[i][j] += dp[k][i] + 1 。

优化点:我们发现,在状态转移⽅程中,我们需要确定a 元素的下标。因此我们可以在dp之前,将【所有元素+下标数组】绑定在⼀起,放到哈希表中。这⾥为何要保存下标数组,是因为我们要统计个数,所有的下标都需要统计,之前是覆盖。

 3.初始化:

初始化dp 表为0。

 4.填表顺序:

先固定倒数第⼀个数,然后枚举倒数第⼆个数(这里就不能先固定倒数第二个数,因为要的是各个情况的和而不是最大值)。

 5.返回值:

我们要统计所有的等差子序列,因此返回dp 表中所有元素的和。

代码如下:

这里特别说明一个,题目给出的数都是32位以内的但是相加减可能会越界(😭),有些例子越界后可能会正好形成等差数列从而报错(我替你们试过了😭😭😭),故要设置成long类型。

class Solution {
    public int numberOfArithmeticSlices(int[] nums) {
        //1.创建 dp 表
        //2.初始化
        //3.填表
        //4.返回值
        Map<Long,List<Integer>> map = new HashMap<>();
        int n = nums.length;
        int[][] dp = new int[n][n];
        for(int i = 0;i < n;i++){
            long cmp = (long)nums[i];
            if(!map.containsKey(cmp)){
                map.put(cmp,new ArrayList<Integer>());
            }
            map.get(cmp).add(i);
        }
        int sum = 0;
        for(int j = 2;j < n;j++){
            for(int i = 1;i < j;i++){
                long a = 2L * nums[i] - nums[j];
                if(map.containsKey(a)){
                    for(int k : map.get(a)){
                        if(k < i){
                            dp[i][j] += dp[k][i] + 1;
                        }
                    }
                }
                sum += dp[i][j];
            }
        }
        return sum;

    }
}

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

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

结语:

其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。

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

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

相关文章

pc端vue2项目使用uniapp组件

项目示例下载 运行实例&#xff1a; 这是我在pc端做移动端底代码时的需求&#xff0c;只能在vue2使用&#xff0c;vue3暂时不知道怎么兼容。 安装依赖包时可能会报&#xff1a;npm install Failed to set up Chromium r756035! Set “PUPPETEER_SKIP_DOWNLOAD” env variable …

伪分布式Spark集群搭建

一、软件环境 软 件 版 本 安 装 包 VMware虚拟机 16 VMware-workstation-full-16.2.2-19200509.exe SSH连接工具 FinalShell Linux OS CentOS7.5 CentOS-7.5-x86_64-DVD-1804.iso JDK 1.8 jdk-8u161-linux-x64.tar.gz Spark 3.2.1 spark-3.2.1-bin-…

JVM的整体架构

JVM的整体架构 JVM的架构模型 基本上是基于栈的指令集架构 基于栈式架构的特点 设计和实现更简单&#xff0c;适用于资源受限的系统避开了寄存器的分配难题&#xff1a;使用零地址指令方式分配指令流中的指令大部分是零地址指令&#xff0c;其执行过程依赖于操作栈。指令集更…

AI写作一键生成原创文案,效率高!

AI写作一键生成原创文案&#xff0c;效率高&#xff01;当下时代&#xff0c;文案写作对于各个企业推广产品和服务显得隔外重要。优秀的文案能够吸引用户的注意力&#xff0c;激发购买欲望&#xff0c;从而为企业带来更多的销售机会。然而&#xff0c;对于许多人来说&#xff0…

【Python】新手入门学习:什么是工作目录?

【Python】新手入门学习&#xff1a;什么是工作目录&#xff1f; &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希望得…

【Python机器学习系列】自助法计算机器学习评价指标的置信区间(案例+源码)

这是我的第235篇原创文章。 一、引言 Bootstrap方法是非常有用的一种统计学上的估计方法&#xff0c;是一类非参数Monte Carlo方法&#xff0c;其实质是对观测信息进行再抽样&#xff0c;进而对总体的分布特性进行统计推断。 自助法计算分类模型的AUC、准确率、特异度和灵敏…

商业楼宇一卡通解决方案(1)

智能楼宇发展 智能楼宇也称智能建筑,又称智能大厦。智能楼宇是将建筑技术、通信技术、计算机技术和控制技术等各方面的先进科学技术相互融合、合理集成为最优化的整体,具有工程投资合理、设备高度自动化、信息管理科学、服务高效优质、使用灵活方便和环境安全舒适等特点,是…

Caffeine--实现进程缓存

本地进程缓存特点 缓存在日常开发中起着至关重要的作用, 由于存储在内存中, 数据的读取速度非常快,能大量减少对数据库的访问,减少数据库的压力. 缓存分为两类: 分布式缓存, 例如Redis: 优点: 存储容量大, 可靠性更好, 可以在集群间共享缺点: 访问缓存存在网络开销场景: 缓存数…

Python网站的搭建和html基础

1.Python网站代码及讲解 一般我们搭建小型的网站就用flask库就行了。 &#xff08;1&#xff09;安装flask库 安装完python后&#xff0c;按住windows徽标键和r,弹出“运行”&#xff0c;在里面输入cmd。 回车打开&#xff0c;输入“pip install flask”。 &#xff08;2&am…

4. C++ 类的大小

C 类的大小 ​ C类的大小&#xff0c;是一个比较经典的问题&#xff0c;学过C后&#xff0c;应该对类大小有清晰的认识&#xff0c;长话短说&#xff0c;本文精简凝练&#xff0c;我们进入正题&#xff01;&#xff01;&#xff01; 1.类的大小与什么有关系&#xff1f; 与类…

C#,数值计算,解微分方程的龙格-库塔四阶方法与源代码

Carl Runge Martin Wilhelm Kutta 1 龙格-库塔四阶方法 数值分析中,龙格-库塔法(Runge-Kutta)是用于模拟常微分方程的解的重要的一类隐式或显式迭代法。这些技术由数学家卡尔龙格和马丁威尔海姆库塔于1900年左右发明。 对于一阶精度的欧拉公式有: yi+1=yi+h*K1  K1=f(…

Portraiture2024中文版广泛应用于人像处理的磨皮美化插件

Portraiture插件是一款广泛应用于人像处理的磨皮美化插件&#xff0c;尤其在Photoshop和Lightroom等图像编辑软件中备受欢迎。这款插件能够帮助用户快速实现智能磨皮效果&#xff0c;使皮肤看起来更加平滑细腻&#xff0c;同时保留自然纹理和其他重要细节。 Portraiture for Ph…

基于JAVA实现五子棋游戏设计【附项目源码】分享

基于JAVA实现五子棋游戏设计&#xff1a; 项目源码地址&#xff1a;https://download.csdn.net/download/weixin_43894652/88842612 一、引言 五子棋&#xff0c;又称连珠、连五、五目、五目棋等&#xff0c;是一种传统的棋类游戏。本需求文档旨在详细阐述一个基于Java环境开…

LIGHTHOUSE Apex RBP应用案例|汽车涂装行业 电动汽车电池制造行业的颗粒物监测首选

Lighthouse ApexBP汽车制造中的颗粒物监测技术无疑是汽车制造领域的一项革命性发展。它不仅提供了全面、高精度的颗粒检测&#xff0c;而且能够轻松集成到现有的制造流程中&#xff0c;满足自动化需求&#xff0c;加强质量控制&#xff0c;确保电动汽车电池生产的安全性和效率。…

展览厅设计如何创新而独特

一、独特的建筑外观 展览厅的建筑外观是展览的第一印象&#xff0c;因此需要设计一个独特而有吸引力的外观。可以使用独特的建筑形态、创新的材料和结构&#xff0c;以及艺术化的立面设计。 二、灵活的展示空间 创新的展览厅设计应具备灵活的展示空间&#xff0c;以适应不同类型…

java中几种对象存储(文件存储)中间件的介绍

一、前言 在博主得到系统中使用的对象存储主要有OSS&#xff08;阿里云的对象存储&#xff09; COS&#xff08;腾讯云的对象存储&#xff09;OBS&#xff08;华为云的对象存储&#xff09;还有就是MinIO 这些玩意。其实这种东西大差不差&#xff0c;几乎实现方式都是一样&…

StringBuilder --java学习笔记

StringBuilder 代表可变字符串对象&#xff0c;相当于是一个容器&#xff0c;它里面装的字符串是可以改变的&#xff0c;就是用来操作字符串的StringBuilder比String更适合做字符串的修改操作&#xff0c;效率会更高&#xff0c;代码也会更简洁 StringBuilder的常用构造器和方…

单目标/多目标樽海鞘群优化算法——源码

目录 一、樽海鞘群优化算法&#xff1a; 二、多目标樽海鞘群优化算法&#xff1a; 三、代码运行结果&#xff1a; 四、代码下载&#xff1a; 一、樽海鞘群优化算法&#xff1a; 澳大利亚学者Seyedali Mirjalili等人于2017年提出了樽海鞘群算法&#xff0c;该算法源于对海底…

【C++】string类(介绍、常用接口)

&#x1f308;个人主页&#xff1a;秦jh__https://blog.csdn.net/qinjh_?spm1010.2135.3001.5343&#x1f525; 系列专栏&#xff1a;http://t.csdnimg.cn/eCa5z 目录 string类的常用接口说明 string类对象的常见构造 ​编辑 string字符串的遍历&#xff08;迭代器&#xf…

攻防演练|某车企攻防小记

前言 专注于web漏洞挖掘、内网渗透、免杀和代码审计&#xff0c;感谢各位师傅的关注&#xff01;网安之路漫长&#xff0c;与君共勉&#xff01; 实习期间针对某车企开展的一次攻防演练&#xff0c;过程很曲折&#xff0c;当时的记录没有了只是简单的总结一下。 攻击路径 收…