最长上升子序列(LIS)问题的四种求解方法(JavaScript版,自用)_js 最长递增子序列算法-程序员宅基地

技术标签: 算法  dk算法笔录  javascript  

前言

问题:300. 最长递增子序列

本篇文章用于记录对LIS问题的求解思路总结,用到的方法如下:

1. DP:时间复杂度:O(n^2)        2.二分+贪心:O(nlogn)       

3.dfs+回溯:O(2^n)                 4.树状数组优化DP:O(nlogn)    

在明白LIS问题,我们需要懂得一个概念,那就是何为子串,何为子序列的问题。

简单来说,假设你拥有一数字序列,我从其中抽出数字,若你属于连续抽取,取出的数都是相邻的,则为子串,若非连续,则为子序列。


LIS的定义

最长上升子序列(简称LIS),也有些时候,求的是最长非降序子序列,二者区别就是序列中是否可以有相等的数。

那什么是LIS问题呢,简单来说,假设我给你一个数字序列,如果这个序列中,存在这样一个子序列,该子序列中的数成递增现象,则为上升子序列,而LIS则是求其中最长的子序列,例子如下:

比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8)


方法一:DP

我们都知道,动态规划的一个特点就是当前解可以由上一个阶段的解推出, 由此,把我们要求的问题简化成一个更小的子问题。子问题具有相同的求解方式,只不过是规模小了而已。最长上升子序列就符合这一特性。我们要求n个数的最长上升子序列,可以求前n-1个数的最长上升子序列,再跟第n个数进行判断。求前n-1个数的最长上升子序列,可以通过求前n-2个数的最长上升子序列……直到求前1个数的最长上升子序列,此时LIS当然为1。

  让我们举个例子:求 2 7 1 5 6 4 3 8 9 的最长上升子序列。我们定义d(i) (i∈[1,n])来表示前i个数以A[i]结尾的最长上升子序列长度。

  前1个数 d(1)=1 子序列为2;

  前2个数 7前面有2小于7 d(2)=d(1)+1=2 子序列为2 7

  前3个数 在1前面没有比1更小的,1自身组成长度为1的子序列 d(3)=1 子序列为1

  前4个数 5前面有2小于5 d(4)=d(1)+1=2 子序列为2 5

  前5个数 6前面有2 5小于6 d(5)=d(4)+1=3 子序列为2 5 6

  前6个数 4前面有2小于4 d(6)=d(1)+1=2 子序列为2 4

  前7个数 3前面有2小于3 d(3)=d(1)+1=2 子序列为2 3

  前8个数 8前面有2 5 6小于8 d(8)=d(5)+1=4 子序列为2 5 6 8

  前9个数 9前面有2 5 6 8小于9 d(9)=d(8)+1=5 子序列为2 5 6 8 9

  d(i)=max{d(1),d(2),……,d(i)} 我们可以看出这9个数的LIS为d(9)=5

  总结一下,d(i)就是找以A[i]结尾的,在A[i]之前的最长上升子序列+1,当A[i]之前没有比A[i]更小的数时,d(i)=1。所有的d(i)里面最大的那个就是最长上升子序列。

        简单来说,每次都向前找比它小的数与比它大的数的位置,将第一个比它大的替换掉,这样虽然LIS序列的具体数字可能会变,但是很明显LIS长度还是不变的,因为只是把数替换掉了,并没有改变增加或者减少长度。

状态设计:F[i]代表以A[i]结尾的LIS的长度

状态转移:F[i]=max{F[j]+1}(1<=j< i,A[j]< A[i])

边界处理:F[i]=1(1<=i<=n)

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

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
    let max = 1;
    const dp = new Array(nums.length).fill(1);
    for (let i = 1; i < nums.length; i++) {
        for (let j = 0; j < i; j++) {
            nums[j] < nums[i] && (dp[i] = Math.max(dp[j] + 1, dp[i]));
        }
        max = Math.max(dp[i], max);
    }
    return max;
};

        这个算法的时间复杂度为〇(n²),代码也清晰,但这并不是最优的算法。在限制条件苛刻的情况下,这种方法行不通。那么怎么办呢!有没有时间复杂度更小的算法呢?说到这里了,当然是有的啦!还有一种时间复杂度为〇(nlogn)的算法,下面就来看看。


方法二:二分+贪心

        新建一个low数组,里面的low[i]表示长度为i的LIS结尾元素的最小值。对于一个上升子序列,显然,其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。因此,我们只需要维护low数组,对于每一个a[i],如果a[i] > low[当前最长的LIS长度],就把a[i]接到当前最长的LIS后面,即low[++当前最长的LIS长度]=a[i]。
        那么,怎么维护low数组呢?
        对于每一个a[i],如果a[i]能接到LIS后面,就接上去;否则,就用a[i]取更新low数组。具体方法是,在low数组中找到第一个大于等于a[i]的元素low[j],用a[i]去更新low[j]。如果从头到尾扫一遍low数组的话,时间复杂度仍是O(n^2)。我们注意到low数组内部一定是单调不降的,所有我们可以二分low数组,找出第一个大于等于a[i]的元素。二分一次low数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
    function binary_search(arr, l, r, key) {
    if (arr[r] < key)
        return r + 1;

    while (l < r) {
        let mid = l + Math.floor((r - l) / 2);
        if (arr[mid] < key)
            l = mid + 1;
        else
            r = mid;
    }
    return l;
}
    let B = [nums[0]];
    for (let i = 1; i < nums.length; i++) {
        let item=B.at(-1);
        if(item < nums[i]) B.push(nums[i]);
        else if(nums[i]<item) {
            let next=binary_search(B,0,B.length-1,nums[i])
            B[next]=nums[i]
        };
    }
    return B.length;
};

方法三:dfs + 回溯

这个方法其实会稍微好理解些,比如此时的序列为:5,3,4,9,7

我们换种想法,我们的目标是为了能够得到最长的子序列,那么,假设我们能够得到所有的序列,在求得的同时进行剪枝操作,或许就能求出来,但我们不可避免的就是,时间复杂度会特别高:O(2^n),只做了解,因为特别扯淡,千万别在面试用哈哈

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
    let res = 1;
    const dfs = (nums, path, index) => {
        res = Math.max(res, path.length);
        for (let i = index; i < nums.length; i++) {
            if (path.length > 0 && path[path.length - 1] >= nums[i]) {
                continue;
            }
            if ((path.length > 0 && path[path.length - 1] < nums[i]) || path.length === 0) {
                path.push(nums[i]);
            }
            dfs(nums, path, i + 1);
            path.pop();
        }
    }
    dfs(nums, [], 0);
    return res;
};

方法四:树状数组优化DP

        在方法一中,我们遇到一个问题就是,在动态规划数组的状态,每次都需要去遍历F函数前面的值,这会大大的增加我们的时间开销。我们可以借助数据结构来优化这个过程。用树状数组来维护F数组(据说分块也是可以的,但是分块是O(n*sqrt(n))的时间复杂度,不如树状数组跑得快),每次都来获取到F数组中,第i个前面的数,大于F[i]的值,这样就能大大地提高我们的运行速度,代码如下:

/**
 * @param {number[]} nums
 * @return {number}
 */
// 实现树状数组
class FenwickTree {
    constructor(size) {
        this.size = size;
        this.tree = new Array(size + 1).fill(0);
    }
    // 更新
    update(index, delta) {
        for (; index <= this.size; index += index & -index) {
            this.tree[index] = Math.max(this.tree[index], delta);
        }
    }
    // 查询
    query(index) {
        let maxVal = 0;
        for (; index > 0; index -= index & -index) {
            maxVal = Math.max(maxVal, this.tree[index]);
        }
        return maxVal;
    }
}

var lengthOfLIS = function(nums) {
    // 去重,排序,并创建二维存索引
    const numToIndex = new Map([...new Set(nums)].sort((a, b) => a - b).map((num, index) => [num, index + 1]));
    const n = nums.length;
    const tree = new FenwickTree(numToIndex.size);
    let maxLen = 0;

    for (let i = 0; i < n; i++) {
        const index = numToIndex.get(nums[i]);
        const prevMax = tree.query(index-1);
        if(prevMax==0|| nums[i]!==nums[i-1]){
            const dpVal=prevMax+1
            tree.update(index, dpVal);
            maxLen = Math.max(maxLen, dpVal);
        }
    }
    return maxLen;
};

 值得一提的是:树状数组求LIS不去重的话就变成了最长不下降子序列了

一个问题多解化,有时候有助于学习,题目往往会难在你是否发现此种类型的题。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Cdkouo/article/details/136587410

智能推荐

java中的volatile关键字的功能详解_volatile 关键字,-程序员宅基地

文章浏览阅读4.6k次。Cookie的应用场景:1,记录用户的登陆状态,提示用户是否记住密码;2,购物车购物功能;我们知道,在web开发过程中,我们都要和cookie打交道,有时候离开了cookie还真玩不转。cookie最典型的应用莫过于登陆提示,最近在做一个小项目,正好要用到cookie的知识,在这里顺便做一下总结。_volatile 关键字,

通过 ICMP 协议实现 Ping Tunnel 建立可穿透网络隧道-程序员宅基地

文章浏览阅读7.1k次。Twitter via Ping Tunnel周四 Cola 没去幼儿园,中午带着他去 KFC 吃东西。回来的时候小林指着西总布胡同说走这条路回去还是原路返回,他说还是..._ping tunnel

基于springboot+vue.js的名城小区物业管理系统(附带文章和源代码设计说明文档ppt)-程序员宅基地

文章浏览阅读817次,点赞18次,收藏20次。博主介绍:CSDN深耕的技术专家、博客专家、有着常年的工作经验、全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战文末获取源码+数据库。

Arthas使用教程 阿里巴巴开源项目、史上最强java线上诊断工具-程序员宅基地

文章浏览阅读1.4w次,点赞31次,收藏263次。什么是 Arthas摘录一段官方 Github 上的简介Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。当你遇到以下类似问题而束手无策时,Arthas 可以帮助你解决:这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!是否有一个全局视角来._arthas

java 方法注释格式_JAVA注释方法及格式-程序员宅基地

文章浏览阅读2.7k次。2019独角兽企业重金招聘Python工程师标准>>>JAVA注释方法及格式1、单行(single-line)--短注释://……单独行注释:在代码中单起一行注释, 注释前最好有一行空行,并与其后的代码具有一样的缩进层级。如果单行无法完成,则应采用块注释。注释格式:/* 注释内容 */行头注释:在代码行的开头进行注释。主要为了使该行代码失去意义。注释格式:// 注释内容行尾注释:..._方法注释

egret4.X版本项目无法与egret 5.X项目共存解决_egret 4.x老项目升级-程序员宅基地

文章浏览阅读367次。在编译egret 5.X 项目项目中执行egret clean_egret 4.x老项目升级

随便推点

【光伏功率预测】基于EMD-PCA-LSTM的光伏功率预测模型(Matlab代码实现)_计及相似日的 lstm 光伏出力预测模型研究-程序员宅基地

文章浏览阅读917次。随着电厂规模的不断扩增,电厂的数据量也呈爆炸式的增长,传统的神经网络光伏功率预测模型[7-10]一方面受电厂来源数据的制约,忽略了部分环境因素对光伏功率的影响[11] ,缺乏对多元环境序列信息的有效利用;另一方面,由于光伏功率与多元环境序列信息呈非线性变化,随着网络输入变量的增多,会导致模型收敛速度减慢[12-14] ,并出现过拟合问题;因此,要提高光伏功率预测模型的准确性,不仅要充分利用影响光伏功率的关键环境因素,也要进一步挖掘光伏功率预测与关键环境因素随时间变化的本质特征。长短期记忆神经网络;_计及相似日的 lstm 光伏出力预测模型研究

Flutter开发学习实战之商城项目(后端+Flutter端)_flutter 商城-程序员宅基地

文章浏览阅读4.7k次,点赞3次,收藏39次。Flutter_mall 商城项目引言:此Flutter工程项目是在学习 youxinLu 大佬写的一个商城项目:作者项目简介:Flutter_Mall是一款Flutter开源在线商城应用程序,是基于litemall基础上进行开发,litemall包含了Spring Boot后端 + Vue管理员前端 + 微信小程序用户前端 + Vue用户移动端感兴趣的同学可以自行研究部署,Flutter_Mall基本上包含了litemall中小程序的功能。学习的最好方法就是动手实践:于是将 youxinL_flutter 商城

Leetcode 104. 二叉树的最大深度(DAY 2)-程序员宅基地

文章浏览阅读380次。原题题目代码实现/** * Definition for a binary tree node. * struct TreeNode { * int val; * struct TreeNode *left; * struct TreeNode *right; * }; */int maxDepth(struct TreeNode* root){ if(!root) return 0; if(!root->left

记一次RATEL脱壳配合Il2CppDumper解密完成的样本分析-程序员宅基地

文章浏览阅读2.4k次,点赞2次,收藏8次。今天分析一个网赚类的样本,业务方要我搞清楚为什么每次完成任务出现红包时,总会随之弹出一个插屏广告。大概就是下图这样,说实话,弹几层广告确实挺恶心,关起来都麻烦先在测试机装上应用,发现有root检测,试过几个通用的过root检测的frida脚本,发现不行看了下代码,是网易的壳正好,最近在看Ratel,刚好来练练手,试试在非root环境下Ratel的脱壳怎么样Rate..._il2cppdumper

Jmeter post请求传参问题_jmeter post传参-程序员宅基地

文章浏览阅读167次。作为一位过来人也是希望大家少走一些弯路,如果你不想再体验一次学习时找不到资料,没人解答问题,坚持几天便放弃的感受的话,在这里我给大家分享一些自动化测试的学习资源,希望能给你前进的路上带来帮助。_jmeter post传参

解决classNotFound的问题的思路_fatal error: class 'tradecreatedemo' not found-程序员宅基地

文章浏览阅读1k次。用Ctrl+Shift+t可以查看class,对于报错信息,我们把没有找到的class放到查找框里进行查看,找到之后把这个jar包放到WEB-INF的lib目录下,build path一下就可以了。以上是在java web项目中,没有使用maven的情况可以使用.如果使用maven,有时也会碰到这种情况,原因可能是jar包冲突,也可能是tomcat缓存,还可能是jar包放到了jre的ext..._fatal error: class 'tradecreatedemo' not found