HDU 5542 n个数中恰好长度为m的单升子序列数 (DP+树状数组优化)

73 篇文章 0 订阅
11 篇文章 0 订阅
该博客介绍了如何利用动态规划(DP)和树状数组来解决一个关于找出长度为m的单调上升子序列的问题。在n个数的范围内,通过离散化和树状数组,实现了O(n^2 logn)的时间复杂度求解方案,详细阐述了状态转移方程和优化过程。
摘要由CSDN通过智能技术生成


  1. 题意】 
  2. 给你n(1e3)个数,每个数都在[1,1e9]范围。 
  3. 然后让你保持数的顺序不变,选出长度恰好为m(1<=m<=n)的单调上升子序列。 
  4. 问你有多少种选择方案 
  5.  
  6. 【类型】 
  7. DP 
  8. 最长上升子序列 
  9. 离散化+树状数组 
  10.  
  11. 【分析】 
  12. 这道题一开始我是以O(nlogn)最长上升子序列的插入法做。但是很难实现。卡了一段时间。 
  13.  
  14. 后来结合n只有1000的情况,我们应该设想到O(n^2)的做法。 
  15. 题目涉及到选择,我们不妨回归到一个很经典的DP模型—— 
  16. 用f[i][j]表示前i个数中选择了j个数的上升子序列的方案数。 
  17. 这个状态的一个好处是,它把这个上升自序列的长度也包含了,就是j。 
  18. 还有一个性质,我们如何查看一个数能否接在前面的序列后?其实只要知道之前上升子序列的最后一位就可以进行判定了。 
  19. 鉴于这个,我们修改下f[i][j]的含义—— 
  20. f[i][j]表示前i个数中选择了j个数,且最后一个数严格为a[i]时的上升子序列方案数。 
  21. 这样就有状态转移f[i][j]=∑f[p][j-1],p∈[1,i-1]且a[p]<a[i] 
  22.  
  23. 因为我们可以严格保持i的升序,所以就只需要找到所有长度为j-1且尾节点a[p]<a[i]的子序列个数即可。 
  24. 且因为for i for j已经使得复杂度变成O(n^2),所以这个操作要在log(n)级别的时间内完成。 
  25. 而且这个操作设计到动态操作,于是我们想到树状数组(线段树也可)。 
  26.  
  27. 这样这道题的做法就很清晰了。 
  28. 1,离散化 
  29. 2,状态转移 
  30. for(int i=1;i<=n;i++) 
  31. { 
  32.     //其实这个j的顺序并不重要,因为我们更新的尾节点长度为a[i],自己是无法延伸而来的 
  33.     for(int j=top;j>=1;j--) 
  34.     { 
  35.         if(j==1)f[i][j]=1; 
  36.         else add(f[i][j],cnt(j-1,a[i]-1)); 
  37.         add(j,a[i],f[i][j]); 
  38.     } 
  39.     add(ans,f[i][m]); 
  40. } 
  41.  
  42. 【时间复杂度&&优化】 
  43. O(n^2 logn) 
  44. 取模还是很消耗时间的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;
}















  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值