zoukankan      html  css  js  c++  java
  • 最长上升子序列

    1、问题描述

    给定一个无序的整数数组,找到其中最长上升子序列的长度。如:[5, 3, 4, 8, 6, 7] 返回 4。

    2、算法分析

    面对这个问题,首先要定义一个"状态"来代表它的子问题, 并且找到它的解。

    注意,大部分情况下,某个状态只与它前面出现的状态有关,而独立于后面的状态。

    假如考虑求 A[1], A[2], ..., A[i], i < N 的最长非降子序列的长度,缩小问题规模,让 i = 1, 2, 3... 来分析,然后定义 d(i)表示前 i 个数中以 A[i] 结尾的最长非降子序列的长度。

    这个 d(i) 就是我们要找的状态。 如果我们把 d(1) 到 d(N) 都计算出来,那么最终我们要找的答案就是这里面最大的那个。 状态找到了,下一步找出状态转移方程。

    以上面的例子来方便理解如何找到状态转移方程的,N 个数的序列是:

    5  3  4  8  6  7

    根据上面找到的状态,可以得到:

    • i = 1 的 LIS 长度 d(1) = 1, d[] = {5}
    • i = 2 的 LIS 长度 d(2) = 1, d[] = {3}
    • i = 3 的 LIS 长度 d(3) = d(2) + 1 = 2, d[] = {3, 4}
    • i = 4 的 LIS 长度 d(4) = max{ d(1), d(2), d(3) } + 1 = 3, d[] = {3, 4, 8}

    状态转移方程已经很明显了,如果已经求出了 d(1) 到 d(i-1), 那么 d(i) 可以用下面的状态转移方程得到:

    d(i) = max{ 1, d(j) + 1 }, 其中 j < i, A[j] <= A[i]
    

    想要求 d(i),就把 i 前面的各个子序列中, 最后一个数不大于 A[i] 的序列长度加 1,然后取出最大的长度即为 d(i)。 当然了,有可能 i 前面的各个子序列中最后一个数都大于 A[i],那么 d(i) = 1, 即它自身成为一个长度为 1 的子序列。

    分析完了,上图。

    3、复杂度分析

    时间复杂度:O(n2)

    空间复杂度:O(n)

    4、代码实现

    int lengthOfLIS(int* nums, int numsSize) {
        
        if (numsSize == 0)  return 0;
        
        int *d = (int *)malloc(sizeof(int) * numsSize);
        int len = 1;
        
        for(int i = 0; i < numsSize; ++i){
            
            d[i] = 1;
            
            for(int j = 0; j < i; ++j)
                // 如果当前的数值 A[i] 大于 它之前的数值 A[j] && 最长的段
                if(nums[j] < nums[i] && d[j] + 1 > d[i])
                    d[i] = d[j] + 1;
            
            if(d[i] > len) len = d[i];
        }
        
        free(d);
        
        return len;
    }
    
    int main()
    {
        int A[] = {
            5, 3, 4, 8, 6, 7
        };
        printf("%d", lengthOfLIS(A, 6));
        return 0;
    }
    

    5、进阶:O(nlogn)算法

    假设序列 d[9] = { 2, 1, 5, 3, 6, 4, 8, 9, 7 }。

    定义一个序列 B,令 i = 1 to 9 循环考察 d 数组。用一个变量 Len 来记录最大的递增长度。注意:B 的索引从 1 开始

    ①、把 d[0] 有序地放到 B 里,令 B[1] = 2,即当只有一个数字 2 的时候,Len = 1 的 LIS 的末尾最大值是 2。
    
    ②、把 d[2] 有序地放到 B 里,令 B[1] = 1,即 Len = 1 的 LIS 的末尾最大值是 1,d[1] = 2 已经没用了,因为 2 > 1。
    
    ③、d[3] = 5,因为 d[3] > B[1],所以令 B[1+1] = B[2] = d[3] = 5,即 Len = 2 的 LIS 的最小末尾是 5,这时 B[] = { 1, 5 }。
    
    ④、d[4] = 3,B[1] < d[3] < B[2],放在 B[1] 的位置显然不合适,因为 1 < 3,不应该替换到小的值,而应该淘汰掉大的值,因为这样容易产生更长的序列,所以 Len = 2 的 LIS 最小末尾是 3,将 5 淘汰掉,这时 B[] = { 1, 3 }。
    
    ⑤、d[5] = 6,因为 d[5] > B[2],所以令 B[2+1] = B[3] = d[5] = 6,即 Len = 3 的 LIS 的最小末尾是 6,这时 B[] = { 1, 3, 6 }。
    
    ⑥、d[6] = 4,3 < d[6] < 6,于是把 6 替换掉,这时 Len = 3, B[] = { 1, 3, 4 }。
    
    ⑦、d[7] = 8,d[7] > B[3],将 8 追加到 B 数组末尾,这时 Len = 4, B[] = { 1, 3, 4, 8 }。
    
    ⑧、d[8] = 9,d[8] > B[4],将 9 追加到 B 数组末尾,这时 Len = 5, B[] = { 1, 3, 4, 8, 9 }。
    
    ⑨、d[9] = 7,B[3]=4 < d[9] < B[4]=8,所以最新的 B[4] = 7,这时 Len = 5, B[] = 1, 3, 4, 7, 9。
    

    注意:{ 1, 3, 4, 7, 9 } 不是 LIS,它只是存储的对应长度 LIS 的最小末尾。

    有了这个末尾,就可以一个一个地插入数据。虽然最后一个 d[9] = 7 更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9(d[11] = { 2, 1, 5, 3, 6, 4, 8, 9, 7, 8, 9 }),那么继续执行下去,8 更新到 d[5],9 更新到 d[6],得出 LIS 的长度为 6,B[] = { 1, 3, 4, 7, 8, 9 }。

    在 B 中插入数据是有序的,而且是进行替换而不需要挪动,所以可以利用二分查找,将每一个数字的插入时间优化到 O(logn),于是算法的时间复杂度就降低到了 O(nlogn)。

    // 在非递减序列 [left, right](闭区间)上二分查找第一个大于等于 key 的位置,如果都小于 key,就返回 left+1
    int upper_bound(int B[], int left, int right, int key)
    {
        int mid;
        // 将 key 插入到数组末尾
        if (B[right] < key)
            return right + 1;
        // num[left] ≤ key < nums[right] 之后 left 将大于 right,循环结束
        while (left < right) {
            mid = (left + right) / 2;
            if (B[mid] < key) {
                left = mid + 1;
            }
            else {
                right = mid;
            }
        }
        return left;
    }
    
    int lengthOfLIS(int* nums, int numsSize)
    {
        if (numsSize < 2) return numsSize;
        int* B = (int *)malloc(sizeof(int) * (numsSize + 1));
        B[0] = 0;  // 无意义
        B[1] = nums[0];  // 从 1 开始是为了让 len、pos 不需要 -1 或 +1
        int len = 1;
        for (int i = 1; i < numsSize; i++) {
            // 找到插入位置
            int pos = upper_bound(B, 1, len, nums[i]);
            B[pos] = nums[i];
            // 打印 B 数组,看看每次循环的变化,B[0] 无意义
            printf("%d    ", pos);
            for (int k = 0; k <= pos; k++) {
                printf("%d", B[k]);
            }
            printf("
    ");
            if (len < pos) {
                len = pos;
            }
        }
        return len;
    }
    
    int main()
    {
        int A[] = { 2, 1, 5, 3, 6, 4, 8, 9, 7, 8, 9 };
        printf("%d", lengthOfLIS(A, 11));
        
        return 0;
    }
    
  • 相关阅读:
    Java HashMap的原理、扩容机制、以及性能
    Istanbul BFT共识算法解读
    golang中slice的扩容机制
    Use the "Enclave Signing Tool" to sign enclave files
    以太坊椭圆曲线Specp256k1通过消息的hash和签名(对消息的hash的签名)来恢复出公钥和计算r值
    Intel SGX SDK toolkits
    Intel SGX Edger8r
    Intel SGX C++ library
    SGX Makefile学习笔记
    在ubuntu中安装gem5
  • 原文地址:https://www.cnblogs.com/dins/p/longest-ascending-subsequence.html
Copyright © 2011-2022 走看看