最长上升子序列(LIS)详解
最长上升子序列(Longest Increasing Subsequence)解法和优化
学习动态规划的入门知识碰到的经典问题,这里记录一些我的理解
最长上升子序列(LIS)
两个概念
首先,需要明确两个概念:
子串(Substring):对应字符串从左到右的顺序摘取的片段,要求必须连续(不能有间隔字符),如:$abcde$种$bcd$就是一个子串;
子序列(Subsequence):对应字符串从左到右的顺序摘取片段,要求可以有间隔字符,如:$abcde$中$ace$是一个子序列。
因此,一定有如下说法:子串一定是子序列,而子序列不一定是子串,这很好理解。
问题描述
给定一个长为n的数列,求出这个数列的最长上升子序列的长度。
Sample Input:
1
2
5
4 2 3 1 5Sample Output:
13
数据范围:$1\leq n\leq 1000; 0\leq a_i \leq 1e6$
一般解法
具体思路:
定义状态: $dp[i]$表示以$a_i$为末尾的最长上升子序列的长度;
状态转移: 一种是$dp[i]$本身,另一种则是在遍历过程中出现的以$a_j(a_j<a_i且j<i)$为结尾的LIS末尾加上$a_i$得到的子序列;
方程如下:
时间复杂度$O(n^2)$,可以开始搞了。
AC代码($O(n^2)$)
1 |
|
优化
上述方法看起来很巧妙,并且感觉已经足够完美了,那么能不能再完美一些(时间复杂度再低一些)呢?
答案是可以。
我们定义一个新的状态:$dp[i]$表示所有数组成的LIS中,所有长度为$i$的LIS中的末尾元素最小值(orz真的很难理解);
状态定义好了,我们需要做的就是遍历一遍$a_1$到$a_n$,并更新这个$dp$数组,具体操作如下:
首先,$dp$数组的长度为0,我命名为len=0;
出现$a_i>dp[len]$时,不管三七二十一,直接加在$dp[len]$后面,并使得len++;
出现$a_i<dp[len]$时,我们需要把这个$a_i$替换掉$dp$数组从左到右第一个大于等于$a_i$的元素(注意:此时dp数组长度不变),因为我们要求最优,因而就需要$dp$数组里面元素尽可能小,这样才有更大的可能性再去往后加数。
具体思路是这样,是不是一脸懵逼?
没事,我们来手动模拟一下:
以数组(4,2,3,1,5)为例:
首先,len = 0,dp = {0,0,0,0,0},
开始:
指向第一个元素,4 > 0,因此加进dp里,dp = {4,0,0,0,0}len = 1;
指向第二个元素,2 < 4,因此替换掉4,dp = {2,0,0,0,0}len = 1;
指向第三个元素,3 > 2,因此加进dp里,dp = {2,3,0,0,0}len = 2;
指向第四个元素,1 > 3,因此替换掉2,dp = {1,3,0,0,0}len=2;
指向第五个元素,5 > 3,因此加进dp里,dp = {1,3,5,0,0}len = 3;
结束。
因此 len= 3
这样好理解多了吧?
但是我们可以发现,最后的dp数组它不是正确的答案(正确答案是2,3,5),但这并不影响len长度的真确性,为什么呢?因为我们在操作$a_i<dp[len]$的时候,只是把更优的数(更小的数)替换掉了第一个比他大的数,而长度并没有改变,这种求最优的过程并没有影响到LIS的长度,而这个dp数组则代表了一种最可能性,代表每一步都是最优的,意思是如果后面还有数的话,这个答案就可以再接着往下更新,可能又有新的替换。(这里可能表述得不太清晰,还是需要自己多悟呀)
可有人会问了:这确实是个新思路,但遍历一遍a数组需要n复杂度,插入到dp也需要n复杂度,这不还是$O(n^2)$复杂度吗?
话是这么说,但我们的插入操作可以利用二分查找(lower_bound或者你手打)来优化呀!
为什么可以呢?答:因为dp数组一定是单调的(依据状态转移过程不难得出)。
AC代码($O(nlogn)$)
1 |
|
- 第一次学这个东西可能确实需要点时间来理解,我会努力加深印象的。
以上。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!