题面
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(log2n∗logpn+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;
}