[BZOJ 2111] 排列计数

Link:

BZOJ 2111 传送门

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)小根堆的个数只受序列中数的大小关系影响,与其差值无关

每一个数值两两不等的序列能产生的小根堆数相同

 

posted @ 2018-07-10 22:21  NewErA  阅读(150)  评论(0编辑  收藏  举报