MENU

LeetCode300. 最长上升子序列

July 14, 2018 • Read: 3147 • LeetCode阅读设置

经典 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;
  • }
Last Modified: May 16, 2021
Archives Tip
QR Code for this page
Tipping QR Code