当前位置: 首页 > news >正文

【动态规划 | 子序列问题】子序列问题的最优解:动态规划方法详解

在这里插入图片描述

算法相关知识点可以通过点击以下链接进行学习一起加油!
斐波那契数列模型路径问题 多状态问题子数组

动态规划是解决子序列问题的利器。面对最长公共子序列、最长递增子序列等经典问题时,掌握状态定义、转移方程和边界处理三大核心要素,就能快速找到最优解。本文将用最简洁的方式,带你掌握动态规划解决子序列问题的精髓,提升算法解题能力。

请添加图片描述

Alt

🌈个人主页:是店小二呀
🌈C/C++专栏:C语言\ C++
🌈初/高阶数据结构专栏: 初阶数据结构\ 高阶数据结构
🌈Linux专栏: Linux
🌈算法专栏:算法
🌈Mysql专栏:Mysql

🌈你可知:无人扶我青云志 我自踏雪至山巅 请添加图片描述

文章目录

    • 子序列概念
    • 300. 最长递增子序列(重要)
    • 376. 摆动序列
    • 673. 最长递增子序列的个数
    • 646. 最长数对链
    • 1218. 最长定差子序列
    • 873. 最长的斐波那契子序列的长度
    • 1027. 最长等差数列
    • 446. 等差数列划分 II - 子序列

子序列概念

子序列是一个从原始序列中删除某些元素(可以不删除元素,也可以删除一些元素)后,剩余元素保持原有顺序的序列。换句话说,子序列是从原始序列中选择若干个元素,保持它们的相对顺序。

300. 最长递增子序列(重要)

题目】:300. 最长递增子序列

在这里插入图片描述

算法思路

在这里插入图片描述

由于这道题是“子序列”问题,若要找到以第 i 个位置为结尾的所有子序列,我们需要在区间 [0, i - 1] 内查找符合条件的子序列。因为要求是最长子序列,因此我们可以使用 max 函数不断比较,确保只选择最长的子序列。

需要注意的是,nums[j] < nums[i] 是前提条件,确保在 i 位置之前的子序列是递增的。以后类似的子序列问题一般采用这种思路,绘图在理解过程中非常重要。

代码实现

class Solution {
public:int lengthOfLIS(vector<int>& nums) {int n = nums.size();vector<int> dp(n, 1);for(int i = 1; i < n; i++)for(int j = 0; j <= i; j++)if(nums[j] < nums[i]) dp[i] = max(dp[j] + 1, dp[i]);int ret = 0;for(auto x : dp) ret = max(ret, x);return ret; }
};

376. 摆动序列

题目】:376. 摆动序列
在这里插入图片描述

算法思路

在这里插入图片描述

这道题的算法思路与“最长湍流子数组”相似,但由于是处理“子序列”问题,我们需要在区间 [0, i - 1] 内查找所有符合条件的子序列,并通过 j 变量更新结果。而对于子数组问题,直接处理即可。

代码实现

class Solution {
public:int wiggleMaxLength(vector<int>& nums) {//1.创建dp表int n = nums.size();vector<int> f(n, 1);auto g = f;//2.填表int ret = 1;//更新最新的结果for(int i = 1; i < n; i++){for(int j = 0; j < i; j++){if(nums[j] < nums[i]) f[i] = max(g[j] + 1, f[i]);else if(nums[j] > nums[i]) g[i] = max(f[j] + 1, g[i]);}ret = max(ret, max(f[i], g[i]));}return ret;}
};

673. 最长递增子序列的个数

题目】:673. 最长递增子序列的个数

在这里插入图片描述

算法思路

【小demo】:在数组中找出最大值出现的次数

在这里插入图片描述

通过这个小技巧,在以后需要找出最大值、最小值或最长/最短值的同时,还能统计其出现次数。这个小 demo 可以帮助实现这一需求

在这里插入图片描述

根据’经验 + 题目要求’,我们可以得到一个简单的状态表示。然而,这不足以直接满足’最长值与出现次数’的需求,因此需要使用两个状态表示。结合我们的小 demo,可以得出相应的状态转移方程。

同时,我们需要定义变量来记录长度和次数,供返回值使用。因为这些信息是必须的:当 relen == len[i] 时,recount++;当小于时,则需要重新更新结果并进行记录

代码实现

class Solution {
public:int findNumberOfLIS(vector<int>& nums) {int n = nums.size();//1.创建dp表vector<int> len(n, 1);auto count = len;int relen = 1, recount = 1;//2.填表操作for(int i = 1; i < n; i++){for(int j = 0; j < i; j++){if(nums[j] < nums[i]){if(len[j] + 1 > len[i]){len[i] = len[j] + 1;count[i] = count[j];}else if(len[j] + 1 == len[i]){count[i] += count[j];}}}if(relen == len[i]){recount += count[i];}else if(relen < len[i]){relen = len[i];recount = count[i];}}//3.返回值操作return recount;}
};

646. 最长数对链

题目】:646. 最长数对链

在这里插入图片描述

算法思路

在这里插入图片描述

首先,对题目进行分析,并通过绘图将题目提供的信息转化为数字和图形,以便更清晰地理解和处理问题。

细节问题

"无论是子数组问题还是之前的子序列问题,通常以 i 位置为结尾的所有子数组或字符串都会保证倒数第二个元素出现在 i 位置之前。而这道题却要求倒数第二个元素出现在 i 位置右侧。为了解决这个问题,我们需要根据第一个位置进行排序。

排序的依据是根据题目要求和数学分析得出的,同时我们将题目转化为一个类似于子序列的问题,按照已有的思路进行解决。关键是要理解子序列的含义,并找出如何将这道题目转化为一个子序列相关问题

代码实现

class Solution {
public:int findLongestChain(vector<vector<int>>& pairs) {//1.预处理:按照第一个元素排序sort(pairs.begin(), pairs.end());//2.创建dp表int n = pairs.size();vector<int> dp(n, 1);//3.填表int ret = 1;for(int i = 1; i < n; i++){for(int j = 0; j < i; j++){if(pairs[j][1] < pairs[i][0]){dp[i] = max(dp[j] + 1, dp[i]);ret = max(ret, dp[i]);}}}//4.返回值return ret;}
};

1218. 最长定差子序列

题目】:1218. 最长定差子序列

在这里插入图片描述

算法思路

image-20250319203904659

根据 i 位置的情况,我们结合数学分析得出状态转移方程。需要注意的是,题目并未限定子序列必须是严格递增的,因此可能会出现重复元素。

在这种情况下,我们可以通过不同的前一个元素来更新 dp[nums[i]]。然而,最终我们关心的是最长子序列的长度,因此重复元素不会导致错误,最终的结果会自动选取最大的长度。

为了优化空间复杂度,我们可以选择使用哈希表来存储动态规划状态。通过将“元素”和对应的最长子序列长度(dp[j])绑定并存入哈希表中,我们甚至可以省去 dp 数组的空间,直接在哈希表中进行动态规划。

代码实现

class Solution {
public:int longestSubsequence(vector<int>& arr, int difference) {unordered_map<int, int> hash;int n = arr.size();int ret = 1; hash[arr[0]] = 1;for(int i = 1; i < n; i++){hash[arr[i]] = hash[arr[i] - difference] + 1;ret = max(ret, hash[arr[i]]);}return ret;}
};

873. 最长的斐波那契子序列的长度

题目】:873. 最长的斐波那契子序列的长度

在这里插入图片描述

算法思路

在这里插入图片描述

根据斐波那契数的性质和题目要求,不能仅凭一个位置的元素来推出斐波那契数列的其他元素,需要借助两个元素来推算出下一个元素,从而得到最长的斐波那契子序列。

通过分析斐波那契数列的性质以及最后一个位置的元素,我们得出了状态转移方程。为了简化计算,当无法构成斐波那契数列时,我们默认长度为2,最终在返回值时进行判断。

此外,我们通过固定序列的最后一个元素,再依次固定倒数第二个元素,来简化遍历过程,便于高效计算。

代码实现

class Solution {
public:int lenLongestFibSubseq(vector<int>& arr) {int n = arr.size();vector<vector<int>> dp(n, vector<int>(n, 2));unordered_map<int, int> hash;for(int i = 0; i < n; i++) hash[arr[i]] = i;int ret = 2;for(int j = 2; j < n; j++) //固定最后一个数{for(int i = 1; i < j; i++){int x = arr[j] - arr[i];if(hash.count(x) && x < arr[i]) dp[i][j] = dp[hash[x]][i] + 1;ret = max(ret, dp[i][j]);}}return ret < 3 ? 0 : ret;}
};

1027. 最长等差数列

题目】:1027. 最长等差数列

在这里插入图片描述

算法思路

在这里插入图片描述

这道题同"873. 最长的斐波那契子序列的长度"很相似,无非是从"斐波那契"换成了"等差数列",同时题目没有说明"严格递增",意味着可能存在重复元素。

这里通过"上道题经验 + 题目分析",需要两个元素去确定我们的状态。根据最后一个位置进行分析,得到我们的状态转移方程。

优化方案

在这里插入图片描述

针对重复元素的下标处理,我们采用了一种策略:一遍遍历动态规划,一遍记录每个元素离它最近的下标。对于填表顺序,有两种选择。第一种是先固定最后一个元素,再枚举倒数第二个元素,这种方法可能会导致重复记录元素下标。相比之下,第二种方法,即先固定倒数第二个元素,再枚举最后一个元素,更符合我们保存最近元素下标的需求。

由于采用‘先固定倒数第二个元素,枚举最后一个元素’的策略,hash.count(x) 已经保证了 xnums[i] 之前。因此,不需要额外的顺序判断。如果选用其他方法,就需要加上顺序判断,以防止计算出的 x 导致逆序。

代码实现

class Solution {
public:int longestArithSeqLength(vector<int>& nums) {int n = nums.size();vector<vector<int>> dp(n, vector<int>(n, 2));unordered_map<int, int> hash;hash[nums[0]] = 0;int ret = 2;for(int i = 1; i < n; i++){for(int j = i + 1; j < n; j++){int x = 2 * nums[i] - nums[j];if(hash.count(x))dp[i][j] = dp[hash[x]][i] + 1;ret = max(ret, dp[i][j]);}hash[nums[i]] = i;}return ret;}
};

446. 等差数列划分 II - 子序列

题目】:446. 等差数列划分 II - 子序列

在这里插入图片描述

算法思路

在这里插入图片描述

根据前两道题的经验和题目分析,许多状态表示的产生是由于需要两个位置的元素来确定另一个元素,从而推导出状态转移方程。

题目中未明确提到“严格递增”,因此需要考虑元素重复的情况。通过绘图和数学分析,得到关键的状态信息,并分析最后一个位置的状态。最终得出结论:只有当 a 存在且 Kx < i 时,才会发生状态转移。

优化方案

在这里插入图片描述

这样就能将相同元素考虑进去了。当遇到 index >= i 的情况时,说明 x 对应的元素已经不符合构成等差数列的条件(因为 nums[i] 应该是递增的)。因此,我们可以通过 else break; 提前退出内层循环,避免进行不必要的计算。

代码实现

class Solution {
public:int numberOfArithmeticSlices(vector<int>& nums) {int n = nums.size();vector<vector<int>> dp(n, vector<int>(n));unordered_map<long long, vector<int>> hash;for(int i = 0; i < n; i++) hash[nums[i]].push_back(i);int sum = 0;for(int j = 2; j < n; j++){for(int i = 1; i < j; i++){long long x = (long long)2 * nums[i] - nums[j];if(hash.count(x)){for(auto index: hash[x]){if(index < i)dp[i][j] += dp[index][i] + 1;else break;}}sum += dp[i][j];}}return sum;}
};

在这里插入图片描述
快和小二一起踏上精彩的算法之旅!关注我,我们将一起破解算法奥秘,探索更多实用且有趣的知识,开启属于你的编程冒险!

http://www.dtcms.com/a/312050.html

相关文章:

  • RK628F HDMI-IN调试:应用接口使用
  • Vulnhub ELECTRICAL靶机复现(附提权)
  • QPainter::CompositionMode解析
  • junit总@mockbaen与@mock的区别与联系
  • flutter分享到支付宝
  • Linux进程控制核心:创建·等待·终止·替换
  • Qt 信号和槽正常连接返回true,但发送信号后槽函数无响应问题【已解决】
  • 深入解析Java Stream Sink接口
  • Design Compiler:Milkyway库的创建与使用
  • 1-7〔 OSCP ◈ 研记 〕❘ 信息收集▸主动采集E:SMB基础
  • 硬件-可靠性学习DAY1——系统可靠性设计指南:从原理到实践
  • Markdown 中的图表 Mermaid 与 classDiagram
  • Thread 中的 run() 方法 和 start() 方法的
  • 笔记:C语言中指向指针的指针作用
  • MQTT协议测试环境部署
  • 错误: 找不到或无法加载主类 原因: java.lang.ClassNotFoundException
  • (nice!!!)(LeetCode 每日一题) 2561. 重排水果 (哈希表 + 贪心)
  • UNet改进(29):记忆增强注意力机制在UNet中的创新应用-原理、实现与性能提升
  • 【嵌入式汇编基础】-ARM架构基础(三)
  • 动态规划解最长回文子串:深入解析与优化问题
  • 【redis】基于工业界技术分享的内容总结
  • JS的作用域
  • 第15届蓝桥杯Python青少组中/高级组选拔赛(STEMA)2024年1月28日真题
  • sqli-labs:Less-20关卡详细解析
  • MFC 实现托盘图标菜单图标功能
  • 中州养老Day02:服务管理护理计划模块
  • 中之人模式下的虚拟主持人:动捕设备与面捕技术的协同驱动
  • 2025系规教材改革后,论文怎么写?
  • 错误处理_IncompatibleKeys
  • 在Linux上对固态硬盘进行分区、格式化和挂载的步骤