广东工业大学2020级年ACM第一次月赛---H一道难题
题目链接:http://gdutcode.sinaapp.com/problem.php?id=1525
题目描述:
有一个长度为n的序列,特征为m,满足a[i]=a[i-m] (i>m),a[i]=i (i<=m)。
即
1,2,...,m[i]-1 ,m[i],1,2,...,m[i]-1,m[i],...(n-i) mod (m[i]+1)
例如 n=7,m=3时,序列如下:
“1 ,2, 3, 1, 2, 3, 1”
现可删除若干元素(也可以不删,也可以全删),问会出现多少不同的序列? 序列不同当且仅当长度不一致或者某一位不同
答案对1e9+7取模 即ans%1000000007
前置知识:动态规划、子序列
一、题目简述:
首先,不难看出,题目中的序列是一个以m为周期的序列,题目给出的例子也帮助我们更好得理解了这一点m之后的位置实际上是1~m的不断重复。
其次,对于题目中的删除若干元素所出现的不同序列,实际上求的就是原序列的非重复子序列(包含空序列)的个数,那么对于题目中的删除操作,我们也可以反向理解为要计算保留某些数字产生的不重复的子序列个数。
什么是子序列?
它是指序列的一部分项按原有次序排列而得的序列
这个应该不难理解,举个简单的例子:对于序列{1,2,3,1},{1}、{2}、{3}、{1,1}、{1,2,3}等都是它的子序列。
什么是动态规划?
关于这个,本人的授课水平与经验实在有限。。。贴出两个网站供各位参考学习:
https://www.sohu.com/a/149075950_684445
https://www.zhihu.com/question/39948290
另外可以配合着高中学的数学归纳法、杨辉三角和斐波那契数列等一同理解。
如果能够很好地理解动态规划,我们可以继续把目光转回这道题上。
二、初步思路
首先我们考虑:对于一个长度为n的没有重复数字的序列,其子序列(包括空序列)的个数是多少?
答案应该是2^n,这个应该不难理解,因为对于每个数字,我们在构造其子序列的时候都有选择与不选择两种方案(如果全部选择就是原序列,如果全都不选就是空序列)
那么我们令dp[i]表示前i个数字可以形成的非重复子序列个数,或者说,以第i个数字结尾的序列有多少个子序列。
我们可以知道其边界是dp[0]=1(空序列)
其状态转移方程是dp[i]=dp[i-1]*2(相当于我们对于第i个数字做出选择)
for(int i=1;i<=m;i++) dp[i]=(dp[i-1]*2)%MOD; //这里的MOD被提前设置为了题目中的模数
但是,这里考虑的只是没有重复数字的情况,很显然,这个状态转移方程只对m之前的序列有效,在m之后,重复的数字疯狂出现, 例如{1,2,3,1}这个序列,选择保留第一个和选择保留最后一个产生的子序列都是{1}
我们需要想办法剔除这些重复的情况
三、去重
我们思考,当第j个数和第i个数一样的时候,究竟对哪些子序列产生了影响?
由于子序列数字的相对位置不发生变化,对于第i个数字,如果它被第j个数字所替换,这种情况是重复的(也就是说选择第i个数字和不选择i~j -1而选择j的数字是同一种情况)。
还是用{1,2,3,1}进行举例,此时i=1,j=4那么上述的两种情况都是子序列{1}
也即是说,当我们对j作出选择的时候,要剔除它替换掉上一次出现的位置所产生的重复子序列。
那么dp[j]是要减去dp[i]吗?
实际上应当是减去dp[i-1],我们回顾dp数组的定义,dp[i] 表示的是以第i个字符结尾的有多少个子序列,而既然是替换就自然是减去dp[i-1]喽。
然后由于题目中的序列的周期性,对于第i个(i>m)数字,它上次出现的位置就是i-m。
for(int i=m+1;i<=n;i++) dp[i]=((dp[i-1]*2)%MOD-(dp[i-m-1])%MOD)%MOD; //这里只是简单用了(a-b)%p=(a%p-b%p)%p的性质。
四、一点小细节
理清了这么些复杂的思路后,我兴高采烈地拿着代码一波提交,然后果然WA了。。。。。。
实际上在每一次减法运算之后,应该加上这样一句:
dp[i]=(dp[i]+MOD)%MOD;
至于原因,其实并不复杂,大家不妨自行思考一下...
最后附上完整代码:
#include<iostream> #include<cstdio> #include<string> #include<cmath> #include<algorithm> #include<cstring> #define MOD 1000000007 using namespace std; int T; int n,m; int dp[1000086]; int main() { cin>>T; while(T--) { cin>>n>>m; dp[0]=1; for(int i=1;i<=m;i++) dp[i]=(dp[i-1]*2)%MOD; for(int i=m+1;i<=n;i++) { dp[i]=((dp[i-1]*2)%MOD-(dp[i-m-1])%MOD)%MOD; dp[i]=(dp[i]+MOD)%MOD; } printf("%d\n",dp[n]); } return 0; }