算法与数据结构(二十三)动态规划设计:最长递增子序列

注:此文只在个人总结 labuladong 动态规划框架,仅限于学习交流,版权归原作者所有;

也许有读者看了前文 动态规划详解,学会了动态规划的套路:找到了问题的「状态」,明确了 dp 数组/函数的含义,定义了 base case;但是不知道如何确定「选择」,也就是找不到状态转移的关系,依然写不出动态规划解法,怎么办?

不要担心,动态规划的难点本来就在于寻找正确的状态转移方程,本文就借助经典的「最长递增子序列问题」来讲一讲设计动态规划的通用技巧:数学归纳思想

最长递增子序列(Longest Increasing Subsequence,简写 LIS)是非常经典的一个算法问题,比较容易想到的是动态规划解法,时间复杂度 O(N^2),我们借这个问题来由浅入深讲解如何找状态转移方程,如何写出动态规划解法。比较难想到的是利用二分查找,时间复杂度是 O(NlogN),我们通过一种简单的纸牌游戏来辅助理解这种巧妙的解法。

力扣第 300 题「最长递增子序列open in new window」就是这个问题:

输入一个无序的整数数组,请你找到其中最长的严格递增子序列的长度,函数签名如下:

int lengthOfLIS(int[] nums);

比如说输入 nums=[10,9,2,5,3,7,101,18],其中最长的递增子序列是 [2,3,7,101],所以算法的输出应该是 4。

注意「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的。下面先来设计动态规划算法解决这个问题。

一、动态规划解法

动态规划的核心设计思想是数学归纳法。

相信大家对数学归纳法都不陌生,高中就学过,而且思路很简单。比如我们想证明一个数学结论,那么我们先假设这个结论在 k < n 时成立,然后根据这个假设,想办法推导证明出 k = n 的时候此结论也成立。如果能够证明出来,那么就说明这个结论对于 k 等于任何数都成立。

类似的,我们设计动态规划算法,不是需要一个 dp 数组吗?我们可以假设 dp[0...i-1] 都已经被算出来了,然后问自己:怎么通过这些结果算出 dp[i]

直接拿最长递增子序列这个问题举例你就明白了。不过,首先要定义清楚 dp 数组的含义,即 dp[i] 的值到底代表着什么?

我们的定义是这样的:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度

Info
为什么这样定义呢?这是解决子序列问题的一个套路,后文 动态规划之子序列问题解题模板 总结了几种常见套路。你读完本章所有的动态规划问题,就会发现 dp 数组的定义方法也就那几种。

根据这个定义,我们就可以推出 base case:dp[i] 初始值为 1,因为以 nums[i] 结尾的最长递增子序列起码要包含它自己。

举两个例子:
在这里插入图片描述

这个 GIF 展示了算法演进的过程:

在这里插入图片描述

根据这个定义,我们的最终结果(子序列的最大长度)应该是 dp 数组中的最大值。

int res = 0;
for (int i = 0; i < dp.length; i++) {res = Math.max(res, dp[i]);
}
return res;

读者也许会问,刚才的算法演进过程中每个 dp[i] 的结果是我们肉眼看出来的,我们应该怎么设计算法逻辑来正确计算每个 dp[i] 呢?

这就是动态规划的重头戏,如何设计算法逻辑进行状态转移,才能正确运行呢?这里需要使用数学归纳的思想:

假设我们已经知道了 dp[0..4] 的所有结果,我们如何通过这些已知结果推出 dp[5]

在这里插入图片描述

根据刚才我们对 dp 数组的定义,现在想求 dp[5] 的值,也就是想求以 nums[5] 为结尾的最长递增子序列。

nums[5] = 3,既然是递增子序列,我们只要找到前面那些结尾比 3 小的子序列,然后把 3 接到这些子序列末尾,就可以形成一个新的递增子序列,而且这个新的子序列长度加一

nums[5] 前面有哪些元素小于 nums[5]?这个好算,用 for 循环比较一波就能把这些元素找出来。

以这些元素为结尾的最长递增子序列的长度是多少?回顾一下我们对 dp 数组的定义,它记录的正是以每个元素为末尾的最长递增子序列的长度。

以我们举的例子来说,nums[0]nums[4] 都是小于 nums[5] 的,然后对比 dp[0]dp[4] 的值,我们让 nums[5] 和更长的递增子序列结合,得出 dp[5] = 3

在这里插入图片描述

for (int j = 0; j < i; j++) {if (nums[i] > nums[j]) {dp[i] = Math.max(dp[i], dp[j] + 1);}
}

i = 5 时,这段代码的逻辑就可以算出 dp[5]。其实到这里,这道算法题我们就基本做完了。

读者也许会问,我们刚才只是算了 dp[5] 呀,dp[4], dp[3] 这些怎么算呢?类似数学归纳法,你已经可以算出 dp[5] 了,其他的就都可以算出来:

for (int i = 0; i < nums.length; i++) {for (int j = 0; j < i; j++) {// 寻找 nums[0..j-1] 中比 nums[i] 小的元素if (nums[i] > nums[j]) {// 把 nums[i] 接在后面,即可形成长度为 dp[j] + 1,// 且以 nums[i] 为结尾的递增子序列dp[i] = Math.max(dp[i], dp[j] + 1);}}
}

结合我们刚才说的 base case,下面我们看一下完整代码:

int lengthOfLIS(int[] nums) {// 定义:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度int[] dp = new int[nums.length];// base case:dp 数组全都初始化为 1Arrays.fill(dp, 1);for (int i = 0; i < nums.length; i++) {for (int j = 0; j < i; j++) {if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1);}}int res = 0;for (int i = 0; i < dp.length; i++) {res = Math.max(res, dp[i]);}return res;
}

至此,这道题就解决了,时间复杂度 O(N^2)。总结一下如何找到动态规划的状态转移关系:

1、明确 dp 数组的定义。这一步对于任何动态规划问题都很重要,如果不得当或者不够清晰,会阻碍之后的步骤。

2、根据 dp 数组的定义,运用数学归纳法的思想,假设 dp[0...i-1] 都已知,想办法求出 dp[i],一旦这一步完成,整个题目基本就解决了。

但如果无法完成这一步,很可能就是 dp 数组的定义不够恰当,需要重新定义 dp 数组的含义;或者可能是 dp 数组存储的信息还不够,不足以推出下一步的答案,需要把 dp 数组扩大成二维数组甚至三维数组。

目前的解法是标准的动态规划,但对最长递增子序列问题来说,这个解法不是最优的,可能无法通过所有测试用例了,下面讲讲更高效的解法。

注:核心问题是 2 个:

  1. dp 数组的定义:最好能直接对应问题,如最长递增子序列中 dp 数组表示以 nums[i] 结尾的最长的递增最序列;
  2. dp 数组的推理:即知道 dp[0...i-1] 都已知,如何求出 dp[i]

其中 dp 数组如何定义很重要!

二、拓展到二维

我们看一个经常出现在生活中的有趣问题,力扣第 354 题「俄罗斯套娃信封问题open in new window」,先看下题目:

在这里插入图片描述

这道题目其实是最长递增子序列的一个变种,因为每次合法的嵌套是大的套小的,相当于在二维平面中找一个最长递增的子序列,其长度就是最多能嵌套的信封个数

前面说的标准 LIS 算法只能在一维数组中寻找最长子序列,而我们的信封是由 (w, h) 这样的二维数对形式表示的,如何把 LIS 算法运用过来呢?

在这里插入图片描述

读者也许会想,通过 w × h 计算面积,然后对面积进行标准的 LIS 算法。但是稍加思考就会发现这样不行,比如 1 × 10 大于 3 × 3,但是显然这样的两个信封是无法互相嵌套的。

这道题的解法比较巧妙:

先对宽度 w 进行升序排序,如果遇到 w 相同的情况,则按照高度 h 降序排序;之后把所有的 h 作为一个数组,在这个数组上计算 LIS 的长度就是答案

画个图理解一下,先对这些数对进行排序:

在这里插入图片描述

然后在 h 上寻找最长递增子序列,这个子序列就是最优的嵌套方案:

在这里插入图片描述

那么为什么这样就可以找到可以互相嵌套的信封序列呢?稍微思考一下就明白了:

首先,对宽度 w 从小到大排序,确保了 w 这个维度可以互相嵌套,所以我们只需要专注高度 h 这个维度能够互相嵌套即可。

其次,两个 w 相同的信封不能相互包含,所以对于宽度 w 相同的信封,对高度 h 进行降序排序,保证二维 LIS 中不存在多个 w 相同的信封(因为题目说了长宽相同也无法嵌套)。

下面看解法代码:

// envelopes = [[w, h], [w, h]...]
public int maxEnvelopes(int[][] envelopes) {int n = envelopes.length;// 按宽度升序排列,如果宽度一样,则按高度降序排列Arrays.sort(envelopes, new Comparator<int[]>() {public int compare(int[] a, int[] b) {return a[0] == b[0] ? b[1] - a[1] : a[0] - b[0];}});// 对高度数组寻找 LISint[] height = new int[n];for (int i = 0; i < n; i++)height[i] = envelopes[i][1];return lengthOfLIS(height);
}int lengthOfLIS(int[] nums) {// 见前文
}

为了清晰,我将代码分为了两个函数, 你也可以合并,这样可以节省下 height 数组的空间。

此处放上 Leetcode 官方答案,个人觉着解释的更清晰一些:

根据题目的要求, 如果我们选择了 k k k 个信封, 它们的的宽度依次为 w 0 , w 1 , ⋯ , w k − 1 w_0, w_1, \cdots, w_{k-1} w0,w1,,wk1, 高度依次为
h 0 , h 1 , ⋯ , h k − 1 h_0, h_1, \cdots, h_{k-1} h0,h1,,hk1, 那么需要满足: { w 0 < w 1 < ⋯ < w k − 1 h 0 < h 1 < ⋯ < h k − 1 \left\{\begin{array}{l} w_0<w_1<\cdots<w_{k-1} \\ h_0<h_1<\cdots<h_{k-1} \end{array}\right. {w0<w1<<wk1h0<h1<<hk1
同时控制 w w w h h h 两个维度并不是那么容易, 因此我们考虑固定一个维度, 再在另一个维度上进行选 择。例如, 我们固定 w w w
维度, 那么我们将数组 envelopes 中的所有信封按照 w w w 升序排序。这样一 来,
我们只要按照信封在数组中的出现顺序依次进行选取, 就一定保证满足: w 0 ≤ w 1 ≤ ⋯ ≤ w k − 1 w_0 \leq w_1 \leq \cdots \leq w_{k-1} w0w1wk1 了。然而小于等于 ≤ \leq 和小于 < < < 还是有区别的,但我们不妨首先考虑一个简化版本的问题:
如果我们保证所有信封的 w 值互不相同, 那么我们可以设计出一种得到答案的方法吗? 在 w w w 值互不相同的前提下,小于等于 ≤ \leq
和小于 < < < 是等价的,那么我们在排序后,就可以完全忽略 w w w 维度, 只需要考虑 h h h 维度了。此时, 我们需要解决的问题即为:
给定一个序列, 我们需要找到一个最长的子序列, 使得这个子序列中的元素严格单调递增, 即上面 要求的:
h 0 < h 1 < ⋯ < h k − 1 > h_0<h_1<\cdots<h_{k-1}> h0<h1<<hk1> 那么这个问题就是经典的「最长严格递增子序列」问题了, 读者可以参考力扣平台的 300 . 最长递增 子序列 及其 官方题解。最长严格递增子序列的详细解决方法属于解决本题的前置知识点, 不是本文分析的重点, 因此这里不再赘述,会在方法一和方法二中简单提及。 当我们解决了简化版本的问题之后, 我们来想一想使用上面的方法解决原问题, 会产生什么错误。当 w w w
值相同时,如果我们不规定 h h h 值的排序顺序,那么可能会有如下的情况: 排完序的结果为 [ ( w , h ) ] = [ ( 1 , 1 ) , ( 1 , 2 ) , ( 1 , 3 ) , ( 1 , 4 ) ] [(w,h)]=[(1,1),(1,2),(1,3),(1,4)] [(w,h)]=[(1,1),(1,2),(1,3),(1,4)], 由于这些信封的 w w w 值都相同, 不存在一个 信封可以装下另一个信封, 那么我们只能在其中选择 1 个信封。然而如果我们完全忽略 w w w 维度, 剩下的 h h h 维度为 [ 1 , 2 , 3 , 4 ] [1,2,3,4] [1,2,3,4],
这是一个严格递增的序列, 那么我们就可以选择所有的 4 个信封了, 这就产生了错误。 因此,我们必须要保证对于每一种 w w w 值,我们最多只能选择 1 个信封。 我们可以将 h h h 值作为排序的第二关键字进行降序排序, 这样一来, 对于每一种 w w w 值, 其对应的信封 在排序后的数组中是按照 h h h 值递减的顺序出现的, 那么这些 h h h 值不可能组成长度超过 1 的严格递增的序列,这就从根本上杜绝了错误的出现。 因此我们就可以得到解决本题需要的方法:

  • 首先我们将所有的信封按照 w w w 值第一关键字升序、 h h h 值第二关键字降序进行排序;
  • 随后我们就可以忽略 w w w 维度, 求出 h h h 维度的最长严格递增子序列, 其长度即为答案。 下面简单提及两种计算最长严格递增子序列的方法, 更详细的请参考上文提到的题目以及对应的官方 题解。

最后赋上个人题解:

    def maxEnvelopes(self, envelopes):""":type envelopes: List[List[int]]:rtype: int"""envelopes.sort(key = lambda x: (x[0], -x[1]))# heigth = [1] * len(envelopes)# for i in range(len(envelopes)):#     heigth[i] = envelopes[i][1]# return self.lcs(heigth)return self.lcsPro(envelopes)def lcs(self, heigth):if len(heigth) <= 1:return 1dp = [1] * len(heigth)for i in range(len(heigth)):for j in range(i):if heigth[i] > heigth[j]:dp[i] = max(dp[i], dp[j] + 1)return max(dp)def lcsPro(self, envelopes):if len(envelopes) <= 1:return 1dp = [1] * len(envelopes)for i in range(len(envelopes)):for j in range(i):if envelopes[i][1] > envelopes[j][1]:dp[i] = max(dp[i], dp[j] + 1)return max(dp) ```

三、参考文献

[1] 动态规划设计:最长递增子序列
[2] Leetcode 官方题解

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

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

相关文章

js简介以及在html中的2种使用方式(hello world)

简介 javascript &#xff1a;是一个跨平台的脚本语言&#xff1b;是一种轻量级的编程语言。 JavaScript 是 Web 的编程语言。所有现代的 HTML 页面都使用 JavaScript。 HTML&#xff1a; 结构 css&#xff1a; 表现 JS&#xff1a; 行为 HTMLCSS 只能称之为静态网页&#xff0…

github以及上传代码处理

最近在github上传代码的时候出现了&#xff1a; /video_parser# git push -u origin main Username for https://github.com: gtnyxxx Password for https://gtny2010github.com: remote: Support for password authentication was removed on August 13, 2021. remote: Plea…

ROS局部路径规划器插件teb_local_planner流程梳理(上)

在我之前的文章《ROS导航包Navigation中的 Movebase节点路径规划相关流程梳理》中已经介绍过Move_base节点调用局部路径规划器插件的接口函数是computeVelocityCommands&#xff0c;接下来&#xff0c;我们就从这个函数入手梳理一下teb_local_planner功能包的工作流程。 ☆注&a…

推荐一个绘图平台(可替代Visio)

不废话&#xff0c;简易记网址&#xff1a; draw.io 网站会重定向到&#xff1a;https://app.diagrams.net/

Databend 开源周报第 106 期

Databend 是一款现代云数仓。专为弹性和高效设计&#xff0c;为您的大规模分析需求保驾护航。自由且开源。即刻体验云服务&#xff1a;https://app.databend.cn 。 Whats On In Databend 探索 Databend 本周新进展&#xff0c;遇到更贴近你心意的 Databend 。 数据脱敏 Data…

云原生 AI 工程化实践之 FasterTransformer 加速 LLM 推理

作者&#xff1a;颜廷帅&#xff08;瀚廷&#xff09; 01 背景 OpenAI 在 3 月 15 日发布了备受瞩目的 GPT4&#xff0c;它在司法考试和程序编程领域的惊人表现让大家对大语言模型的热情达到了顶点。人们纷纷议论我们是否已经跨入通用人工智能的时代。与此同时&#xff0c;基…

CCF C³ 走进百度:大模型与可持续生态发展

2023年8月10日&#xff0c;由CCF CTO Club发起的第22期C活动在百度北京总部进行&#xff0c;以“AI大语言模型技术与生态发展”主题&#xff0c;50余位企业界、学界专家、研究人员就此进行深入探讨。 CCF C走进百度 本次活动&#xff0c;CCF秘书长唐卫清与百度集团副总裁、深…

如何保证数据传输的安全?

要确保数据传输的安全&#xff0c;您可以采取以下措施&#xff1a; 使用加密协议&#xff1a;使用安全的传输协议&#xff0c;如HTTPS(HTTP over SSL/TLS)或其他安全协议&#xff0c;以保护数据在传输过程中的安全性。加密协议可以有效防止数据被窃听或篡改。 强化身份验证&…

3种获取OpenStreetMap数据的方法【OSM】

OpenStreetMap 是每个人都可以编辑的世界地图。 这意味着你可以纠正错误、添加新地点&#xff0c;甚至自己为地图做出贡献&#xff01; 这是一个社区驱动的项目&#xff0c;拥有数百万注册用户。 这是一个社区驱动的项目&#xff0c;旨在在开放许可下向每个人提供所有地理数据。…

【云计算原理及实战】初识云计算

该学习笔记取自《云计算原理及实战》一书&#xff0c;关于具体描述可以查阅原本书籍。 云计算被视为“革命性的计算模型”&#xff0c;因为它通过互联网自由流通使超级计算能力成为可能。 2006年8月&#xff0c;在圣何塞举办的SES&#xff08;捜索引擎战略&#xff09;大会上&a…

Sentinel 规则持久化

文章目录 Sentinel 规则持久化一、修改order-service服务1.引入依赖2.配置nacos地址 第二步修改非常麻烦&#xff0c;可以略过&#xff0c;直接使用已经打好包的来使用二、修改sentinel-dashboard源码1. 解压2. 修改nacos依赖3. 添加nacos支持4. 修改nacos地址5. 配置nacos数据…

HCIP第五节------------------------------------------ospf

一、OSPF基础 1、动态路由分类 2、距离矢量协议 运行距离矢量路由协议的路由器周期性地泛洪自己的路由表。通过路由的交互&#xff0c;每台路由器都从相邻的路由器学习到路由&#xff0c;并且加载进自己的路由表中&#xff0c;然后再通告给其他相邻路由器。 对于网络中的所有…

AR/VR眼镜转接器方案,实现同时传输视频快充方案

简介 虚拟现实头戴显示器设备&#xff0c;简称VR头显VR眼镜&#xff0c;是利用仿真技术与计算机图形学人机接口技术多媒体技术传感技术网络技术等多种技术集合的产品&#xff0c;是借助计算机及最新传感器技术创造的一种崭新的人机交互手段。VR头显VR眼镜是一个跨时代的产品。不…

AgentBench——AI智能体基准测试和排行榜

如果您有兴趣了解有关如何对AI大型语言模型或LLM进行基准测试的更多信息,那么一种新的基准测试工具Agent Bench已成为游戏规则的改变者。这个创新工具经过精心设计,将大型语言模型列为代理,对其性能进行全面评估。该工具的首次亮相已经在AI社区掀起了波澜,揭示了ChatGPT-4目…

Selenium 测试用例编写

编写Selenium测试用例就是模拟用户在浏览器上的一系列操作&#xff0c;通过脚本来完成自动化测试。 编写测试用例的优势&#xff1a; 开源&#xff0c;免费。 支持多种浏览器 IE&#xff0c;Firefox&#xff0c;Chrome&#xff0c;Safari。 支持多平台 Windows&#xff0c;Li…

Spring中JavaBean的生命周期及模式

( 本篇文章大部分讲述了是底层知识&#xff0c;理念及原理 ) ( 如果只想了解&#xff0c;看我标记的重点即可&#xff0c;如果想明白其中原理&#xff0c;请耐心看完&#xff0c;对你大有受益 ) 目录 一、简介 ( 1 ) 是什么 ( 2 ) 背景概述 ( 3 ) 作用 二、生命周期 2.1 …

【计算机视觉|生成对抗】StackGAN:使用堆叠生成对抗网络进行文本到照片逼真图像合成

本系列博文为深度学习/计算机视觉论文笔记&#xff0c;转载请注明出处 标题&#xff1a;StackGAN: Text to Photo-realistic Image Synthesis with Stacked Generative Adversarial Networks 链接&#xff1a;[1612.03242] StackGAN: Text to Photo-realistic Image Synthesis…

ViewFs And Federation On HDFS

序言 ViewFs 是在Federation的基础上提出的,用于通过一个HDFS路径来访问多个NameSpace,同时与ViewFs搭配的技术是client-side mount table(这个就是具体的规则配置信息可以放置在core.xml中,也可以放置在mountTable.xml中). 总的来说ViewFs的其实就是一个中间层,用于去连接不…

STM32 F103C8T6学习笔记8:0.96寸单色OLED显示屏显示字符

使用STM32F103 C8T6 驱动0.96寸单色OLED显示屏: OLED显示屏的驱动&#xff0c;在设计开发中OLED显示屏十分常见&#xff0c;因此今日学习一下。一篇文章从程序到显示都讲通。 文章提供源码、原理解释、测试工程下载&#xff0c;测试效果图展示。 目录 OLED驱动原理—IIC通信…

无涯教程-Perl - undef函数

描述 此函数未定义EXPR的值。用于标量,列表,哈希,函数或类型范围。在带有诸如undef $hash {$key}之类的语句的哈希上使用&#xff1b;实际上将指定键的值设置为未定义的值。 如果要从哈希中删除元素,请使用delete函数。 语法 以下是此函数的简单语法- undef EXPRundef返回…