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;
    }
    
  • 相关阅读:
    Code Forces 650 C Table Compression(并查集)
    Code Forces 645B Mischievous Mess Makers
    POJ 3735 Training little cats(矩阵快速幂)
    POJ 3233 Matrix Power Series(矩阵快速幂)
    PAT 1026 Table Tennis (30)
    ZOJ 3609 Modular Inverse
    Java实现 LeetCode 746 使用最小花费爬楼梯(递推)
    Java实现 LeetCode 745 前缀和后缀搜索(使用Hash代替字典树)
    Java实现 LeetCode 745 前缀和后缀搜索(使用Hash代替字典树)
    Java实现 LeetCode 745 前缀和后缀搜索(使用Hash代替字典树)
  • 原文地址:https://www.cnblogs.com/dins/p/longest-ascending-subsequence.html
Copyright © 2011-2022 走看看