最长上升子序列O(nlogn)算法核心思想简述

【题目描述】

给定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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值