【DP】【LIS】最长递增子序列 - O(N^2)方法 + O(NlogN)方法

分三部分:
1、 O ( N 2 ) O(N^2) O(N2) 方法
2、LIS、LDS 应用
3、 O ( N l o g N ) O(NlogN) O(NlogN)方法
  LeetCode - 300. Longest Increasing Subsequence

Given an unsorted array of integers, find the length of longest increasing subsequence(LIS).

Input: [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The LIS is [2,3,7,101], therefore the length is 4.

There may be more than one LIS combination, it is only necessary for you to return the length

Could you improve it to O(n log n) time complexity?

一、 O ( N 2 ) O(N^2) O(N2) 方法

  这道题很容易想到 O(N^2) 的方法,就是 i 从 1 到 len - 1,每次更新以 i 为止的序列中,LIS长度是多少(也就是这个 LIS 一定是以 i 这个为止为结尾)。可以AC,AC代码如下:

int lengthOfLIS(vector<int>& nums) {
    const int len = nums.size();
    if(len <= 1) return len;
    vector<int> dp(len, 1);
    int max_len = 1;
    for(int i = 1; i < len; ++i) {
        for(int j = 0; j < i; ++j)
            if(nums[i] > nums[j])	// >= 的话,就是相等也算
                dp[i] = max(dp[i], dp[j] + 1);
        max_len = max(max_len, dp[i]);
    }
    return max_len;
}
二、LIS、LDS 应用

  首先 LIS 是最长递增子序列,如果想要算上相等的,也就是最长非递减子序列,上边的 > 改成 >= 就行了,如果是 LDS,即最长递减子序列,> 改成 < 就行了。
  比如一个数组,先定义绝对距离:为两个相邻数之间的差的绝对值的总和,比如 [3, 2, 5] 这个数组,绝对距离就是 1 + 3 = 4,然后定义移动次数:为将一个数插入到数组任意位置,其他数不动为一次移动(类似链表的插入),给一个数组,问达到最小的绝对距离,最少的移动次数是多少?

样例输入:4 2 7 6
样例输出:2 (改为 2 4 6 7)

  第一步肯定是想想最小的绝对距离是什么情况。最小情况这个数组一定是有序的,升序降序绝对距离一样。然后考虑怎么达到有序,最少几步,就可以想到,算出这个数组当前状态最长递增子序列长度,以及最长递减子序列长度(相同的数,也算)。然后用这两个数中间大的,用数组长度减一下就行了,即(下边代码中的 LIS 和 LDS 都是有的等于号的!因为相同也算有序):

int solution(vector < int > arr) {
	int lis = lengthOfLIS(arr), lds = lengthOfLDS(arr);
	return arr.size() - max(lis, lds);
}
三、 O ( N l o g N ) O(NlogN) O(NlogN)方法

  题目的 Note 里说了,能不能想一种 O ( N l o g N ) O(NlogN) O(NlogN) 的方法。是可以的,我们考虑 [ 3, 2, 5 ] 这个例子,当我们遍历到 2 的时候,发现前边 LIS 长度是 1,2 这个位置自己的 LIS 就是 1,然后 2 又比 3 小,那么 3 这个数,其实没有任何用了,之后也不会有任何用,因为比 3 大 的都比 2 大,比 2 大的又不一定比 3 大,所以 2 更好用
  我们用一个数组 tail[i] 存储,长度为 i 的 LIS 的结尾元素是多少
  从前往后遍历,一共分为 3 种情况:
1、如果 nums[i] 比最短的(长度为1的)LIS的 end element 还要小,说明可以替换这个 LIS 为 nums[i],就像上边说的 [ 3, 2, 5 ] 的例子,2 比 3 小,所以 2 替换 3 成为新的长度为 1 的 LIS 的结尾元素;
2、如果 nums[i] 比最长的 LIS 的结尾元素还要大,说明可以扩展这个当前最长的 LIS,成为新的 LIS;
3、如果 num[i] 在中间,找到结尾元素小于 nums[i] 的 LIS 里边最长的,比如长度是 4,然后扩展这个 LIS,并且扔掉原来的长度为 5 的 LIS,因为原来的 LIS 的结尾元素比 nums[i] 大,也就是条件更严格。
  中心思想就是维护一个 tail 数组,tail[i] 存储长度为 i + 1 的 LIS 的结尾元素,如果发现能够将结尾元素变小且不影响长度,就将其变小
  就拿 LeetCode 的例子来说,[ 10, 9, 2, 5, 3, 7, 101, 18 ],记录结尾元素的数组名叫 tail,初始值全为 0:

nums 数组情况操作LIS 数组tail 数组
10初始化初始化时直接 tail[0] = nums[0][10]tail[0] = 10
9case 1替换 10[9]tail[0] = 9
2case 1替换 9[2]tail[0] = 2
5case 2复制并扩充 [2][2 ]
[2, 5]
tail[0] = 2
tail[1] = 5
3case 3复制并扩充 [2],并删除 [2,5][2 ]
[2, 3]
tail[0] = 2
tail[1] = 3
7case 2复制并扩充 [2,3][2]
[2, 3]
[2, 3 ,7]
tail[0] = 2
tail[1] = 3
tail[2] = 7
101case 2复制并扩充 [2,3,7][2]
[2, 3]
[2, 3, 7]
[2, 3, 7, 101]
tail[0] = 2
tail[1] = 3
tail[2] = 7
tail[3] = 101
18case 3复制并扩充 [2,3,7],并删除[2,3,7,101][2]
[2, 3]
[2, 3, 7]
[2, 3, 7, 18]
tail[0] = 2
tail[1] = 3
tail[2] = 7
tail[3] = 18

  最后最长的 LIS 数组的长度或者 tail 数组的大小就是结果。看懂了上边的三个情况,代码就很好写了(上边的 LIS 数组只是用来方便理解的,实际并不需要)。其实如果 nums[i] 这个数已经在数组中,就直接continue就行了。上边的情况1和3其实也可以合并,tail 数组从前往后遍历,找到第一个>= nums[i] 大的数,替换掉它,就可以了,因为 tail 数组一定是有序的,找到第一个>= nums[i] 大的数,直接用 C++ STL 的 lower_bound() 就行:

int lengthOfLIS(vector<int>& nums) {
    if (nums.size() == 0) return 0; 
    // tail[i] 记录 i+1 长度的 LIS 的结尾是几
    vector<int> tail(nums.size(), 0);
    tail[0] = nums[0];
    int len = 1; // always points empty slot in tail 

    for (size_t i = 1; i < nums.size(); ++i) {
        if(nums[i] > tail[len - 1]) {
            tail[len++] = nums[i];
            continue;
        }
        if(find(tail.begin(), tail.begin() + len, nums[i]) != tail.begin() + len)
            continue;
        // 返回第一个 >= nums[i] 大的 iterator
        *lower_bound(tail.begin(), tail.begin() + len, nums[i]) = nums[i];
    }
    return len;
}

  这个算法,由于每次遍历使用的是 lower_bound(),它内部是是对有序数组进行二分搜索的,所以搜索 第一个 >= nums[i] 大的数的复杂度是 O ( l o g N ) O(logN) O(logN),所以整体算法复杂度是 O ( N l o g N ) O(NlogN) O(NlogN)
  STL 中 upper_bound 是找第一个 > x 的位置,lower_bound 是找第一个 >= x 的位置,如果想找 < 或者 <=,修改后边得到比较函数即可,就像 sort 一样。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值