- 题意】
- 给你n(1e3)个数,每个数都在[1,1e9]范围。
- 然后让你保持数的顺序不变,选出长度恰好为m(1<=m<=n)的单调上升子序列。
- 问你有多少种选择方案
- 【类型】
- DP
- 最长上升子序列
- 离散化+树状数组
- 【分析】
- 这道题一开始我是以O(nlogn)最长上升子序列的插入法做。但是很难实现。卡了一段时间。
- 后来结合n只有1000的情况,我们应该设想到O(n^2)的做法。
- 题目涉及到选择,我们不妨回归到一个很经典的DP模型——
- 用f[i][j]表示前i个数中选择了j个数的上升子序列的方案数。
- 这个状态的一个好处是,它把这个上升自序列的长度也包含了,就是j。
- 还有一个性质,我们如何查看一个数能否接在前面的序列后?其实只要知道之前上升子序列的最后一位就可以进行判定了。
- 鉴于这个,我们修改下f[i][j]的含义——
- f[i][j]表示前i个数中选择了j个数,且最后一个数严格为a[i]时的上升子序列方案数。
- 这样就有状态转移f[i][j]=∑f[p][j-1],p∈[1,i-1]且a[p]<a[i]
- 因为我们可以严格保持i的升序,所以就只需要找到所有长度为j-1且尾节点a[p]<a[i]的子序列个数即可。
- 且因为for i for j已经使得复杂度变成O(n^2),所以这个操作要在log(n)级别的时间内完成。
- 而且这个操作设计到动态操作,于是我们想到树状数组(线段树也可)。
- 这样这道题的做法就很清晰了。
- 1,离散化
- 2,状态转移
- for(int i=1;i<=n;i++)
- {
- //其实这个j的顺序并不重要,因为我们更新的尾节点长度为a[i],自己是无法延伸而来的
- for(int j=top;j>=1;j--)
- {
- if(j==1)f[i][j]=1;
- else add(f[i][j],cnt(j-1,a[i]-1));
- add(j,a[i],f[i][j]);
- }
- add(ans,f[i][m]);
- }
- 【时间复杂度&&优化】
- O(n^2 logn)
- 取模还是很消耗时间的233
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<map>
#include<set>
using namespace std;
const int mod = 1000000007;
const int N = 1111;
map<int,int>f;
set<int>s;
set<int>::iterator it;
int dp[N][N],c[N][N],a[N];
int cnt;
void add(int i,int j,int num)
{
while(i<=cnt) {
c[i][j]+=num;
if(c[i][j]>=mod) c[i][j]%=mod;
i+=i&(-i);
}
}
int sum(int i,int j)
{
int res=0;
while(i>0) {
res+=c[i][j];
if(res>=mod) res%=mod;
i-=i&(-i);
}
return res;
}
int main()
{
int T,n,m,i,j,pre;
scanf("%d",&T);
for(int _=1;_<=T;_++) {
s.clear();
f.clear();
memset(dp,0,sizeof(dp));
memset(c,0,sizeof(c));
scanf("%d%d",&n,&m);
for(i=1;i<=n;i++) {
scanf("%d",&a[i]);
s.insert(a[i]);
}
cnt=0;
for(it=s.begin();it!=s.end();it++) {
if(!f[*it]) f[*it]=++cnt;
}
for(i=1;i<=n;i++) {
for(j=1;j<=m && j<=i;j++) {
pre=f[a[i]];
if(j==1) dp[i][j]=1;
else {
dp[i][j]=sum(pre-1,j-1);
if(dp[i][j]>=mod) dp[i][j]%=mod;
}
// printf("dp[%d][%d]=%d\n",i,j,dp[i][j]);
add(pre,j,dp[i][j]);
}
}
int res=0;
for(i=m;i<=n;i++) {
res+=dp[i][m];
if(res>=mod) res%=mod;
}
printf("Case #%d: %d\n",_,res);
}
return 0;
}