【题目描述】
给定N个数,求这N个数的最长上升子序列的长度。
【样例输入】
7
2 5 3 4 1 7 6
【样例输出】
4
下面记录本人对 “贪心 + 二分” 【O(nlogn)】 算法的一些理解。
0 本文思路:
1)介绍解题过程
2)分析解法可行性
不想听废话,请直接移步 第四部分 :“4 操作的意义”,演示该操作的效果与意义。
1 核心思想:
(贪心)对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。
2 操作过程:
现创建一个新数组 b[ ], 用来存放:考虑到第 i 个元素时,我们所获得的当前LIS。(b 下标从 1 开始)
例如:
2 5 3 4 1 7 6
考虑到a[1], b = [2]
考虑到a[5], b = [2,5]
考虑到a[3], b = [2,3]
考虑到a[4], b = [2,3,4]
…
对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。
因此,对于每一个a[ i ],如果a[ i ] > b[len(b)],就把 a [ i ]接到 b 后面,即 b[++len(b)] = a [ i ] (易得这是求严格递增)。
如果 a[ i ] <= b[len(b)] ,就用 a [ i ] 取更新 b 数组。
具体方法是,在 b 数组中找到 “第一个大于等于” a [ i ]的元素 b [ j ],用a [ i ]去更新 b[ j ]。
如果从头到尾扫一遍 b数组的话,时间复杂度仍是O(n^2)。我们注意到 b数组内部一定是单调不降的(因为 b 数组是我们获得的,当前的LIS),所有我们可以二分 b数组,找出第一个大于等于a[ i ]的元素。二分一次 b数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。
3 分析:
为什么可以这样做?
把下标大的元素,放到了下标比它小的元素的前面,不会有问题吗?
情况1:a[ i ] > b[len(b)]
显然,直接加到 b 数组后面,LIS 长度 +1.
情况2:a[i] <= b[len(b)]
按照上述做法,应在 b 数组中找到,第一个不小于它的元素,并用a[i] 替换之。
例如:
假设有
b = [2,4,6,8],
假设当前 a[i] = 5,按条件在 b 中找到元素 ‘4’ ,并替换之,得:
b = [2,4,5,8]
证明替换的可行性:
可以看到,替换之后,只有被替换的位置的元素值改变了,剩余的并没有受到影响,并且,b 依旧保持了递增排序,且长度没有改变,这是最重要的一点。
我们要求的是 最长上升子序列 的最大长度 => 即 b 的最大长度。
在遍历 a 数组的过程中, b 表示 当前获得的 LIS, b 的长度即为当前的 最长上升子序列的长度。既然替换之后,b 的长度没有变,=> 当前最长上升子序列的长度没有变 => 没有改变子问题的结果(虽然b 保存的不是真正的 LIS 序列,但我们最后要得到的是一个 长度,b 变化只要不改变长度,也就不会改变答案)。
b 数组发生替换之后,其又有了新意义: b 的长度表示,曾经一定有一个合法的序列 X = {x1,x2…xn},使得 遍历到当前位置时的LIS 的长度 len(b) = len(X)。也就是说,b 的长度,一定是由一个合法的状态推过来的。
以上证明了,进行替换不影响最终结果。
可这样替换有什么意义呢?
4 操作的意义
接着上述例子
b = [2,4,5,8,10]
现在假设 a = […6,7,8…]
①开始判断 a 中的元素 ‘6’, 按照上述做法,6 < b[len(b) = 5] = 10 ,(10是插入 b 的门槛);比 6 大的有: 8 、10,则替换 8,得:
b = [2,4,5,6,10]
②开始判断 a 中的元素 ‘7’, 按照上述做法,7 < b[len(b) = 5] = 10 ,(10是插入的门槛),比 6 大的有: 10,则替换 10,得:
b = [2,4,5,6,7]
③开始判断 a 中的元素 ‘8’, 按照上述做法,8 > b[len(b) = 5] = 7 ,(7是插入的门槛),则插入,得:
b = [2,4,5,6,7,8]
注意,通过 ‘6’ 的替换,本来比 ‘7’大的有 ‘8’
‘10’ ,替换成 ‘6’ 后,比 ‘7’大的只有 '10’了,那么 ‘7’ 替换 ‘10’后,插入 b 数组的门槛由 ‘10’ 变成了 ‘7’。如果前面不替换 ‘6’,则 ‘7’ 只能替换 ‘8’,不能替换 ‘10’,则没有降低门槛。
再来看,如果在①②中,不做替换:
①开始判断 a 中的元素 ‘6’, 按照上述做法,6 < b[len(b) = 5] = 10 ,无法插入,保持不变,得
b’ = [2,4,5,8]
②开始判断 a 中的元素 ‘7’, 按照上述做法,7 < b[len(b) = 5] = 10 ,无法插入,保持不变,得
b’= [2,4,5,7]
②开始判断 a 中的元素 ‘8’, 按照上述做法,7 < b[len(b) = 5] = 10 ,无法插入,保持不变,得
b’= [2,4,5,7]
显然 len(b’) < len(b)
通过观察可以发现,经过我们的替换,将更小的数替换掉 b 数组中第一个不小于它的数(即替换掉不改变 b 递增性质的数),使得,在判断后续元素过程中,本来无法加入 b 数组的元素,经前面的替换操作后,满足了插入的条件,可以插入。因为插入可以使得 b 数组的长度增长,而替换不改变 b 的长度,所以我们要尽可能的创造出后续元素可插入的条件。
那么这个替换操作,其实就是创造这个条件。
用新数去更新前边的元素,这个元素可能不是最优解的一部分,但是它可以使得后面还未加入的、比较小的数更有可能进入这个数组
b。通俗地来说,作为门槛,他本来要大于当前序列的最后一个数才能加进去;就是如果我太大了,我就乖乖呆在末尾;如果前面有一个数比我大,也就是我比你好,既然我在你后面也就是我们两者只能选其一,那我只好把你替换掉了。虽然我这临时临头换的不一定最合适,但是对于后面还有很多的人等着排进来的情况下,我给他们创造了更多机会,使得这个序列的最后一个数有可能变小,让更多的人进来。
int a[Max]; //a存放原始数组,
int dp[Max]; //maxLen存放以该元素为中点的最长子序列的长度
int b[Max];
int blen;
int main() {
int n; //输入数字个数
cin>>n;
for(int i = 1; i <= n; ++i) {
cin>>a[i];
dp[i] = 1;
} //初始化
// 复杂度 O(n`2)
for(int i = 1; i <= n; ++i)
for(int j = 1; j < i ; ++j) {
if(a[j] < a[i])
dp[i] = max(dp[j] + 1,dp[i]);
}
cout<< * max_element(dp+1,dp+1+n)<<endl;
// 复杂度 O(nlogn)
for(int i = 1;i <= n;++i){
if(a[i] > b[blen]) b[++blen] = a[i];
else{
int p = lower_bound(b+1,b+1+n,a[i]) - b;
b[p] = a[i];
}
}
cout<<blen;
}
参考博客:https://www.cnblogs.com/frankchenfu/p/7107019.html
https://blog.csdn.net/lxt_Lucia/article/details/81206439