时间复杂度:(nlogn)
这种优化的主要思想是贪心,对于一串最大上升子序列,其结尾元素越小对答案的贡献越大,也就是说,其结尾元素越小就越有可能在其末尾接上新的数。
具体做法的话,我们会使用一个dp数组存储长度为 i 的lis的结尾的最小值。对于dp数组,我们分别对每一个元素进行判断,若当前元素大于dp数组结尾的元素,我们就将该元素插入dp数组的末尾,数组长度加一;若当前元素小于dp数组的结尾元素,我们就使用二分查找,在已有的dp数组中寻找到第一个大于等于当前元素的数,使用当前元素将其替换掉,这样可以保证dp数组里存储lis的数据尽可能的小,这样就越有利于保证越可能在lis末尾插入新元素。
例:
对于一个数组arr: arr[] = 2 5 18 3 4 9 15 dp[1] = 2; 5大于2,所以dp[2] = 5 18大于5,所以dp[3] = 18 3小于18,所以使用二分查找,查找到的角标是2,所以更新dp[2] = 3 4小于18,所以使用二分查找,查找到的角标是3,所以更新dp[3] = 4 9大于4,所以dp[4] = 9 15大于9,所以dp[5] = 15
所以该数组的lis就是5
这里值得注意的一件事是:通过这种方法找到的lis并不一定是真正的lis,只是能够保证其长度和真正的lis相同,因为这种操作的贪心思想会导致数组中较小的数会把dp数组中较大的数替换掉。
如: 1 3 4 6 7 2
其真正的lis应该是:1 3 4 6 7,但通过这种方法找出来的lis却是:1 2 4 6 7。
代码模板:
# include<iostream>
# include<string.h>
# include<algorithm>
using namespace std;
const int maxn = 5e5 + 5;
int dp[maxn];
int arr[maxn];
int main(){
int n;
int len = 1;
int ans = 1;
cin >> n;
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= n; i++){
cin >> arr[i];
}
dp[1] = arr[1];
for(int i = 2; i <= n; i++){
if(arr[i] > dp[len])
dp[++len] = arr[i];
else if(arr[i] < dp[len]){
int temp = lower_bound(dp + 1, dp + len + 1, arr[i]) - dp;
dp[temp] = arr[i];
}
}
cout << len << endl;
return 0;
}