最长上升子序列

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("\n");
        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;
}
posted @ 2020-03-15 13:34  和风细羽  阅读(216)  评论(0编辑  收藏  举报