为什么会有人推得出来第三题想不出来签到题啊 (⊙_⊙)?
题面
有一棵有根树 (T)。从根节点出发,在点 (u) 时,设点 (u) 还有 (d) 个未访问过的儿子,则有 (frac1{d+1}) 的概率向上(深度较小的方向)走一步,有 (frac1{d+1}) 的概率走向一个未访问过的儿子。从根节点往上走则结束游走。
记 (f(T)) 为这样游走到达的点的深度之和的期望。
给定 (N)((Nle10^7)),对 ((1,2,dots,N)) 的所有排列 (P),建立小根的笛卡尔树 (T_P),求
答案对给定的正整数 (mod) ((nlt modle2 imes10^9))取模,(mod) 不一定是质数。
解析
先分析在 (T) 上的游走方法。笛卡尔树是二叉树,若当前点有未访问的儿子,则:
- 只有一个儿子时,有 (frac 12) 的概率会走向该儿子;
- 有两个儿子时,有 (frac 13) 的概率第一次就走向该儿子,有 (frac 13 imesfrac 12) 的概率第二次走向该儿子,即总共有 (frac 12) 的概率会走向该儿子。
于是我们发现是否会到达一个儿子的概率恒为 (frac12),与儿子个数无关,这会使我们之后的推导方便很多。
考虑到笛卡尔树本身是一个分治结构——从最小值处划分为两个区间分别建笛卡尔树,而一个排列建立笛卡尔树仅仅与排列的元素个数有关。由此可以设计一个以排列元素大小为状态的 DP。
设 (g_n) 表示「对 (n) 个元素的所有排列 (P_n) 建立笛卡尔树 (T_{P_n}),其 (f(T_{P_n})) 之和」,(g_N) 即我们要求的答案。但是深度之和并不好直接计算(尽管可以用期望的线性性拆成单点的贡献,但是之后的推导会绕一个大圈,不如下面的方法直观)。
有一个非常常用的性质:(sum dep=sum siz),于是设计辅助 DP (f_n) 表示「对 (n) 个元素的所有排列 (P_n) 建立笛卡尔树 (T_{P_n}),从根出发期望能够到达多少个点」。
转移则考虑枚举左子树的大小 (l),选出左子树的元素 (inom{n-1}{l})。利用期望的线性性,左子树的贡献为 (f_l) 乘上右子树的方案数,一个排列显然和一棵笛卡尔树一一对应,所以贡献即为 (f_l imes(n-l-1))。右子树同理,最后还要加上根的贡献,对于 (n!) 种笛卡尔树根的贡献都是 (1)。
先不管 (g_n),继续推导 (f_n) 的式子:
这么多阶乘容易让人联想到指数生成函数的样子,不妨化一下:
显然可以把 (frac{f_n}{n!}) 看成一个整体,发现转移式的主体是一个前缀和。记 (F_n) 为 (frac {f_i}{i!}) ((ige1))的前缀和,则式子可以简化为:
(F_0=0),多次迭代过后可以得到 (F_n) 的通项。
有一个类似于调和级数前 ((n+1)) 项的东西,设调和级数前 (n) 项为 (H_n)。
现在回头看一看 (g_n),大致转移与 (f_n) 相同,但是根的贡献是 (f_n),也即 (siz_n) 的期望值(所以先推导 (f))。
同样的,我们记 (G_n) 为 (frac{g_i}{i!}) 的前缀和,把 ((1)) 代入。
对 ((2)) 进行迭代也可以得到 (G_n) 的通项公式:
我们要算的答案是 (g_n=n!(G_n-G_{n-1})),由于 (mod) 不一定是质数,那还得继续推式子。
这样分母就可以全部抵消了,预处理调和级数前 (n) 项系数的前缀和与后缀和可以 (mathcal O(n)) 求解。
源代码
/* Lucky_Glass */
#include <cstdio>
#include <cstring>
#include <algorithm>
const int N = 1e7 + 10;
typedef long long llong;
int mod;
inline int reduce(llong key) {
return int((key %= mod) < 0 ? key + mod : key);
}
int pre[N], suf[N];
int main() {
freopen("cartesian.in", "r", stdin);
freopen("cartesian.out", "w", stdout);
int n; scanf("%d%d", &n, &mod);
pre[0] = 1;
for (int i = 1; i <= n; ++i) pre[i] = reduce(1ll * pre[i - 1] * i);
suf[n + 1] = 1;
for (int i = n; i; --i) suf[i] = reduce(1ll * suf[i + 1] * i);
int ans = 0;
for (int i = 1; i <= n; ++i)
ans = reduce(ans + 1ll * pre[i - 1] * suf[i + 1]);
int ex_ans = 0;
for (int i = 2, tmp = 0; i <= n; ++i) {
tmp = reduce(pre[i - 2] + (i - 1ll) * tmp);
ex_ans = reduce(ex_ans + 1ll * tmp * suf[i + 1]);
}
ans = reduce(1ll * ans + ex_ans);
printf("%d
", ans);
return 0;
}