BZOJ 2111: [ZJOI2010]Perm 排列计数(DP+Lucas定理)

题面

http://www.lydsy.com/JudgeOnline/problem.php?id=2111


题解

题目大意:求n个节点的点权为1-n的排列的完全二叉树中有多少个小根堆。

很明显,将题目中的排列当成编号1-n的完全二叉树,然后在上面放1-n的点权,要求满足节点x的权值小于节点x<<1和x<<1|1。

树形DP,令f[x]为以x为根的方案,分为三种情况:

①x是叶子节点,f[x]=1
②x有且只有左儿子,f[x] = f[x<<1]
③x有两个儿子,x本身最小,此时左右两棵子树也都是小根堆,用乘法原理算上方案f[x<<1]*f[x<<1|1]。由于每棵子树的大小确定,给任意那么多个数方案也确定。排列可以交换,所以还要乘上组合数C(siz[x]-1, siz[x<<1])

siz数组在递归时同时计算。此题要求方案对质数p取模,p看起来很大,n又不大,但没说p>n,于是上Lucas定理

正因为发现了一道水题而高兴时,一交反手就是一个WA。试了组数据调试了一下,Lucas爆零了!(一堆0)

我顿时如丧考妣醍醐灌顶:预处理阶乘和逆元时一定要注意啊,就预处理1-min(n,p-1)就好了,不然范围搞大了一堆0啊。

时间复杂度: O(log2nlogpn+n)


CODE

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <cmath>
#define maxn 1000100

using namespace std;

int n, p;
int fac[maxn], inv[maxn];
int f[maxn], siz[maxn];

int Pow(int x, int y){
    int res = 1;
    while(y){
        if(y & 1)  res = 1LL * res * x % p;
        x = 1LL * x * x % p;
        y >>= 1;
    }
    return res;
}

void Init(){

    int up = min(n, p-1);
    fac[0] = 1;
    for(int i = 1; i <= up; i++)
        fac[i] = 1LL * fac[i-1] * i % p;

    inv[up] = Pow(fac[up], p-2);
    for(int i = up-1; i >= 0; i--)
        inv[i] = 1LL * inv[i+1] * (i+1) % p;
}

int C(int n, int m){
    if(m > n)  return 0;
    return 1LL * fac[n] * inv[n-m] % p * inv[m] % p;
}

int Lucas(int n, int m){
    if(m > n)  return 0;
    int ans = 1;
    for(; m; n/=p, m/=p)
        ans = 1LL * ans * C(n%p, m%p) % p;
    return ans;
}

void DP(int x){
    int ls = x << 1, rs = x << 1 | 1;
    if(rs <= n){
        DP(ls);  DP(rs);
        siz[x] = siz[ls] + siz[rs] + 1;
        f[x] = 1LL * f[ls] * f[rs] % p * Lucas(siz[x]-1, siz[ls]) % p;
    }
    else if(ls <= n){
        DP(ls);
        siz[x] = siz[ls] + 1;
        f[x] = f[ls];
    }
    else{
        siz[x] = 1;
        f[x] = 1;
    }
}

int main(){

    scanf("%d%d", &n, &p);

    Init();

    DP(1);

    printf("%d\n", f[1]);

    return 0;
}

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值