这道题是我看了题解以后才做出来的,真是一道神题,但是写题解的大神都不愿解释得太详细,所以我想了很久才想明白。。
看了题解以后真的觉得很像数的划分、约瑟夫问题还有国王游戏,代码出奇地简洁,但是思维量相当地高。
主要思路:离散。
三个引理:
①在n->n-1的转化过程中,我们删除了一个点后,我们可以将n-1个点视为仍是1~n-1的排列。
②在若排列Pn为一个合法抖动子序列,则交换i∈[1,n)与i+1,必能得到另一个抖动子序列。
③抖动序列的对称性,若存在第一段上升的长度为n的抖动子序列,则以n+1-x代x必能得到一个第一段下降的长度为n的抖动子序列。
设f[i][j]为长度为i的,以j开头的,第一段下降的抖动子序列的个数,则循题意可得2*f[n+1][n+1]即为答案。
考虑转移:
①若j的下一个是j-1,则需要一个长度为n-1的,以j-1开头的上升子序列;
再分两种情况:
若j==n,则j-1为i-1中正数第一个数,所以可以转移到f[i][1];
若j<n,则j-1为i-1中正数第i-1-(j-1)+1-1个数,所以可转移到f[i-1][j-i].
我们根据状态设定,显然有f[i][0]=f[i][1]=0.
∴f[i][j]->f[i-1][j-i].
②若j的下一个不是j-1,则对于任意一个以j-1开头的下降子序列,均可以通过交换j-1和j+1得到。
∴f[i][j]->f[i][j-1]
综,f[i][j]=f[i-1][j-i]+f[i][j-1].
减少代码量?一个技巧:
改变状态,令f[i][j]=f[i+1][j+1],则初始状态变更为f[1][1]=1,i∈[1,n],j∈[1,n].
#include<iostream>
using namespace std;
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
int f[2][10001];
int main(){
int n,p;
bool tmp;
scanf("%d%d",&n,&p);
f[1][1]=1;
for(int i=2;i<=n;++i){
tmp=i&1;
for(int j=1;j<=i;++j)
f[tmp][j]=(f[!tmp][i-j+1]+f[tmp][j-1])%p;
}
printf("%d",(f[tmp][n]<<1)%p);
}
两年后再来做这道题——这是什么奇怪的做法嘛!
完全可以令f[i][j][k]表示1~i在最终的排列中被分成j段,两边有k个是上升的。
那么新加入一个i+1,可以加在两边,或者新建一段,或者连接两段——就这么无脑!
而且吐槽一下两年前的我,是不是看题解看懵逼了,第一个做法感觉完全可以暴力枚举下一位然后前缀和优化一下就行了嘛。。。并不需要考虑太复杂。
#include<cstdio>
#include<iostream>
using namespace std;
const int N=4200+5;
typedef long long LL;
int f[N][N][3];
int main()
{
freopen("codevs1523.in","r",stdin);
freopen("codevs1523.out","w",stdout);
int n,p;
scanf("%d%d",&n,&p);
f[1][1][0]=1;
for(int i=1;i<n;++i)
for(int j=min(i,n-i+1);j;--j)
for(int o=3;o--;)
if(f[i][j][o])
{
//printf("f(%d,%d,%d)=%d\n",i,j,o,f[i][j][o]);
if(j>1)f[i+1][j-1][o]=(f[i+1][j-1][o]+(LL)f[i][j][o]*(j-1))%p;
f[i+1][j+1][o]=(f[i+1][j+1][o]+(LL)f[i][j][o]*(j+1-o))%p;
if(o<2)f[i+1][j][o+1]=(f[i+1][j][o+1]+(LL)f[i][j][o]*(2-o))%p;
}
printf("%d\n",((f[n][1][0]+f[n][1][1])%p+f[n][1][2])%p);
}