经典 dp 问题,用 dp [i] 表示前 i+1 个数的最长上升子序列,也就是以 ai 为末尾的最长上升子序列长度,要注意的是 dp 初始化应该是 1 而不是 0,因为对于每个数其本身就是一个长度为 1 的最长上升子序列
O ($n^2$) 的做法
- class Solution {
- public int lengthOfLIS(int[] nums) {
- int n = nums.length;
- int [] dp = new int[1000000];
- int res = 0;
- for(int i = 0;i < n;i++) {
- dp[i] = 1;
- for(int j = 0;j < i;j++)
- if(nums[j] < nums[i])
- dp[i] = Math.max(dp[i],dp[j] + 1);
- res = Math.max(res,dp[i]);
- }
- return res;
- }
- }
O ($nlogn$) 的做法
定义 d [k]:长度为 k 的上升子序列的最末元素,若有多个长度为 k 的上升子序列,则记录最小的那个最末元素 (d 中元素是单调递增的)
首先 len = 1,d [1] = a [1],然后对 a [i]:若 a [i]>d [len],那么 len++,d [len] = a [i];
否则,我们要从 d [1] 到 d [len-1] 中找到一个 j,满足 d [j-1]<a [i]<d [j],则根据 d 的定义,我们需要更新长度为 j 的上升子序列的最末元素(使之为最小的)即 d [j] = a [i]; 最终答案就是 len。利用 d 的单调性,在查找 j 的时候可以二分查找,从而时间复杂度为 $nlogn$
假设存在一个序列 d [1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的 LIS 长度为 5, 我们定义一个序列 B,然后令 i = 1 to 9 逐个考察这个序列
首先,把 d [1] 有序地放到 B 里,令 B [1] = 2,就是说当只有 1 一个数字 2 的时候,长度为 1 的 LIS 的最小末尾是 2。这时 Len=1
然后,把 d [2] 有序地放到 B 里,令 B [1] = 1,就是说长度为 1 的 LIS 的最小末尾是 1,d [1]=2 已经没用了,很容易理解吧。这时 Len=1
接着,d [3] = 5,d [3]>B [1],所以令 B [1+1]=B [2]=d [3]=5,就是说长度为 2 的 LIS 的最小末尾是 5,很容易理解吧。这时候 B [1..2] = 1, 5,Len=2
再来,d [4] = 3,它正好加在 1,5 之间,放在 1 的位置显然不合适,因为 1 小于 3,长度为 1 的 LIS 最小末尾应该是 1,这样很容易推知,长度为 2 的 LIS 最小末尾是 3,于是可以把 5 淘汰掉,这时候 B [1..2] = 1, 3,Len = 2
继续,d [5] = 6,它在 3 后面,因为 B [2] = 3, 而 6 在 3 后面,于是很容易可以推知 B [3] = 6, 这时 B [1..3] = 1, 3, 6,还是很容易理解吧?Len = 3 了噢
第 6 个,d [6] = 4,你看它在 3 和 6 之间,于是我们就可以把 6 替换掉,得到 B [3] = 4。B [1..3] = 1, 3, 4, Len 继续等于 3
第 7 个,d [7] = 8,它很大,比 4 大,嗯。于是 B [4] = 8。Len 变成 4 了
第 8 个,d [8] = 9,得到 B [5] = 9,嗯。Len 继续增大,到 5 了
最后一个,d [9] = 7,它在 B [3] = 4 和 B [4] = 8 之间,所以我们知道,最新的 B [4] =7,B [1..5] = 1, 3, 4, 7, 9,Len = 5。于是我们知道了 LIS 的长度为 5
!!!!! 注意。这个 1,3,4,7,9 不是 LIS,它只是存储的对应长度 LIS 的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个 d [9] = 7 更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把 8 更新到 d [5],9 更新到 d [6],得出 LIS 的长度为 6
然后应该发现一件事情了:在 B 中插入数据是有序的,而且是进行替换而不需要挪动 —— 也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到 O (logn)~ 于是算法的时间复杂度就降低到了 O (nlogn)~!
- #include<iostream>
- #include<cstring>
- using namespace std;
- int a[1000];
- int dp[1000];
- int BinarySearch(int x, int len) {//二分查找dp[]里面第一个大于等于x的数
- int left = 1, right = len, mid;
- while(left <= right) {
- mid = right + (left - right) / 2;
- if(x == dp[mid])
- return mid;
- else if(x > dp[mid])
- left = mid + 1;
- else if(x < dp[mid])
- right = mid - 1;
- }
- return left;
- }
- int main() {
- int N;
- cin >> N;
- memset(dp, 0, sizeof(dp));
- for(int i = 1; i <= N; i++)
- cin >> a[i];
- dp[1] = a[1];
- int len = 1;//当前已经求出来的最长序列长度
- int j;//dp[]的下标
- for(int i = 2; i <= N; i++) {
- if(a[i] > dp[len])//如果a[i]>dp[len] len +1
- j = ++len;
- else//反之, 更新j
- j = BinarySearch(a[i], len);
- dp[j] = a[i];//把a[i]更新入dp[]数组
- }
- cout<<len<<endl;
- return 0;
- }