最长上升子序列是我在计蒜客中的蓝桥杯模拟题中遇到的。
当事看到题目自己想了想,和最长相同子序列差不多。然后自己写了两种解法 ,
一种是采用递归式的方法(时间复杂度估计2^n),
另一种事采用了动态规划的方式(时间复杂度n*n)。
后来得知还存在一种时间复杂度更加优秀的二分法(时间复杂度n * logn), 于是看了下面的博客。
参考博客
这篇博客中二分法说的还算清楚(反正我一遍就了解得差不多),但还是有一些缺憾(为什么要用二分查找来经行优化没有说明)。
后来我自己研究了一小会有所明悟,故此总结。
下面就直接进入二分法:
假设有一段序列A:2 7 8 4 9 5 3
再构建一个数组B:用来查询最优结果。
对于LIS来,说它的结果序列必定是一个由小到大的递增序列。(最重要的是有序的)。
于是对与数组B来说那么它也必定是一个有序的。
我们不妨每次选取最大的放在后面, 小的放在前面来经行查询。
B[0]初始化为最小值。
然后经行3轮选取,后就有了下面的数组B:2 7 8
在后面继续经行选取时会遇到4,7是大于4的第一个数,于是可以将7直接替换成4。
注意:这里的替换并不会影响最长上升子序列的长度(因为替换并没有改变序列的相对位置)。
接下来继续后面的选取:
看到这里是不是觉得so easy 啊!
是的,只需要扫描一遍数组A, 然后每次在B中找到需要替换, 或者增加的位置就行。
这里就需要二分查找法。找到在序列B中第一个大于或等于当前关注点的位置。如果末尾值小于当前关注点(那么就可以直接添加到末尾)。
最后B数组的长度就是LIS的结果了。
最后 show my code。
/*找到第一个比队列中大的位置*/
int binary_search_upper(int* A, int left, int right, int key){
if(A[right] < key){
return right;
}
if(A[right] == key){
return right;
}
int mid;
while(right > left){
mid = left + (right - left) / 2;
if(A[mid] <= key){
left = mid + 1;
}else{
right = mid;
}
}
return left;
}
int LIS_nlogn(int* A, int n){
int* B = new int[n + 1];
int len = 0;
int next;
B[0] = M;
for(int i = 0; i < n; i++){
next = binary_search_upper(B, 0, len - 1, A[i]);// 找到在序列B中第一个大于或等于当前关注点的位置
B[next] = A[i];
if(next >= len - 1){// 记录长度, 以免后面还需要扫描一遍查询长度
len = next + 1; // 这里记录的时长度, next是下标,所以需要加减1
}
}
delete []B;
return len;
}
end
DP:
// 最长上升序列
// D 序列代表1-n的最长子序列为多少
// A 表示待求序列,n表示序列长度
int LIS_DP(int*A, int* D ,int n){
int d[n + 2];
memset(d, 0, sizeof(d));
for(int i = 1; i <= n; i++){
d[i] = 1;
for(int j = i - 1; j >= 1; j--){
if(A[i] > A[j]){
if(d[j] + 1 > d[i]){
d[i] = d[j] + 1;
}
}
}
if(D[i - 1] < d[i]) D[i] = d[i];// 记录最大值
else D[i] = D[i - 1];
}
return D[n];
}
// 最长降序序列
int LIS_DP2(int*A, int* D ,int n){
int d[n + 2];
memset(d, 0, sizeof(d));
for(int i = n; i >= 1; i--){
d[i] = 1;
for(int j = i + 1; j <= n; j++){
if(A[i] > A[j]){
if(d[j] + 1> d[i]){
d[i] = d[j] + 1;
}
}
}
if(D[i + 1] < d[i]) D[i] = d[i];// 记录最大值
else D[i] = D[i + 1];
}
return D[1];
}