[BZOJ 2111] 排列计数
Link:
Solution:
小根堆的模型还是很容易能看出来的
利用树形$dp$统计方案数:$dp[i]=dp[lc]*dp[rc]*C[sz[i]-1][sz[lc]]$
(小根堆的个数只受序列中数的大小关系影响,与其差值无关,因此每一个组合产生相同的个数)
其中组合数的计算全是坑点啊……
需要注意此题中$MOD$的值可能小于$n$
因此直接求组合数以及用费马小定理算逆元的方法都是错误的($fac[i](i>MOD)$均为0!)
于是需要使用$Lucas$定理来计算组合数,来保证$a,b$均小于$MOD$时才调用$fac$和$inv$
此时$fac$和$inv$的上界都应设为$MOD-1$来保证正确性(或者使用另一种线性求1至$n$逆元的方式)
Code:
#include <bits/stdc++.h> using namespace std; #define lc (i<<1) #define rc (i<<1|1) typedef long long ll; const int MAXN=1e6+10; int sz[2*MAXN]; ll n,MOD,fac[MAXN],inv[MAXN],dp[MAXN]; ll quick_pow(ll a,ll b) { ll ret=1; for(;b;b>>=1,a=a*a%MOD) if(b&1) ret=ret*a%MOD; return ret; } ll C(ll a,ll b) //n大于MOD时一定要用Lucas { if(a<b) return 0; if(a<MOD&&b<MOD) return fac[a]*inv[b]%MOD*inv[a-b]%MOD; return C(a/MOD,b/MOD)*C(a%MOD,b%MOD)%MOD; } int main() { scanf("%lld%lld",&n,&MOD); fac[0]=1; /* int mn=min(n,MOD-1); 如果n>MOD则要重新设置上限 for(int i=1;i<=mn;i++) fac[i]=fac[i-1]*i%MOD; inv[mn]=quick_pow(fac[mn],MOD-2); for(int i=mn-1;i>=0;i--) inv[i]=inv[i+1]*(i+1)%MOD; */ for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD; inv[0]=inv[1]=1;//求1-n逆元的线性做法 for(int i=2;i<=n;i++) inv[i]=(MOD/i+1)*inv[i-MOD%i]%MOD; for(int i=2;i<=n;i++) inv[i]=inv[i]*inv[i-1]%MOD; for(int i=n;i>=1;i--) { sz[i]=sz[lc]+sz[rc]+1; dp[i]=(lc>n?1:dp[lc])*(rc>n?1:dp[rc])%MOD*C(sz[i]-1,sz[lc])%MOD; } printf("%lld",dp[1]); return 0; }
Review:
(1)遇到模数要输入的题目要敏感,考虑模数是否可能小于$n$
此时必须要利用$Lucas$定理求解组合数,求逆元时上限也要从$n$改为$MOD-1$
(2)小根堆的个数只受序列中数的大小关系影响,与其差值无关
每一个数值两两不等的序列能产生的小根堆数相同