最长上升子序列 详解(B3637 题解)

目录

题目描述

解题思路

        一.枚举

        二.动态规划

                ①寻找方法

                        ②实现思路

           三、贪心 + 二分

                        ①普通解法

                        ②lower_bound 函数

                四、树状数组


题目描述

这是一个简单的动规板子题。

给出一个由 n(n≤5000) 个不超过 10^6 的正整数组成的序列。请输出这个序列的最长上升子序列的长度。

最长上升子序列是指,从原序列中按顺序取出一些数字排在一起,这些数字是逐渐增大的。

输入格式

第一行,一个整数 n,表示序列长度。

第二行有 n 个整数,表示这个序列。

输出格式

一个整数表示答案。

输入输出样例

输入 #1

6
1 2 4 1 3 4

输出 #1

4

说明/提示

分别取出 1、2、3、4 即可。

解题思路

        一.枚举

                这道题的第一种思路就是枚举。我们一个个枚举出所有的情况,然后再找出最大值就行了。不过这样的话,代码的时间复杂度会非常高。

        二.动态规划

                ①寻找方法

                我们先来举个例子:

                        3

                        1,3

                        2,3

                        1,2,3

                我们现在看看这几串数,首先,它们的结尾都是 3 。其次,如果我们想再向 3 后面继续添数,我们要保证这个数必须大于 3 ,对吧?那么,我们添的这个数,与 3 前面的数有关吗(除了第一串数)?没有!

                换句话说,我们给下一位添的数只与现在子序列结尾的数有关。那么我们例子里所有的子序列,都可以归为“以 3 为结尾的上升子序列”。

                同时,在样例中, 3 后面只有一个数。如果我们把 4 补充在所有子序列的结尾,我们发现,最长的上升子序列还是第 4 个子序列。如果最后一位是一样的,那大概率后面添的数也是一样的。我们假设现在还可以添上 x 个数,那么最长的上升子序列还是第 4 个子序列。

                所以,如果一个子序列以 i 结尾,那么只有最长的那个才有可能通过添数成为答案,其余的子序列就不需要枚举了。

                上面说了这么多,我们得出了两点结论:

                        1.下一位添的数只取决于目前子序列的最后一位。

                        2.我们只要对拥有相同结尾的子序列中最长的一个子序列去进行操作。

                大家有没有发现?第一点结论符合动态规划中的“无后效性”的要求;第二点结论符合动态规划中的“最优子结构”的要求。所以,这道题我们可以用动态规划去做!

                        ②实现思路

                        那么,既然这道题可以用动态规划做,我们就先定义一下 f(i)

                        f(i):以数组中第 i 个数结尾的最长上升子序列的长度。

                        定义完了f(i),我们来考虑如何写出状态转移方程。我们用 f[j] 的值去计算 f[i] ,其中 j 要比 i 小。考虑 f[i] 是以数组中第 i 个数 a[i] 结尾的,我们只需要关心它能接到前面哪些子序列的后面。有两种方法:

                                (1):谁都不接,自己成为一个长度为 1 的上升子序列, f[i]=1 。

                                (2):对于所有 i 前面的位置 j ,且满足 a[j]<a[i] 的, f[i]=f[j]+1 ,即在以 a[j] 结尾的最长上升子序列的基础上,再增加一个自己带来的长度 1 。

                        为了使 f[i] 的值最大,应该对于所有的 j ,取 f[j]+1 的最大值。

                        也就是:

                        f[i] = max { f[i] , f[j] + 1 } ( 1 <= j < i , a[j] < a[i] )

                        但是要注意一点,我们最后要的答案是所有 f[i] 的最大值,因为我们不能确定整个序列的最长上升子序列是以哪个数结尾的,所以需要枚举一边,取最大值。

                        代码:

#include<bits/stdc++.h>
using namespace std;
int f[10010];
int a[10010];
int main(){
    int n;
    cin>>n;
	int ans=0;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	for(int i=1;i<=n;i++)
	{
    	f[i]=1;
		for(int j=1;j<i;j++)
		{
			if(a[j]<a[i])
			{
				f[i]=max(f[i],f[j]+1);
			}
		}
	}
	for(int i=1;i<=n;i++)
	{
		ans=max(ans,f[i]);
	}
	cout<<ans;
    return 0;
}

           三、贪心 + 二分

                        虽然动态规划的做法已经能AC这道题,不过遇到一些比较大的数据,它还是会超时。所以我们还需要更快的方法。

                        ①普通解法

                        我们用 f[i] 表示长度为 i 的上升子序列中最小的结尾。注意,这个 f[i] 的定义与前面的算法不同, i 的含义不是以原来数组中第 i 个数结尾,而是不论以谁结尾,上升子序列的长度如果是 i 的话就把信息记录在 f[i] 里边。同时,如果有多个长度都是 i 的上升子序列,我们选择所有子序列中结尾最小的记录下来。因为拥有最小结尾的上升子序列,更有可能被后面的数字接上,成为更长的上升子序列。

                        在初始状态,考虑数组 a 的第一个数字,这时,有唯一的长度为 1 的上升子序列,它的结尾是 a[1] 。我们可以举个例子, a 数组有 7 个数,分别是“1 7 3 5 8 4 9”。

下标1234567
a 数组1735849
f 数组1

                        接下来,我们一个数一个数地看下去。下一个数是 a[2] ,它等于 7 。

它可以接在 1 后面,成为长度为 2 的上升子序列,结尾是 7 。之前并没有长度是 2 的上升子序列, f[2] 是空的,所以我们直接在 f[2] 添上 7 。

下标1234567
a 数组1735849
f 数组17

                        下个数字是 3 ,目前长度是 1 的子序列是以 1 结尾的,长度是 2 的子序列是以 7 结尾的, 3 肯定不能添在 7 后面,只能添在 1 后面,成为一个长度为 2 的上升子序列,结尾是 3 ,比之前的 f[2] ( 7)小,修改 f[2]=3 。

下标1234567
a 数组1735849
f 数组13

                        下个数字是 5 ,它可以添在长度为 2 ,结尾是 3 的子序列后面,成为长度为 3 ,结尾是 5 的上升子序列。

下标1234567
a 数组1735849
f 数组135

                        下个数字是 8 ,它可以添在长度为 3 ,结尾是 5 的子序列后面,成为长度为 4 ,结尾是 8 的上升子序列。

下标1234567
a 数组1735849
f 数组1358

                        下个数字是 4 ,它不能添在 8 、 5 的后面,能添在 3 的后面。所以我们把 f[3] 替换成 4 。 

下标1234567
a 数组1735849
f 数组1348

                        最后一个数是 9 ,它可以添在长度为 4 ,结尾是 8 的子序列后面,成为长度为 5 ,结尾是 9 的上升子序列。

下标1234567
a 数组1735849
f 数组13489

                        最后的答案就是,最长上升子序列的长度是 5 ,最小以 9 结尾。

                        到现在,我们可以总结出一个算法。一个个考虑  a 数组中的数字,对于当前的数字 a[i] ,首先看看它是否比 f 数组的最后一个数字大,有两种情况:

                                (1)它比 f 数组的最后一个数字大,我们可以把它接在 f 数组的最后一个数字后面,我们就得到了一个更长的子序列,以 a[i] 结尾。

                                (2)它不比 f 数组的最后一个数字大,那么我们就在 f 数组中,从右往左找到最靠右的、比 a[i] 小的数字,接到它的后面。也相当于把 f 数组最靠左的第一个大于等于 a[i] 的数字修改为 a[i] 。

                        我们来看看这种方法的时间复杂度。对于每个数字 a[i] ,它要么接在 f 数组的末尾,要么去遍历 f 数组寻找最靠左的第一个大于等于 a[i] 的数字进行替换。这种方法在最坏情况下的复杂度是O(n),因为 i 也要遍历,所以总的复杂度是O(n^{2}),看似和前面的做法没什么不同。

                        但是!

                        我们通过上面的表格可以看出来,实际上, f 数组是单调不减的!所以,“遍历 f 数组寻找最靠左的第一个大于等于 a[i] 的数字进行替换”这一操作,其实是不需要从左往右遍历的,可以在数组上进行二分查找。这样每次查找的复杂度就下降到了O(logn),总时间复杂度为O(nlogn)

                        代码:

#include<bits/stdc++.h>
using namespace std;
int a[100010];
int f[100010];
int n,R,l,r,mid,ans;
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	f[0]=0;
	R=0;
	for(int i=1;i<=n;i++)
	{
		if(a[i]>f[R])
		{
			f[R+1]=a[i];
			R++;
		}
		else
		{
			l=0;
			r=R;
			while(l<=r)
			{
				mid=(l+r)/2;
				if(f[mid]<a[i])
				{
					l=mid+1;
				}
				else
				{
					ans=mid;
					r=mid-1;
				}
			}
			f[ans]=a[i];
		}
	}
	int t=0;
	for(int i=1;i<=n;i++)
	{
		if(f[i])
		{
			t++;
		}
	}
	cout<<t;
	return 0;
}

                        ②lower_bound 函数

                        在C++中,有一个可以代替二分的,就是lower_bound 函数。下面,我们用 lower_bound 去代替二分。

                        代码:

#include<bits/stdc++.h>
using namespace std;
int a[100010];
int f[100010];
int n,ans=0;
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	memset(f,0x7f,sizeof(f));
	for(int i=1;i<=n;i++)
	{
		int x=lower_bound(f,f+n,a[i])-f;
		ans=max(ans,x+1);
		f[x]=a[i];
	}
	cout<<ans;
	return 0;
}

                四、树状数组

                        我们是否能用某些数据结构帮助我们降低时间复杂度呢?我们想一想,原序列 a 数组的每个元素,是不是都有一个权值和一个下标?最长上升子序列其实就是在求最多有多少的元素,它们的权值和下标都单调递增。

                        所以我们可以将 a 数组的每一个元素都记下它的下标,然后按照它们的权值进行从小到大的排序。接着我们枚举 a 数组,我们的转移就变成了从之前的标号比它小的状态转移过来。这时,我们只需要建立一个树状数组,枚举 a 数组时按元素的序号找到它之前序号比它小的长度最大的状态去更新,然后把它也加进树状数组中。

                        也就是,先把 a 数组的每一个元素都记下它的下标,同时从小到大排序。然后枚举 a 数组,每次用编号小于等于 a[i] 编号的元素的最长上升子序列长度+1来更新答案,同时把编号大于等于A[ i ]编号元素的最长上升子序列长度+1。

                        代码:
 

#include<bits/stdc++.h>
using namespace std;
struct node
{
	int val,id;
}a[100010];
int n;
int f[100010];
bool cmp(node x,node y)
{
	if(x.val!=y.val)
	{
		return x.val<y.val;
	}
	return x.id>y.id;
}
int answer(int x)
{
	int ans=0;
	for(;x>=1;x-=x&-x)
	{
		ans=max(ans,f[x]);
	}
	return ans;
}
void add(int x,int y)
{
	for(;x<=n;x+=x&-x)
	{
		f[x]=max(f[x],y);
	}
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i].val;
		a[i].id=i;
	}
	sort(a+1,a+n+1,cmp);
	for(int i=1;i<=n;i++)
	{
		add(a[i].id,answer(a[i].id)+1);
	}
	int ans=answer(n);
	cout<<ans;
	return 0;
}
  • 16
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值