LIS的nlog(n)算法

本文介绍了一种求解最长上升子序列(LIS)问题的有效算法,该算法的时间复杂度为O(n log n),通过使用二分查找来更新候选子序列,实现了高效的动态规划解决方案。
#include <iostream>
#include <cstring>
#include <cstdio>
#define maxn 1000
using namespace std;


int ans[maxn],a[maxn],len;


int Search(int data){//我们要找到的是最小的那个>=mid的ans的位置
    int l=0,r=len,mid;
    while(l<r){
        mid=(l+r)/2;
        if (ans[mid]>=data) r=mid;
        else l=mid+1;
    }
    return l;
}


int main(){
    int n;
    scanf("%d",&n);
    for (int k=1;k<=n;k++)
        scanf("%d",&a[k]);


    ans[1]=a[1];len=1;
    for (int k=2;k<=n;k++){
        if (a[k]>ans[len]){
            len++;
            ans[len]=a[k];
        }
        else {
            int kk=Search(a[k]);
            ans[kk]=a[k];
        }
    }


    printf("%d\n",len);
}
//这个模板是求最长上升子序列(LIS)的长度的哈
//先解释下什么是最长上升子序列
//比如我有一个序列 2 1 5 3 6 4 8 9 7
//那么最长上升子序列就是1 5 4 8 9,要是这还不明白是什么意思那就别丢人了,退役吧
//dp求法很好想这里就不说了
//这里讲一个时间复杂度 nlogn的做法
//就是我开一个数组ans,ans[k]就是长度为k的LIS的最小末尾是什么然后开个len记录一下最大的k是多少就好
//那么我们现在就一个一个往里面放数字
//如果这个数字a>ans【len】那么len++,ans【len】=a即可
//否则我们就要找到这个a能加到前面的哪个ans中这个是必要的,因为前面的ans其实是对后面的ans有影响的
//这个我们等下就可以看出来
//具体怎么加,二分就好啦。
<think>我们要求解最长递增子序列(Longest Increasing Subsequence, LIS)问题,并且要求时间复杂度为O(n log n)。 传统动态规划方法的时间复杂度是O(n^2),我们可以通过结合动态规划和二分查找的方法将时间复杂度优化到O(n log n)。 思路: 1. 我们维护一个数组`tail`,其中`tail[i]`表示长度为i+1的所有递增子序列中,结尾元素最小的那个子序列的结尾元素值。 注意:这个数组是单调递增的,因此我们可以使用二分查找。 2. 我们遍历原数组,对于每个元素,我们试图在`tail`数组中找到第一个大于等于它的元素的位置: - 如果找到这样的位置(设为pos),说明我们可以用当前元素替换掉`tail[pos]`(因为当前元素比`tail[pos]`小,这样可以使得长度为pos+1的子序列的结尾更小,有利于后面得到更长的子序列) - 如果没有找到(即当前元素比`tail`中所有元素都大),那么我们就在`tail`数组后面追加当前元素,这表示我们找到了一个更长的递增子序列。 3. 最终,`tail`数组的长度就是最长递增子序列的长度。 注意:这个算法只能得到最长递增子序列的长度,而不能直接得到具体的子序列(但可以通过额外记录来得到,不过题目只要求长度)。 步骤: 初始化一个数组`tail`,长度为0。 遍历原数组`nums`的每个元素`num`: 在`tail`数组中二分查找第一个大于等于`num`的元素的位置`pos`。 如果`pos`等于当前`tail`的长度(即在所有元素之后),则将`num`添加到`tail`末尾。 否则,将`tail[pos]`的值更新为`num`。 最后,`tail`的长度即为最长递增子序列的长度。 为什么是严格递增?因为我们在查找时使用“大于等于”,当遇到相等的元素时,我们也会进行替换(因为替换相等的元素并不会改变长度,但是会使得后续更有可能形成更长的序列,而且题目要求严格递增,所以相等元素不会出现在同一个子序列中,因此我们替换掉相等的元素也是安全的,实际上在严格递增的情况下,相等元素不会扩展序列长度,但替换可以保持`tail`数组的正确性)。 但是注意:严格递增的情况下,我们遇到相等的元素时不应该扩展序列长度,但是需要替换掉`tail`中第一个大于等于它的元素(也就是它自己或者比它大的第一个元素)。如果遇到相等的元素,我们替换掉相等元素,这样做的目的是为了在后续的遍历中,如果出现比当前元素稍大的数,可以形成更长的序列。例如,原序列为[1, 3, 5, 7, 3, 4]: 第一个3出现时,tail数组为[1,3,5,7];当遇到第二个3时,我们在tail中找到第一个大于等于3的元素就是3(位置1),然后将其替换为3(实际上没变,但逻辑上我们更新了该位置的最小结尾值)。然后接着遍历到4,我们在tail中找第一个大于等于4的是5(位置2),将5替换为4,此时tail变成[1,3,4,7],长度为4,但实际上的最长递增子序列可以是[1,3,5,7]或[1,3,4,7]都是长度为4。 然而,实际上第二个3出现时,我们不应该将3加入到tail中,因为严格递增要求后面的数必须大于前面的数。但是我们的做法是更新tail中与3相等的位置,这样并不会改变tail的长度,也不会影响后续的扩展(因为4大于3,所以可以替换5,从而在之后可能形成更长的序列)。 因此,算法如下(使用C语言实现): 注意:二分查找的实现,我们使用左闭右开区间。 例子:nums = [10, 9, 2, 5, 3, 7, 101, 18] 期望结果:4 ([2,5,7,101] 或 [2,3,7,101]) 步骤: 初始化tail[0] = nums[0] -> tail = [10] 9: 在[10]中找第一个>=9 -> 10,替换10 -> tail=[9] 2: 在[9]中找第一个>=2 -> 9,替换9 -> tail=[2] 5: 在[2]中找第一个>=5 -> 没找到(5>2),所以添加到tail -> tail=[2,5] 3: 在[2,5]中找第一个>=3 -> 5,替换5 -> tail=[2,3] 7: 在[2,3]中找第一个>=7 -> 没找到(7>3),所以添加到tail -> tail=[2,3,7] 101: 添加到tail -> [2,3,7,101] 18: 在[2,3,7,101]中找第一个>=18 -> 101,替换101 -> [2,3,7,18] 最终长度为4。 所以算法正确。 C语言实现: 1. 使用动态数组tail,并记录其当前长度len。 2. 遍历数组,对每个元素进行二分查找。 二分查找函数:在数组tail[0..len-1]中查找第一个大于等于x的元素位置。 如果找到,返回下标;如果没找到(即x比所有元素都大),返回len(此时需要添加到末尾)。 注意:C语言中数组下标从0开始。 代码实现: 初始化:tail = (int *)malloc(n * sizeof(int)); len=0; 遍历每个元素: 如果len==0,则直接加入tail,len=1。 否则,二分查找tail[0..len-1]中第一个大于等于当前元素x的位置pos(即左边界)。 如果pos==len,则tail[len++]=x; 否则,tail[pos]=x; 最后,长度为len。 时间复杂度:O(n log n),因为每个元素进行一次二分查找(O(log n)),总共n个元素。 下面我们编写代码: </think> 下面我将详细介绍如何使用 C 语言实现时间复杂度为 $O(n \log n)$ 的最长严格递增子序列(LIS算法,并分析其原理和实现细节。 ### 算法原理 该算法基于 **贪心 + 二分查找** 策略,核心思路是: 1. 维护一个数组 `tail`,其中 `tail[i]` 表示长度为 $i+1$ 的所有递增子序列中最小结尾元素的值 2. 通过二分查找确定每个新元素在 `tail` 数组中的位置 3. 不断更新 `tail` 数组使其保持最小结尾元素特性 4. 最终 `tail` 数组的长度即为 LIS 的长度 时间复杂度为 $O(n \log n)$:遍历数组 $O(n)$,每个元素二分查找 $O(\log n)$[^1][^2]。 ### C 语言实现 ```c #include <stdio.h> #include <stdlib.h> int binary_search(int* arr, int len, int target) { int left = 0, right = len; while (left < right) { int mid = left + (right - left) / 2; if (arr[mid] < target) { left = mid + 1; } else { right = mid; } } return left; } int lengthOfLIS(int* nums, int numsSize) { if (numsSize == 0) return 0; int* tail = (int*)malloc(numsSize * sizeof(int)); int len = 0; // 当前LIS长度 for (int i = 0; i < numsSize; i++) { // 二分查找插入位置 int pos = binary_search(tail, len, nums[i]); if (pos == len) { // 当前元素比所有结尾都大,扩展序列 tail[len++] = nums[i]; } else { // 替换现有位置的元素 tail[pos] = nums[i]; } } free(tail); return len; } int main() { int nums[] = {10, 9, 2, 5, 3, 7, 101, 18}; int n = sizeof(nums) / sizeof(nums[0]); int result = lengthOfLIS(nums, n); printf("最长递增子序列长度: %d\n", result); // 输出: 4 return 0; } ``` ### 关键步骤解析 1. **初始化**: - `tail` 数组存储可能的递增序列结尾元素 - `len` 记录当前找到的 LIS 长度 2. **遍历处理每个元素**: - 使用二分查找在 `tail[0..len-1]` 中找到第一个 $\geq$ 当前元素的位置 - 若位置 `pos == len`,说明当前元素可扩展序列 - 否则替换 `tail[pos]` 为当前元素,保持最小结尾特性 3. **示例分析**(输入 `[10,9,2,5,3,7,101,18]`): | 元素 | tail 数组 | len | 操作 | |------|---------------|-----|------------------| | 10 | [10] | 1 | 直接添加 | | 9 | [9] | 1 | 替换 10→9 | | 2 | [2] | 1 | 替换 9→2 | | 5 | [2,5] | 2 | 扩展 | | 3 | [2,3] | 2 | 替换 5→3 | | 7 | [2,3,7] | 3 | 扩展 | | 101 | [2,3,7,101] | 4 | 扩展 | | 18 | [2,3,7,18] | 4 | 替换 101→18 | 4. **为什么正确**: - 维护的 `tail` 数组始终保持最小结尾元素 - 替换操作不影响已有序列长度,但为后续扩展创造条件 - 最终 `len` 即为 LIS 长度(实际序列可通过回溯获取) ### 复杂度分析 - **时间复杂度**:$O(n \log n)$ - $n$ 次循环,每次二分查找 $O(\log n)$ - **空间复杂度**:$O(n)$ - `tail` 数组额外空间[^1] 此算法效率远优于 $O(n^2)$ 的动态规划解法(当 $n=10^6$ 时,$n \log n \approx 2 \times 10^7$,而 $n^2=10^{12}$)[^3]。 ### 扩展应用 该算法可扩展解决: 1. 最长非递减子序列(允许相等) 2. 二维偏序问题(如信封嵌套) 3. 最长递增子序列的计数问题
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值