暴力解法
如果直接采用暴力解法,以一个指针i来维护一个窗口,然后统计出该窗口的所有序列, 计算最长的长度,但是在i进行移动时指到的值不一定会接到找到前一个窗口的子序列后面,也就是如果统计每个窗口的最长序列不一定对整个序列的最长序列有用,所以纯暴力法只能dfs搜索统计最大的深度。
#include<iostream>
#include<vector>
using namespace std;
const int N=1010;
int a[N];
int n;
int dfs(int u)
{
//找到给定起始位置的子序列,然后返回最长的值
int res=1;
for(int i=u+1;i<n;i++)
{
//看后面的值能否接到u位置的值的后面
if(a[i]>a[u])res=max(res,1+dfs(i));
}
return res; //返回该位置的最长上升子序列的长度
}
int main()
{
cin>>n;
for(int i=0;i<n;i++)cin>>a[i];
int res=0;
for(int i=0;i<n;i++)
{
res=max(res,dfs(i));
}
cout<<res<<endl;
return 0;
}
dfs都是一条道走到黑的,所以每次只能搜一条路径,在主函数中需要使用循环开辟多条路径,当然也可以通过剪枝剪掉不需要开辟的路径
dp解法
状态表示为以第i个数结尾的最长上升子序列的长度,更新i时,如果a[i]能接到前i-1个数中,就计算接入后最大的长度。
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int f[N],a[N];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;++i)scanf("%d",&a[i]);
for(int i=1;i<=n;++i)
{
f[i]=1;
//1-i-1的状态
for(int j=1;j<i;++j)
{
if(a[j]<a[i])f[i]=max(f[i],f[j]+1);
}
}
int res=0;
for(int i=1;i<=n;++i)res=max(res,f[i]);
cout<<res<<endl;
return 0;
}
贪心解法
我们发现在状态更新的时候,每一步其实倾向于选择结尾最小的数,因为这样后面可接入的数范围就大一些,加入我们更新到i,前面有长度为1的序列多个,长度为2的序列多个,长度为3的序列多个,按照dp的想法,是将该数接到长度为3的序列中,然后更新i+1个数,万一第i+1个数接入不了i个数的后面,只能接到长度为3的序列后面,只能重新挑选所有长度为3的序列,这样更新i+2个数,也可能重新挑选长度为4的序列。所以每一个长度的序列其实按照贪心的想法只需要保留一个就可,也就是保留末尾数最小的。如果接入的了末尾数最小的,自然也就能增加长度。
所以,使用一个g数组保持一下不同上升子序列长度的最小值。可以证明,该数组是单调增的
证明:
假设长度为6的最后一个值是小于长度为5的最后一个值,则由于长度为5的最后一个值是长度为5的最后一个值中最小的,则长度为6的最后一个值比倒数第二个值大,而长度为5的最后一个值应该比长度为6的倒数第一个值大,则矛盾,所以g数组是单调增加的。
由于g数组是单调增加的,所以每次更新的时候我们能够二分找到最大的比待更新值小的位置在其后面加上待更新的值。
#include<iostream>
using namespace std;
const int N=100010;
int a[N];
int q[N];//存储上升子序列中的最小结尾
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
q[0]=2e-9;
int len=0;
for(int i=1;i<=n;i++)
{
int l=0,r=len;
while(l<r)
{
int mid = (l+r+1)/2;
if(q[mid]<a[i])l=mid;
else r=mid-1;
}
len =max(len,r+1);
q[r+1]=a[i];
}
cout<<len<<endl;
return 0;
}
二分法:
如果从右往左选,则需要l=mid,然后mid需要多上+1,因为要找到最右边
如果从左往右选,则需要r=mid, 然后mid不需要+1,因为要找到最左边
这里并不是那种选出所有长度然后减少点思想,而是直接挑出合适的序列。