首页 > 其他分享 >NlogN 求最长不下降子序列(LIS)

NlogN 求最长不下降子序列(LIS)

时间:2024-04-26 18:33:05浏览次数:12  
标签:int indices tail LIS 序列 长度 NlogN 最长

最长不下降子序列是一道非常经典的题目,我们假设题目如下:

有一个数组 $ a $,现在要从中抽取一个严格上升的子序列(子序列就是你可以从原序列里删除一些数,保留一部分数得到的新数列)(严格上升也就是不能相等只能递增),现在要求出这个子序列最长有多长?

举例来说,假设有一个数组 a = [10, 9, 2, 5, 3, 7, 101, 18]。这个数组的最长上升子序列是 [2, 3, 7, 101],这是从原数组中第3, 5, 6, 7个位置选取得到的,长度为4。(当然最后一个数字换成 18 也可以)

平方做法

通常来讲,平方做法很容易想到,我们开一个数组 $ f $ ,用 $ f_i $ 表示以 $ a_i $ 结尾的 LIS 的长度。我们从左往右计算每一个 $ f_i $ ,每一个 $ f_i $ 可能由 \(f_1, f_2...f_{i-1}\)得到。比方说假如 $ a_i > a_j $,那么 \(a_i\) 就可以接到 $ f_{i-1} $ 表示的序列后面,长度加一,枚举所有之前的 f,取最大值,最终就可以计算出来。

#include <vector>
#include <algorithm>
using namespace std;

int longestIncreasingSubsequence(const vector<int>& a) {
    int n = a.size();
    vector<int> f(n, 1);  // 初始化每个元素的LIS长度为1
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (a[i] > a[j]) {
                f[i] = max(f[i], f[j] + 1);
            }
        }
    }
    return *max_element(f.begin(), f.end());  // 返回f中的最大值,即最长上升子序列的长度
}

int main() {
    vector<int> a = {10, 9, 2, 5, 3, 7, 101, 18};
    int result = longestIncreasingSubsequence(a);
    return result;
}

N log N 做法

更高效的方法是通过维护一个数组 $ tail $ 来实现,其中 $ tail_i $ 存储长度为 i 的所有上升子序列中末尾最小的元素(比如 $ tail_4 $ 表示所有长度为 4 的 LIS 里最后一个数字最小能达到多少)。在更新过程中,我们使用二分搜索来确定一个元素应当放置在 $ tail $ 中的位置。

我们还是从左向右来枚举,对于每个元素 $ a_i $,在 \(tail\) 中找到第一个大于 $ a_i $ 的位置。

  • 如果找到这样的位置,即表示之前有一个序列的末尾可以被替换,所以修改其 $ tail $ 值为新 $ a_i $
  • 如果没有找到,说明 $ a_i $ 比之前所有数字都大,那么把 $ a_i $ 追加到 $ tail $ 末尾,最长的上升序列长度成功加1.

对于 $ tail $ 来说,下标越大,条件越难满足,则能达到的最小值越大,所以 $ tail $ 显然是严格递增的,这不难理解,而每一次操作也维持了 $ tail $ 的单调性。

举例来说,现在\(tail = [3, 5, 6, 19, 25]\),而当前 $ a_i $ 为 8,可以找到 $ tail_4 = 19$ 这表明在我之前出现过一个长度为4,以 19 结尾的序列,以及一个长度为3,以小于8结尾的序列,那么这个长度为3的序列可以接上8,替换掉原来的19,一定更优

#include <vector>
#include <algorithm>
using namespace std;

int longestIncreasingSubsequence(const vector<int>& a) {
    vector<int> tail;
    for (int x : a) {
        auto it = lower_bound(tail.begin(), tail.end(), x);
        if (it == tail.end()) {
            tail.push_back(x);  // 如果x大于tail中所有元素,追加到末尾
        } else {
            *it = x;  // 替换为更小的值,保持序列最小
        }
    }
    return tail.size();  // tail的大小就是最长上升子序列的长度
}

int main() {
    vector<int> a = {10, 9, 2, 5, 3, 7, 101, 18};
    int result = longestIncreasingSubsequence(a);
    return result;
}

获得具体的数列

除了得到最长长度,此方法也是支持把序列获取到的,增加一个数组,每一次更新 tail 时,来记录每个元素的前一个元素的索引,这里附加上 chatgpt 的代码:

def longest_non_decreasing_subsequence(arr):
    from bisect import bisect_right

    if not arr:
        return 0, []

    dp = []  # 存放各长度最长不下降子序列的最小结尾元素的值
    predecessor = [-1] * len(arr)  # 前驱元素的索引
    indices = []  # 存放各长度最长不下降子序列的最小结尾元素的索引

    for i, x in enumerate(arr):
        pos = bisect_right([arr[idx] for idx in indices], x)
        if pos < len(indices):
            indices[pos] = i
            predecessor[i] = indices[pos-1] if pos > 0 else -1
        else:
            indices.append(i)
            predecessor[i] = indices[-2] if len(indices) > 1 else -1

    # 回溯找到最长不下降子序列
    n = len(indices)
    sequence = [0] * n
    k = indices[-1]
    for j in range(n-1, -1, -1):
        sequence[j] = arr[k]
        k = predecessor[k]

    return n, sequence

# 示例
arr = [10, 9, 2, 5, 3, 7, 101, 18]
length, sequence = longest_non_decreasing_subsequence(arr)
print("Length:", length)  # 输出长度
print("Sequence:", sequence)  # 输出序列

严格上升和不下降

要从求最长严格上升子序列变为求最长不下降子序列(也就是允许序列中的元素相等),主要修改的部分就是二分查找部分。在严格上升的场景中,tail 不会出现相等值,我们寻找的是第一个大于等于当前元素的位置,而在不下降的场景中,要找到的是第一个大于当前元素的位置。

举例子来说,当要求变为严格不下降,$ tail $ 就有可能出现相等值,假设 $ tail = [1, 2, 2, 3, 3, 3, 5] $,且下一个 \(a_i\) 为 2。那么应该更新 \(a_2\) 第一个3。而扩展 $ tail $ 的条件也放宽为大于等于,具体代码就不再展示。

标签:int,indices,tail,LIS,序列,长度,NlogN,最长
From: https://www.cnblogs.com/ofnoname/p/18160166

相关文章

  • To be reviewed List
     TobereviewedListNo.ItemAddDateFixDate1InheritableThreadLocal4/26/2024                                                 ......
  • 最长递增子序列
    https://leetcode.cn/problems/longest-increasing-subsequence/?envType=study-plan-v2&envId=top-interview-150classSolution:deflengthOfLIS(self,nums:List[int])->int:dp=[1]*len(nums)foriinrange(1,len(nums)):......
  • 洛谷题单指南-动态规划2-P1439 【模板】最长公共子序列
    原题链接:https://www.luogu.com.cn/problem/P1439题意解读:求最长公共子序列的长度。解题思路:1、O(n^2)的方法:动态规划设两个排列为a,b设dp[i][j]表示a[1~i]与b[1~j]的最长公共子序列长度根据公共子序列结尾是否包含a[i]、b[j]是否相等分情况讨论:如果a[i]==b[j]  则结尾......
  • netstat tasklist taskkill配合使用
    1.查找指定的端口号:netstat-ano|findstr 端口号 2.查找指定的进程号对应的程序名:tasklist/FI"PIDeq进程号" ,/FI"筛选器eq对应的值"   或者 tasklist |findstr进程号 或者 3.杀死指定进程:taskkill/t /f /pid进程号 ......
  • 常用的时间序列分析方法总结和代码示例
    时间序列是最流行的数据类型之一。视频,图像,像素,信号,任何有时间成分的东西都可以转化为时间序列。在本文中将在分析时间序列时使用的常见的处理方法。这些方法可以帮助你获得有关数据本身的见解,为建模做好准备并且可以得出一些初步结论。我们将分析一个气象时间序列。利用逐时ERA......
  • Iterator 和 ListIterator 有什么区别?
    前言Iterator和ListIterator都是Java集合框架中的迭代器接口,它们都可以用于遍历集合中的元素。ListIterator继承自Iterator接口,因此ListIterator可以用于任何实现了Iterator接口的集合,如List和Set。以下是两者的主要区别:原始集合类型的差别Iterator可以遍历Collection中的元......
  • JDK源码分析-LinkedList
    概述相较于ArrayList,LinkedList在平时使用少一些。LinkedList内部是一个双向链表,并且实现了List接口和Deque接口,因此它也具有List的操作以及双端队列和栈的性质。双向链表的结构如下:它除了作为List使用,还可以作为队列或者栈来使用。publicclassLinkedList<E>......
  • JSON 序列化 属性名 大写变成小写 保持不变 newsoft.json system.text.json
    JSON序列化属性名由大写变成小写的问题在ASP.NET中,默认情况下,JSON序列化会将属性名转换为小写(camelcase)以匹配JSON的约定。如果您希望保留C#的命名约定(即属性名的大小写不变),您需要更改默认的JSON序列化器。System.Text.Json使用System.Text.Json(推荐):在Startup.c......
  • Web Component addEventListener .bind(this)的函数带参数引起的报错
     一开始这样写:  this.shadowRoot.querySelector('.prev').addEventListener('click',this.moveSlide(1).bind(this));报错:UncaughtTypeError:Cannotreadpropertiesofundefined(reading'bind')    以为是前面的DOM获取不对,但是怎么改都不对,网上查询后......
  • lazarus数据序列为JSON
    uses  DataSet.Serialize, fpjson;varobj:tjsonobject;procedureTForm1.Button1Click(Sender:TObject);beginuniquery1.Close;uniquery1.SQL.clear;uniquery1.sql.Add('selecttop2*fromtunit');uniquery1.Open;memo1.text:=uniquery1......