学习笔记:最长上升子序列

给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式
第一行包含整数N。
第二行包含N个整数,表示完整序列。

输出格式
输出一个整数,表示最大长度。

数据范围
1≤N≤1000, −1e9≤数列中的数≤1e9


dp状态定义:
f[i]表示以第i个数a[i]结尾的最长上升子序列
对于f[i],表示的状态集合中,最长长上升子序列的最后一个一定是a[i],然后枚举i之前的数a[j](j<i),在所有的j中取一个最大值:f[i]=max(f[i],f[j]+1)


如果只要求输出长度:

const int N = 1e3+5;

int n,a[N],f[N];

int main()
{
    
    
	cin>>n;
	for(int i=1;i<=n;i++)	cin>>a[i];
	
	for(int i=1;i<=n;i++)
	{
    
    
		f[i]=1; //一开始只有自己,初始化为1
		for(int j=1;j<i;j++) //从1枚举到i-1
			if(a[j]<a[i])	f[i]=max(f[i],f[j]+1); //在所有满足的j中取一个最大值
	}
	int res=0;
	for(int i=1;i<=n;i++)	res=max(res,f[i]);
	
	cout<<res<<endl;
	return 0;
}

如果要求打印出最长上升子序列,可以开一个pre数组,记录状态i是由哪一个状态j转移而来的

const int N = 1e3+5;

int n,a[N],f[N],pre[N];

int main()
{
    
    
	cin>>n;
	for(int i=1;i<=n;i++)	cin>>a[i];
	
	for(int i=1;i<=n;i++)
	{
    
    
		f[i]=1; pre[i]=0;
		for(int j=1;j<i;j++)
			if(a[j]<a[i])
			{
    
    
			    if(f[i]<f[j]+1)
			    {
    
    
			        f[i]=f[j]+1;
			        pre[i]=j;
			    }
			}
	}
	int k=1;
	for(int i=1;i<=n;i++)
	    if(f[k]<f[i])  k=i;
	cout<<f[k]<<endl;
	
	//打印最长方案
	while(k) //这里是倒着打印出来的,实际操作过程中可以先存下来,在正序输出
	{
    
    
	    cout<<a[k]<<" ";
	    k=pre[k];
	}
	
	return 0;
}

最长上升子序列 加强版:
给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式
第一行包含整数N。

第二行包含N个整数,表示完整序列。

输出格式
输出一个整数,表示最大长度。

数据范围
1≤N≤1e5, −1e9≤数列中的数≤1e9


由于n是1e5,O(n2)做法会超时 ,需要一个O(n logn )的做法

观察前面的代码发现,时耗主要有两个:
一是枚举每一个数a[i]。
二是对于每一数a[i]找到前面比它小的所有的数a[j],在所有的f[j]中取一个最大值。

首先,第一步枚举a[i]肯定无法在优化了,因为每一个数都必须去枚举。
思考如何优化第二步的过程?


我们可以对所有的上升子序列分一个类,长度为1的一类,为2的一类……
然后开一个b数组维护每一类的上升子序列结尾的最小值。
b[k]的含义是:在所有长度为k的上升子序列中,序列结尾数的最小值。

因为上升子序列的结尾越小,它后面可以衔接上的数的范围就越大。
比如说,有长度为4的上升子序列x1和x2,x1以6结尾,x2以8结尾,那么我们只需要记住x1的6就好了,因为所有8可以衔接上的数,6都可以,而且6还可以接上7,8.


在来看b数组有什么特点?


我们可以证明:
b数组一定是一个严格单调上升的数组,因为b中维护的是每一类子序列结尾的最小值。

对于b[i]和b[i+1],也就是长度为i的上升子序列结尾的最小值和长度为i+1的上升子序列结尾的最小值,b[i+1]一定大于b[i]

反证法:
假设b[i+1]<=b[i], 那么该长度为i+1的子序列的第i个数一定小于b[i](因为是上升子序列),这与b[i]是所有长度为i的上升子序列的结尾数的最小值矛盾了


所以,对于每一个数a[i],我们不用去枚举它前面的每一个数,只需要去b数组中找到比它小的最大的一个数在哪一个位置,比如说在b[k]这个位置,那么f[i]=k+1,至于这个找数的过程就可以用二分来解决(因为b数组严格单调递增)

const int N = 1e5+5, inf = 0x3f3f3f3f;

int n,a[N],f[N];
vector<int> b(N + 1, inf); //初始化b数组,一开始所有数都为无穷大

int main()
{
    
    
	IOS;
	cin >> n;
	for(int i = 1; i <= n; i++)	cin >> a[i];
		
	for(int i = 1; i <= n; i++)
	{
    
    
		f[i]=1; b[1]=min(b[1],a[i]); //更新长度为1的上升子序列结尾的最小值
		
		int l = 0, r = n + 1; //二分找比a[i]小的最大的一个数
		while(l+1 != r)
		{
    
    
			int mid = l + r >> 1;
			if(b[mid] < a[i])	l = mid;
			else	r = mid;
		}
		f[i] = l + 1; 
		b[l+1] = min(b[l+1], a[i]); //更新长度为l+1的上升子序列结尾的最小值
	}
	
	int res = 0;
	for(int i = 1; i <= n; i++)
		res = max(res, f[i]);
		
	cout << res << endl;

	return 0;
}

简洁版:

const int N = 1e5+5;

int n, a[N];
int len, b[N];

int main()
{
    
    
	IOS;
	cin >> n;
	for(int i = 1; i <= n; i++)	cin >> a[i];
	
	//memset(b, 0x3f, sizeof b); 不需要初始化为无穷大
	b[0] = -inf; //哨兵,处理边界 
	for(int i = 1; i <= n; i++)
	{
    
    
		int l = -1, r = len+1; //[0,len]
		while(l+1 != r)
		{
    
    
			int mid = l + r >> 1;
			if(b[mid] < a[i])	l = mid;
			else	r = mid;
		}
		len = max(len, l+1);
		b[l+1] =a[i];
		//b[l+1] = min(b[l+1], a[i]),并不用比较在取最小值,因为a[i]一定比b[l+1]小
	}
		
	cout << len << endl;

	return 0;
}

猜你喜欢

转载自blog.csdn.net/m0_50815157/article/details/113873825
今日推荐