K - 堆里的 NaN
基本信息
总体思路
完全二叉树中,除了最后一个节点的祖先节点(称为特殊点)以外,其它节点的子树都是满二叉树。因此讨论 NaN 节点是否在特殊点即可。
详细题解
以下题解中,用“高度”表示一棵二叉树有多少层,而用“深度”表示某一个节点位于第几层。例如,对于一棵高度为 的满二叉树(也就是说,有 个节点的满二叉树),节点 的深度为 ,而节点 的深度为 。
常用结论
首先,您需要知道一个常用结论。考虑以下问题。
给定一棵 个节点的树,在每个节点中填一个从 到 (含两端)的数,要求每个节点填的数都不一样,且后代节点的数要比祖先节点的数大。求方案数。这个问题有个比较常见的名字,就是“树的拓扑序计数”
这个问题的答案是 ,其中 表示树的所有节点构成的集合, 表示以 为根的子树中有几个节点。也就是说,答案就是 的阶乘除以每个子树大小的乘积。
常用结论的证明
递推方程
令 表示以节点 为根的子树的拓扑序数量, 表示子树 中的节点数量,则递推方程如下:
递推方程的解释如下。
首先,拓扑序里的第一个位置一定是节点 占据的,子树里的节点只能占据剩下的 个位置。由于不同子树里的节点不会互相影响,因此我们先给每个子树分好位置,然后再乘上每个子树内部的方案数即可。
给每个子树分好位置的方案数,相当于共有 种颜色的球,其中第 种颜色的球共有 个,求排列的方案数。这是经典的可重排列问题,方案数为
然后再乘上每个子树内部的方案数,即 ,就得到了递推方程。
公式证明
对子树使用数学归纳法,把答案公式套进递推方程。
套用结论
接下来回到原问题。根据堆的性质,我们可以把原问题转化为以下计数问题。
给定一棵 个节点的完全二叉树,选择一个节点 ,将完全二叉树分为以 为根的子树 和子树之外的部分 。将 填入节点 中,还需要将 到 填入 和 中,使得 和 分别满足后代节点的数要比祖先节点的数大。求方案数。
尝试套用上面的常用结论计算答案。设 表示从 个物品里选 个的组合数, 表示 中每个子树大小的乘积, 表示 中每个子树大小的乘积,答案为
只要 的位置不一样,肯定就是不同的方案,因此最终总的方案数就是 。因为我们算的是从 个排列中等概率抽取的概率,因此概率就是总方案数除以 ,即 。
直接计算这个式子的复杂度至少是 的,但完全二叉树中有许多同构的子树,可以帮助我们减少计算。
完全二叉树的性质
具体来说,完全二叉树具有如下性质:称完全二叉树中编号最大的点以及它的祖先节点为“特殊点”,除了特殊点以外,以其它任意一个点为根的子树都是满二叉树。如下图所示,红色的点就是特殊点,而以任意一个黑色点为根的子树都是满二叉树。

另外还可以发现,除了节点 以外,每一个特殊点都有一个根节点与它同深度的满二叉子树。称该满二叉树为该特殊点的“兄弟树”。例如,节点 的兄弟树是以节点 为根的满二叉树,节点 的兄弟树是以节点 为根的满二叉树。节点 虽然看似没有兄弟树,但可以认为节点 也有一棵高度为 的兄弟树。
因为兄弟树是满二叉树,所以我们只关心它的高度。设整棵树高度为 ,某个特殊点的深度为 。如果该特殊点是父节点的左子节点,那么兄弟树的高度为 ;如果该特殊点是父节点的右子节点,那么兄弟树的高度为 。
记 表示深度为 的特殊点的兄弟树的高度,记 表示以深度为 的特殊点为根的子树中有几个节点。容易得到递推式:
预处理
接下来尝试计算 和 的值。首先预处理如下值。
- 表示一棵高度为 的满二叉树,所有子树大小的乘积。容易得到递推式:
- 表示一棵高度为 满二叉树,再去掉任意一棵高度为 的子树(要求 。也就是说,现在这棵树有 个节点),所有子树大小的乘积。容易得到递推式:
容易得到整棵二叉树子树大小的乘积为
预处理的复杂度是 。
讨论 NaN 节点的位置
接下来我们讨论两种情况: 是特殊点,以及 不是特殊点。
特殊点
如果 是特殊点,设该特殊点深度为 ,则所有以深度小于 的特殊点为根的子树的大小都将减少 ,而其它子树的大小不受影响。也就是说
特殊点只有 个,枚举所有特殊点即可。这一部分的复杂度是 。
非特殊点
如果 不是特殊点,那么它一定位于某个特殊点的兄弟树中。设它位于深度为 的特殊点的兄弟树中,且 的子树高度为 ,则所有以深度小于 的特殊点为根的子树的大小都将减少 ,当然该兄弟树内部的节点也会受到影响,而其它子树的大小不受影响。
为了处理这一情况,我们另外计算以下值。
递推式就是
那么
其中 ,所以式子可以 计算。我们枚举特殊点以及 即可。另外,满足 和 的点 共有 种,所以这一情况求出来的式子要乘以该值。这一部分的复杂度也是 。
得到答案
答案就是两种情况加起来,另外除法需要通过逆元计算。总体复杂度 。
参考代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91 | #include <bits/stdc++.h>
#define MAXP 30
#define MOD ((int) 1e9 + 7)
using namespace std;
int n;
long long ans;
int sz[MAXP + 10];
long long f[MAXP + 10][MAXP + 10];
long long g[MAXP + 10], h[MAXP + 10][MAXP + 10];
long long power(long long a, long long b) {
long long y = 1;
for (; b; b >>= 1) {
if (b & 1) y = y * a % MOD;
a = a * a % MOD;
}
return y;
}
long long inv(long long x) {
return power(x, MOD - 2);
}
// 预处理 g 与 h
void prepare() {
g[0] = g[1] = 1;
for (int i = 2; i <= MAXP; i++) g[i] = g[i - 1] * g[i - 1] % MOD * ((1 << i) - 1) % MOD;
h[1][1] = 1;
for (int i = 2; i <= MAXP; i++) {
for (int j = 1; j < i; j++) h[i][j] = h[i - 1][j] * g[i - 1] % MOD * ((1 << i) - (1 << j)) % MOD;
h[i][i] = 1;
}
}
void solve() {
scanf("%d", &n);
int D = 0;
for (int x = n; x; x >>= 1) D++;
memset(sz, 0, sizeof(sz));
memset(f, 0, sizeof(f));
// 计算 sz 与 f
sz[D] = 1;
long long X = 1;
for (int d = D, now = n; d > 1; d--) {
int nxt = now >> 1;
int z = now & 1 ? D - d + 1 : D - d;
sz[d - 1] = sz[d] + (1 << z);
X = X * g[z] % MOD;
now = nxt;
}
for (int j = 0; j < D; j++) f[1][j] = n - ((1 << j) - 1);
for (int i = 2; i <= D; i++) for (int j = 0, k = 0; k < sz[i]; j++, k = k * 2 + 1) f[i][j] = f[i - 1][j] * (sz[i] - k) % MOD;
X = inv(X * f[D][0] % MOD);
ans = 0;
for (int d = D, now = n; ; d--) {
// NaN 位于深度为 d 的特殊点
long long t = X;
for (int i = 1; i < d; i++) t = t * sz[i] % MOD * inv(sz[i] - sz[d]) % MOD;
t = t * sz[d] % MOD * inv(n) % MOD;
ans = (ans + t) % MOD;
if (d == 1) break;
// NaN 位于深度为 d 的兄弟树
int nxt = now >> 1;
int z = now & 1 ? D - d + 1 : D - d;
for (int j = 1; j <= z; j++) {
long long t = X * f[d - 1][0] % MOD * inv(f[d - 1][j]) % MOD;
t = t * g[z] % MOD * inv(h[z][j]) % MOD;
t = t * inv(g[j]) % MOD * ((1 << j) - 1) % MOD * inv(n) % MOD;
t = t * (1 << (z - j)) % MOD;
ans = (ans + t) % MOD;
}
now = nxt;
}
printf("%lld\n", ans);
}
int main() {
prepare();
int tcase; scanf("%d", &tcase);
while (tcase--) solve();
return 0;
}
|