子序列的个数

子序列的定义:对于一个序列a=a[1],a[2],......a[n]。则非空序列a'=a[p1],a[p2]......a[pm]为a的一个子序列,其中1<=p1<p2<.....<pm<=n。

例如4,14,2,3和14,1,2,3都为4,13,14,1,2,3的子序列。对于给出序列a,有些子序列可能是相同的,这里只算做1个,请输出a的不同子序列的数量。由于答案比较大,输出Mod 10^9 + 7的结果即可。

 

输入

 

第1行:一个数N,表示序列的长度(1 <= N <= 100000)
第2 - N + 1行:序列中的元素(1 <= a[i] <= 100000)

 

输出

 

输出a的不同子序列的数量Mod 10^9 + 7。

输入示例

 

4
1
2
3
2

 

输出示例

 

13

 

解题思路:这道题其实可以适用动态规划来解,

严格来讲这个题不是一个dp的问题,因为它是一个数数(组合数学)的问题,不是一个优化的问题。我们来想想2^n个子序列是怎么来的——之前已经有2^(n-1)个了,再加上最后一个或者不加上最后一个,还有两种选择。


假设dp[i]表示前i项形成的子序列(含空)的个数。下标从1开始,初值是dp[0] = 1,对应代表空子序列。我们考虑第i项,如果所有的数都不相等,应该有dp[i] = dp[i – 1] * 2,其实就是考虑把第i个数放到最后或者不放到最后的情况。

 

然而,因为有可能有相同的数,我们假设第i个数出现之前最近的在j (j < i)位置也出现过,那么实际上我们这种简单*2会有重复,哪些子序列重复呢? 我们设数列为a, 并且下标从1开始。

 

原来恰好以第j个数结尾的那些被我们算了两次,因为以第j个数结尾可以换成以第i个数结尾是一样的。如何计算出这个数的个数呢? 其实这个数等于dp[j – 1],因为前面(j – 1)个数的子序列最后跟上第j个数就可以了。


于是我们有了递推式:

dp[i] = dp[i – 1] * 2  如果a[i]不在之前出现
dp[i] = dp[i – 1] * 2 – dp[j – 1],如果a[i]最近在j的位置出现过。

这里有一个问题:我们如何找到a[i]最近在哪里出现呢?我们可以用一个hash,更简单的方式是直接把a[i]放到数组下标里,记录它最后一次出现的位置,因为数据范围并不大,可以承受。 所以我们有了一个时间复杂度O(n)的动态规划解法。
注意最后答案是dp[n] – 1 因为我们需要减掉一个全空子序列。
伪代码:
输入: a[1..n]
辅助变量: have[x]表示目前x在a中最后一次出现的位置,如果没出现就是0,否则是一个正数。

dp[0] = 1
for  i = 1 to n do
	dp[i] = dp[i – 1] * 2
	if have[a[i]] > 0 then dp[i] -= dp[have[a[i]] – 1]
return dp[n] – 1

 AC代码:

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
const int Mod=1e9+7;
long long a[maxn],last[maxn],dp[maxn];
int main()
{
	memset(last,0,sizeof(last));
	int n,i;
	cin>>n;
	for(i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	dp[0]=1;
	for(i=1;i<=n;i++)
	{
		dp[i]=dp[i-1]*2;
		dp[i]%=Mod;
		if(last[a[i]])//如果a[i]在之前没有出现过 
		 dp[i]=(dp[i]-dp[last[a[i]]-1]+Mod)%Mod;//为什么加Mod?这是这道题的一个坑点,因为                                            
  //dp[i]可能大于Mod,在对Mod取余后dp[i]将会小于dp[last[a[i]]-1],从而得到一个小于0的数, 
		last[a[i]]=i;//记录最后一次a[i]出现的位置 
	}
	cout<<dp[n]-1<<endl;
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值