最长上升子序列

最长上升子序列

1、\(n^2\) 做法

首先我们要知道,对于每一个元素来说,最长上升子序列就是其本身。那我们便可以维护一个 \(dp\) 数组,使得 \(dp[i]\) 表示以第 \(i\) 元素为结尾的最长上升子序列长度,那么对于每一个 \(dp[i]\) 而言,初始值即为 1;

那么 \(dp\) 数组怎么求呢?我们可以对于每一个 \(i\),枚举在 \(i\) 之前的每一个元素 \(j\),然后对于每一个 \(dp[j]\),如果元素 \(i\) 大于元素 \(j\),那么就可以考虑继承,而最优解的得出则是依靠对于每一个继承而来的 \(dp\) 值,取 \(max\)

for(int i=1;i<=n;i++) {
	dp[i]=1;//初始化 
	for(int j=1;j<i;j++){//枚举i之前的每一个j 
	//用if判断是否可以拼凑成上升子序列,如果是,则更新最优状态
		if(a[j]<a[i]) dp[i]=max(dp[i],dp[j]+1);
	}
}

最后,因为我们对于 \(dp\) 数组的定义是到 \(i\) 为止的最长上升子序列长度,所以我们最后对于整个序列,只需要输出 \(dp[n]\) 即可。

从这个题我们也不难看出,状态转移方程可以如此定义:

下一状态最优值=最优比较函数(已经记录的最优值,可以由先前状态得出的最优值)

——即动态规划具有 判断性继承思想

2、\(nlogn\) 做法

我们其实不难看出,对于 \(n^2\) 做法而言,其实就是暴力枚举:将每个状态都分别比较一遍。但其实有些没有必要的状态的枚举,导致浪费许多时间,当元素个数到了 \(10^4-10^5\) 以上时,就已经超时了。而此时,我们可以通过另一种动态规划的方式来降低时间复杂度:

将原来的 \(dp\) 数组的存储由数值换成该序列中,上升子序列长度为 \(i\) 的上升子序列,的最小末尾数值

这其实就是一种几近贪心的思想:我们当前的上升子序列长度如果已经确定,那么如果这种长度的子序列的结尾元素越小,后面的元素就可以更方便地加入到这条我们臆测的、可作为结果的上升子序列中。

#include<bits/stdc++.h>
using namespace std;
const int N=300010;
int a[N],n,m,ans; 
int dp[N]; 
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	memset(dp,0x3f,sizeof(dp));
	//初始值要设为INF
	/*原因很简单,每遇到一个新的元素时,就跟已经记录的f数组当前所记录的最长
	上升子序列的末尾元素相比较:如果小于此元素,那么就不断向前找,直到找到
	一个刚好比它大的元素,替换;反之如果大于,么填到末尾元素的下一个q,INF
    就是为了方便向后替换啊!*/ 
	dp[1]=a[1];
	ans=1;//通过记录dp数组的有效位数,求得个数 
	/*因为上文中所提到我们有可能要不断向前寻找,
	所以可以采用二分查找的策略,这便是将时间复杂
    度降成nlogn级别的关键因素。*/ 
	for(int i=2;i<=n;i++){
		//如果刚好大于末尾,暂时向后顺次填充
		if(a[i]>dp[ans]) {dp[++ans]=a[i];continue;}
		int l=1,r=ans,res=0;
		while(l<=r){
			int mid=(l+r)>>1;
			if(dp[mid]<a[i]){
				res=mid;
				l=mid+1;
			}else{ /*如果仍然小于之前所记录的最小末尾,那么不断
					向前寻找(因为是最长上升子序列,所以dp数组必
					然满足单调)*/
				r=mid-1;
			}
		}
		dp[res+1]=min(dp[res+1],a[i]);//更新最小末尾
	}
	cout<<ans;
	return 0;
}
posted @ 2022-10-06 16:35  「ycw123」  阅读(93)  评论(0编辑  收藏  举报