BZOJ 1485: [HNOI2009]有趣的数列 [Catalan数 质因子分解]
1485: [HNOI2009]有趣的数列
Description
我们称一个长度为2n的数列是有趣的,当且仅当该数列满足以下三个条件:
(1)它是从1到2n共2n个整数的一个排列{ai};
(2)所有的奇数项满足a1<a3<…<a2n-1,所有的偶数项满足a2<a4<…<a2n;
(3)任意相邻的两项a2i-1与a2i(1≤i≤n)满足奇数项小于偶数项,即:a2i-1<a2i。
现在的任务是:对于给定的n,请求出有多少个不同的长度为2n的有趣的数列。因为最后的答案可能很大,所以只要求输出答案 mod P的值。
Input
输入文件只包含用空格隔开的两个整数n和P。输入数据保证,50%的数据满足n≤1000,100%的数据满足n≤1000000且P≤1000000000。
Output
仅含一个整数,表示不同的长度为2n的有趣的数列个数mod P的值。
DP方程的形式对本题影响重大!
发现奇数位置对应唯一的偶数位置,且第i个奇数位置最大$2i-1$,所以只考虑奇数位置,写一个DP:
$f[i][j] $表示前i个奇数位置最大j的方案数
然后只能优化到$O(n^2)$
找啊找从beiyu那里发现另一种方程:
$f[i][j]$ 前i个数,j个放在奇数位置的方案数
限制条件$\frac{i}{2} \le j \le i$并且最终奇数位置放了n个
这不就是Catalan数的走格子模型吗?
并且这个DP方程就是做那道走格子题目最原始的方程,放在奇数是向左走
这些数字是从小到大放进那些位置里,(这样避免了考虑大小影响),并且每一时刻放在奇数位置的个数一定大于等于放在偶数位置的个数,这样就和原始定义里的$+1\quad -1$对应起来啦!
重要的地方在于想到把数字从小到大放进去而不是从左到右考虑每个位置
然后本题没法求逆元,需要质因子分解,这种n小的情况直接保存lp[]就行了,超快
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> using namespace std; typedef long long ll; const int N=2e6+5; inline int read(){ char c=getchar();int x=0,f=1; while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();} while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();} return x*f; } int n,MOD; bool notp[N]; int p[N],lp[N]; void sieve(int n){ for(int i=2;i<=n;i++){ if(!notp[i]) p[++p[0]]=i,lp[i]=p[0]; for(int j=1;j<=p[0]&&i*p[j]<=n;j++){ notp[i*p[j]]=1; lp[i*p[j]]=j; if(i%p[j]==0) break; } } } int e[N]; void add(int x,int d){ while(x!=1){ e[lp[x]]+=d; x/=p[lp[x]]; } } void solve(){ ll ans=1; for(int i=2*n;i>=n+1;i--) add(i,1); for(int i=2;i<=n;i++) add(i,-1); add(n+1,-1); for(int j=1;j<=p[0];j++) for(;e[j];e[j]--) ans=ans*p[j]%MOD; printf("%lld",ans); } int main(){ freopen("in","r",stdin); n=read();MOD=read(); sieve(n<<1); solve(); }