基于二分查找的动态规划 leetcode 300.最长递增子序列(续)

news2024/12/24 21:02:49

封面解释:你看那一口口剑,像不像一个个子序列【狗头】

一。前置条件

阅读本文之前,建议先看一下上一篇文章:

基于二分查找的动态规划 leetcode 300.最长递增子序列-CSDN博客

起码把对应的leetcode题目、以及对应的官方题解二,即“基于二分查找的动态规划”解法看一下,如果看不懂题解二、或者说看了有不少疑惑,那么本文可能适合你。

二。换一种角度思考

上篇从patience game纸牌游戏的玩法角度分析了为什么这种解法能求出最长严格递增子序列,但其实还存在一些关键的问题:

a)这种解法到底是怎么想出来的?怎么会想到用一个纸牌游戏来解决这个问题?

b)题解二真的是动态规划算法吗?

1.暴力解动态规划分析

先让我们来想一下,暴力解是怎么求最长严格递增子序列的。以nums=[3, 4, 5, 1, 2, 7]为例,挨个处理每一个元素,选择出以该元素结尾的最长严格递增子序列,把它加入一个池子中,这个池子中的每一个子序列都是以对应元素结尾的最长严格递增子序列(觉得这个绕的话先别急,往下看)。

(1)处理3,直接把3加入池子就行啦。池子=[3]

(2)处理4,拿它和池子中所有子序列比较一下,看它能不能加到它们末尾,并且只保留最长的那一个。原来池里面只有一个3。

所以现在池子变成:

3
3, 4

池子里面为啥没有4?注意,我们上面已经说了,池子里面每个子序列都是“以对应元素结尾的最长严格递增子序列”。这个对应元素是谁,就是当前处理的元素,就是4啊。[3,4]和[4]谁是以4结尾的最长严格递增子序列?当然是[3,4]啦。

那为什么要这么规定呢?原因也很简单,假设nums=[3, 4, 5],放5之前,池子里是:

3
3, 4
4

拿5挨个试一下,最终发现3,4,5是最长严格递增子序列。但问题是第3行的[4]这个序列有必要试吗?肯定没必要。

这个时候你可能会问:那第1行的[3]有没有必要试?自然有必要。比如nums=[3,8,9,4,5,7],处理到8的时候,池子如下,能把第1行的[3]扔掉吗?自然不能,扔掉了,后面怎么再接出3,4,5,7呢?

3
3,8

好,言归正传,还用原来的例子,nums=[3, 4, 5, 1, 2, 7]

(3)处理完5

3
3,4
3,4,5

。。。。。。

(4)中间省略,现在处理到7了

3
3,4
3,4,5
1,
1,2

拿7挨个试一下,最终发现,[3,4,5,7]是最长的,所以答案就是长度为4

2.暴力解动态规划代码

直接上代码

from typing import List

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        nums_len = len(nums)

        if not nums_len:
            return 0

        dp = [1] * nums_len
        for i in range(1, nums_len):
            for j in range(0, i):
                if nums[j] >= nums[i]:
                    continue

                dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)


if __name__ == '__main__':
    nums = [3, 4, 5, 1, 2, 7]
    print(Solution().lengthOfLIS(nums))

等等,你这个不就是题解一的代码吗?你刚才说的池子呢?

如上图,dp就是那个池子,现在i为5,就是在正处理最后一个元素7,dp前5个元素,就是池子里面的所有序列的长度。

那这些序列的末尾元素呢?就是对应的nums前5个元素啊!

我再把之前处理7的时候的池子贴过来,你看它们的长度是不是依次是1,2,3,1,2

3
3,4
3,4,5
1,
1,2

同时它们的尾元素是不是[3,4,5,1,2],这不就是nums前5个元素吗?

所以我刚才说的,其实就是官方题解一的解法。

现在不是要说题解2吗?别急,题解二的解法还真的能从题解一改良而来,不过还得等一等,我们先确定一下题解一的动态规划算法的三大元素

3.暴力解动态规划算法的三大元素

(1)最优子结构

3
3,4
3,4,5
1,
1,2

处理7的时候,它其实找的就是这里面末尾元素比7小的最长的一个子序列,这是不是就是最优子结构。

(2)重复子问题

看上面的池子好像看的不是很明显,它所解决的问题是什么?池子里面每一个序列的长度就是问题的解,所以问题就是“以对应元素结尾的最长严格递增子序列的长度”,我把图再贴一下,就是红线处的那些值

比如dp[1]=2,代表的就是[3,4]序列的长度,再处理元素5的时候,会用到这个长度,在处理7的时候,仍然会用到这个长度,这是不是就是重复子问题

(3)状态转移函数

把7拼到[3,4,5]之上,即算出dp[5]=4,就是执行了状态转移函数之后的结果。

所以函数就是dp[i]=max(dp[i], dp[j]+1),当然喽j得满足一系列的条件,都在代码里啦,我就不仔细写了。

三。改良

接下来,是时候往题解二努力了。

1.改良思路

3
3,4
3,4,5
1,
1,2

还是再拿出处理7的时候的池子,我们每一个都试了一遍,确实很暴力,这里面有没有哪些是可以省去的,我把它们按长度归个类。

1
3

1,2
3,4

3,4,5

我把7加到长度为1的序列上,得到的必然是长度为2的序列,不管是加到1上,还是加到3上。那我能不能只考虑加到1上呢?当然可以,其实不管是加到哪个是(假设加的是x),都不会影响[x, 7]这个序列以后的命运,原因很简单,后面再加的时候,人家只看到末尾7,谁会管这个序列的前面是什么呢?

但是这只是对于7来说,如果现在加进来的不是7,而是2,即nums=[3, 4, 5, 1, 2, 2],最后变成了一个2,那是不是就有区别了,2可以接到1后面,但不能接到3后面。所以说扔3就行,不要扔1。即保留最小的那个

完整来说,我们的结论是:确实池子中不需要保存所有元素结尾的最长严格递增子序列,我们只要保留同长度的末尾元素最小的那个就行了!并且序列的长度必然是从1开始一个一个增长的,如下图,即有1长度的、有2长度的、有3长度的。

如上图,经过刚才所说规则,同长度只保留末尾元素最小的那个,我们删除了[3]、删除了[3,4]。现在长度为1的末尾元素是1,长度为2的末尾元素是2,长度为3的末尾元素是5,你可能会发现它们是严格递增的,是不是一定这样呢?

我们可以用反证法,假设不是这样,现在假设池子里只有如下两个序列,并且x大于等于4,有没有这种可能?

x
y 4

当然是不可能!因为按照我们的规则,x一定是序列长度为1的末尾元素最小的那一个,但是如果x大于等于4的话,则x必然大于y,而y一定是在nums中位于4之前的一个元素,那我在把[y, 4]这个序列加入池中之前,我完全应该先把[y]加入池中,替换掉[x],因为刚才说了x大于y!

所以池中“长度为n的序列的末尾元素”一定比“长度为n+1的序列的末尾元素”小,所以池中所有序列按照序列长度排序后,它们的末尾元素一定是严格递增的。

严格递增那就咋了呢?它带来了一个更大的惊喜啊!

再回顾一下刚才的情况:

我们删了一些可以忽略的序列,目的是为了在暴力遍历的时候,尽量少遍历一些序列。但此时我们仍然是一个一个遍历,池中序列数量跟nums的长度n仍然可能是一个比例关系,所以每处理一个元素的算法复杂度仍然是O(n),只是顶多乘一个小系数而已。

但如果我们在池中匹配序列的时候,能采用二分法,那就太好了。

上图的三个序列,此时我们处理的nums中元素为7,我们知道一定是把7接到3,4,5之后,形成一个新的序列3,4,5,7,并且加入池中。

1

1,2

3,4,5

3,4,5,7

为啥说一定?回顾一下我们刚才说的原则,7比1、2、5都大,所以它动摇不了前3个序列本身,但由于当前没有长度为4的序列,而且7确实能接到5之后,新增序列[3,4,5,7]

但问题是我们真的需要一个一个序列去比较吗?我们可不可以用二分法快速找出7该去的位置?当然可以,二分查找的逻辑就是:在池中以序列长度排序的各序列尾元素组成的数组中,其实就是上篇文章说的f数组=[1, 2, 5],在此数组中查找大于等于7的最小元素,如果找不到,那就在数组右边插入该元素,所以最终f数组=[1,2,5,7],对应的就是在池中新增了一个[3,4,5,7]的序列。

假设现在找的不7,是3,池子还是一样,f数组仍然是[1,2,5],那大于等于3的最小元素就是5,所以用3更新5,f数组=[1,2,3]

对应池子也变了

那为什么可以用刚才的二分查找的逻辑来找呢?

1

1,2

3,4,5

还是以找7来说明,注意,上面的序列是按照序列长度排序的,并且之前也说过,它们的尾元素是严格递增的,从这个规则上来说,7就不可能去改变这三个序列,因为7比它们的尾元素都大。所以7到底要改变的是谁?7要改变的就是尾元素刚刚好比7大的那个(或都说是尾元素比7大的那些序列中尾元素最小的那个,稍微有点绕),所以说就符合刚才二分查找的逻辑,就是大于等于7的最小值(等于7的时候,其实什么都没变)。如果找不到,就说明7比所有人都大,那就只需要在池中新增一个序列就行了,并且7是接到之前最长的序列之后。

 再换个情况,即假设不是处理7,而是处理元素3,如下图

1

1,2

3,4,5

此时二分查找,比3大的最小尾元素是5,所以在f数组中,3替换5,f由[1,2,5]变成了[1,2,3],对应池中变化我再贴一下,它的实际操作其实是把3接到[1,2]后得到[1,2,3],并用此序列替换掉原来的[3,4,5]。

到这里核心改良思想已经完成了,并且我们有撼动核心的动态规化思想吗?并没有,动态规化的核心思想、三大要素都是围绕上图的池中序列,池中序列就是最优子结构、重复子问题,新增序列、替换序列的时候就是在状态转移,我们改良的只是减少了池中的序列,删除了不必要的序列,并且把逐个匹配序列变成了二分查找,从而让整个算法的时间复杂度变成了O(nlogn)(n个元素,每个元素匹配池中序列都是logn次)

2.实际操作

说的好像很有道理,但是我真的需要保存池中序列吗?而且我还要不停地在池中新增序列,删除序列,我怎么觉得这压根就不是O(nlogn)的复杂度呢?

当然,如果只求长度的话,你完全可以不用保存池中序列,你有没有发现,我们匹配序列的时候只看尾元素,根本就不管每个序列的其它元素。所以说我们只需要保存f数组就行了,而且我们的二分查找就是在f数组上执行的,替换、新增元素也是直接在f数组上执行的!所谓的池只是我们核心思想中的抽象概念而已!

3.改良代码

代码就是前一篇文章中的代码,只不过这里我们只提了f数组,所以把之前的牌堆变量piles改成了f,换个名字而已。

from typing import List

import bisect

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        f = [nums[0]]

        for i in range(1, len(nums)):
            if f[-1] < nums[i]:
                f.append(nums[i])

            find_index = Solution.bisect_left(f, nums[i])
            # find_index = bisect.bisect_left(piles, nums[i])
            f[find_index] = nums[i]

        return len(f)

    @staticmethod
    def bisect_left(a, x, l=0, h=-1):
        if h == -1:
            h = len(a)

        while l < h:
            mid = (l + h) // 2
            if x > a[mid]:
                l = mid + 1
            else:  # elif x <= a[mid]
                # 这里为什么不是h = mid - 1,因为如果找不到x,则找大于x的最小值,即右边界我们可能是需要的
                # 为什么把x == a[mid]的分支也合到这里面?其实只是想跟bisect.bisect_left逻辑保持一致而已,即如果存在重复的x,则返回最左边的x
                # 实际上本题不可能有重复的x,完全可以在找到x后立马返回,能稍微快那么一点点
                h = mid

        return l


if __name__ == '__main__':
    nums = [0, 3, 1, 6, 2, 2, 7]
    print(Solution().lengthOfLIS(nums))

4.那我如果就是要求出具体的最长严格递增子序列呢?

看一下前一篇文章的第4大点吧,最好把整篇都看一下哦,因为它这次就真的离不开牌堆了

https://blog.csdn.net/ogebgvictor/article/details/142533126

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

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

相关文章

论文不会写怎么办?推荐这5款AI论文工具帮你一键搞定!

在当今的学术研究和写作领域&#xff0c;AI论文工具已经成为不可或缺的助手。这些工具不仅能够提高写作效率&#xff0c;还能帮助研究者生成高质量的论文。本文将推荐五款优秀的AI论文工具&#xff0c;并特别推荐千笔-AIPassPaper&#xff0c;以帮助读者更好地完成学术写作任务…

Go 1.19.4 序列化和反序列化-Day 16

1. 序列化和反序列化 1.1 序列化 1.1.1 什么是序列化 序列化它是一种将程序中的数据结构&#xff08;map、slice、array等&#xff09;或对象状态转换成一系列字节序列的过程&#xff0c;这些字节可以被存储或通过网络发送。 在GO中&#xff0c;序列化通常涉及到将结构体或其…

JavaSE——三代日期类(Date、Calendar、LocalDate、LocalTime、LocalDateTime、Instant)

目录 一、util包.Date类——第一代日期类 二、SimpleDateFormat——日期时间格式化 1.日期时间转换成字符串 2.字符串转换成日期时间 三、Calendar日历类——第二代日期类 1.Calendar类中常用的静态方法 2.设置指定的字段(不常用) 3.calendar.setTime()使用案例——获取…

总结拓展十二:SAP采购定价条件

第一节 采购定价条件 1.条件类型介绍 2.条件类型概念 条件类型&#xff08;Condition Technology&#xff09;是SAP中运用较多的配置技术&#xff0c;了解条件技术如何运作&#xff0c;有助于我们理解系统在不同情况下的行为和反应&#xff0c;条件技术广泛地应用于定价、文…

【论文_2000】REINFORCE 和 actor-critic 等策略梯度方法的局部收敛性证明

部分证明不太理解 SUTTON R S, MCALLESTER D A, SINGH S P, et al. Policy gradient methods for reinforcement learning with function approximation [C] // Advances in neural information processing systems, 2000: 1057-1063. 【PDF 链接】 文章目录 摘要引言1 策略梯…

Arthas jad(字节码文件反编译成源代码 )

文章目录 二、命令列表2.2 class/classloader相关命令2.2.1 jad&#xff08;字节码文件反编译成源代码 &#xff09;举例1&#xff1a;反编译指定的函数 &#xff1a;jad com.hero.lte.ems.sysmanager.cache.SMTaskCache executeTask举例2&#xff1a;反编绎时只显示源代码&…

PCIe扫盲(14)

系列文章目录 PCIe扫盲&#xff08;一&#xff09; PCIe扫盲&#xff08;二&#xff09; PCIe扫盲&#xff08;三&#xff09; PCIe扫盲&#xff08;四&#xff09; PCIe扫盲&#xff08;五&#xff09; PCIe扫盲&#xff08;六&#xff09; PCIe扫盲&#xff08;七&#xff09…

如何查看电脑的虚拟内存信息?

1、按下键盘的 win R 键 &#xff0c; 输入&#xff1a;cmd &#xff0c; 然后按下【回车】 2、在弹出的窗口输入&#xff1a;systeminfo &#xff0c; 然后按下【回车】&#xff0c;等待加载结果出来。 3、如下位置&#xff0c;显示的即是当前电脑的【虚拟内存】信息&…

Fusion Access

1.FA桌面云需要微软三剑客 2.AD&#xff0c;DNS&#xff0c;DHCP合并部署在一台虚机&#xff0c;内存配置8G 3.FA各个组件 3.1终端接入 3.2接入和访问控制层 3.3虚拟桌面管理层-桌面云规划及部署 3.4安装Linux基础架构虚拟机FA01 3.4.1安装Tools 3.4.2安装FusionAccess组件&am…

希捷电脑硬盘好恢复数据吗?探讨可能性、方法以及注意事项

在数字化时代&#xff0c;数据已成为我们生活和工作中不可或缺的一部分。希捷电脑硬盘作为数据存储的重要设备&#xff0c;承载着大量的个人文件、工作资料以及珍贵回忆。然而&#xff0c;面对硬盘故障或误操作导致的数据丢失&#xff0c;许多用户不禁要问&#xff1a;希捷电脑…

找到你的工具!5款免费可视化报表工具对比分析

选择合适的可视化工具对于分析和展示数据至关重要&#xff0c;以下是五款免费的可视化工具&#xff0c;它们各具特色&#xff0c;能够适应各种需求。本文将介绍每款工具的优势与不足&#xff0c;帮助你找到最合适的解决方案。 1. 山海鲸可视化 介绍&#xff1a;山海鲸可视化是…

UniApp组件与微信小程序组件对照学习

UniApp只是一个第三方的开发工具&#xff0c;借鉴各种平台的能力&#xff0c;UniApp的组件也借鉴了微信小程序的组件&#xff0c;我们学习时&#xff0c;可以进行对照学习&#xff0c;我们在用UniApp开发微信小程序时&#xff0c;UniApp也只是将代码转成了微信小程序的代码&…

“电瓶车火灾”频发,如何防范自救

1.概述 近年来&#xff0c;随着电动自行车使用的普及化&#xff0c;由此引发的起火事故频繁发生。作为上海市烧伤急救中心&#xff0c;上海交通大学医学院附属瑞金医院的灼伤整形科收治的此类病人数量也在逐年上升。电动自行车&#xff0c;已经成为一种新型火灾事故的“肇事者…

【Docker】02-数据卷

1. 数据卷 数据卷(volume) 是一个虚拟目录&#xff0c;是容器内目录与宿主机目录之间映射的桥梁。 2. 常见命令 docker volume createdocker volume lsdocker volume rmdocker volume inspect 查看某个数据卷的详情docker volume prune 清除数据卷 **数据卷挂载&#xff1a…

【笔记】数据结构|链表算法总结|快慢指针场景和解决方案|链表归并算法和插入算法|2012 42

受堆积现象直接影响的是&#xff1a;平均查找长度 产生堆积现象&#xff0c;即产生了冲突&#xff0c;它对存储效率、散列函数和装填因子均不会有影响&#xff0c;而平均查找长度会因为堆积现象而增大。 2012 42 参考灰灰考研 假定采用带头结点的单链表保存单词&#xff0c;当…

MySQL_表_进阶(1/2)

我们的进阶篇中&#xff0c;还是借四张表&#xff0c;来学习接下来最后关于表的需求&#xff0c;以此完成对表的基本学习。 照例给出四张表&#xff1a; 学院表&#xff1a;(testdb.dept) 课程表&#xff1a;(testdb.course) 选课表:&#xff08;testdb.sc&#xff09; 学生表…

JS面试真题 part7

JS面试真题 part7 31、web常见的攻击方式有哪些&#xff1f;如何防御32、说说JavaScript中内存泄漏的几种情况33、JavaScript如何实现继承34、说说JavaScript数字精度丢失的问题&#xff0c;如何解决35、举例说明你对尾递归的理解&#xff0c;有哪些应用场景 31、web常见的攻击…

使用kaggle命令下载数据集和模型

点击用户头像&#xff0c;点击Settings&#xff1a; 找到API&#xff0c;点击create new token&#xff0c;将自动下载kaggle.json&#xff1a; 在用户目录下创建.kaggle文件夹&#xff0c;并将下载的kaggle.json文件移动到该文件夹&#xff1a; cd ~ mv Downloads/kaggle.j…

Universal Link配置不再困扰,Xinstall来帮忙

在移动互联网时代&#xff0c;App的推广和运营至关重要。而Universal Link作为一种能够实现网页与App间无缝跳转的技术&#xff0c;对于提升用户体验、引流至App具有显著效果。今天&#xff0c;我们就来科普一下Universal Link的配置方法&#xff0c;并介绍如何通过Xinstall这款…

2024-2025华为ICT大赛报名|赛前辅导|学习资料

华为ICT大赛是华为公司打造的面向全球高校的年度ICT赛事&#xff0c;大赛以“联接、荣耀、未来”为主题&#xff0c;协同政府、高等教育机构、培训机构和行业企业&#xff0c;促进高校ICT人才培养、成长和就业&#xff0c;助力ICT人才生态繁荣。2021年3月&#xff0c;大赛成功入…