「SOL」打扫笛卡尔cartesian (模拟赛)

为什么会有人推得出来第三题想不出来签到题啊 (⊙_⊙)?

题面

有一棵有根树 T。从根节点出发,在点 u 时,设点 u 还有 d 个未访问过的儿子,则有 1d+1 的概率向上(深度较小的方向)走一步,有 1d+1 的概率走向一个未访问过的儿子。从根节点往上走则结束游走。

f(T) 为这样游走到达的点的深度之和的期望。

给定 NN107),对 (1,2,,N) 的所有排列 P,建立小根的笛卡尔树 TP,求

Pf(Tp)

答案对给定的正整数 modn<mod2×109)取模,mod 不一定是质数。


解析

先分析在 T 上的游走方法。笛卡尔树是二叉树,若当前点有未访问的儿子,则:

  • 只有一个儿子时,有 12 的概率会走向该儿子;
  • 有两个儿子时,有 13 的概率第一次就走向该儿子,有 13×12 的概率第二次走向该儿子,即总共有 12 的概率会走向该儿子。

于是我们发现是否会到达一个儿子的概率恒为 12,与儿子个数无关,这会使我们之后的推导方便很多。

考虑到笛卡尔树本身是一个分治结构——从最小值处划分为两个区间分别建笛卡尔树,而一个排列建立笛卡尔树仅仅与排列的元素个数有关。由此可以设计一个以排列元素大小为状态的 DP。

gn 表示「对 n 个元素的所有排列 Pn 建立笛卡尔树 TPn,其 f(TPn) 之和」,gN 即我们要求的答案。但是深度之和并不好直接计算(尽管可以用期望的线性性拆成单点的贡献,但是之后的推导会绕一个大圈,不如下面的方法直观)。

有一个非常常用的性质:dep=siz,于是设计辅助 DP fn 表示「对 n 个元素的所有排列 Pn 建立笛卡尔树 TPn,从根出发期望能够到达多少个点」。

转移则考虑枚举左子树的大小 l,选出左子树的元素 (n1l)。利用期望的线性性,左子树的贡献为 fl 乘上右子树的方案数,一个排列显然和一棵笛卡尔树一一对应,所以贡献即为 fl×(nl1)。右子树同理,最后还要加上根的贡献,对于 n! 种笛卡尔树根的贡献都是 1

fn=n!+12l=0n1(n1l)((nl1)!fl+l!fnl1)

先不管 gn,继续推导 fn 的式子:

fn=n!+l=0n1(n1l)(nl1)!fl=n!+l=0n1(n1)!fll!

这么多阶乘容易让人联想到指数生成函数的样子,不妨化一下:

fnn!=1+1nl=0n1fll!

显然可以把 fnn! 看成一个整体,发现转移式的主体是一个前缀和。记 Fnfii!i1)的前缀和,则式子可以简化为:

FnFn1=1+1nFn1Fn=1+n+1nFn1

F0=0,多次迭代过后可以得到 Fn 的通项。

Fn=i=2n+1n+1i

有一个类似于调和级数前 (n+1) 项的东西,设调和级数前 n 项为 Hn

(1)Fn=(n+1)(Hn1)

现在回头看一看 gn,大致转移与 fn 相同,但是根的贡献是 fn,也即 sizn 的期望值(所以先推导 f)。

gn=fn+12l=0n1(n1l)((nl1)!gl+l!gnl1)=fn+l=0n1(n1l)(nl1)!gl=fn+l=0n1(n1)!gll!=n!+l=0n1(n1)!gl+fll!

同样的,我们记 Gngii! 的前缀和,把 (1) 代入。

GnGn1=1+1n(Fn1+Gn1)=Hn+1nGn1(2)Gn=Hn+n+1nGn1

(2) 进行迭代也可以得到 Gn 的通项公式:

Gn=i=1nn+1i+1Hi

我们要算的答案是 gn=n!(GnGn1),由于 mod 不一定是质数,那还得继续推式子。

gn=n!(Hn+i=1n1Hii+1)=n!Hn+n!i=2n1ij=1i11j=n!Hn+1i<jnn!ij

这样分母就可以全部抵消了,预处理调和级数前 n 项系数的前缀和与后缀和可以 O(n) 求解。


源代码

Copy/* 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\n", ans);
  return 0;
}

THE END

Thanks for reading!

霓虹中 错落影像
满城声色褪去喧嚷
废墟上 余碑文几行
未铭记何谈淡忘

——《岁月成碑》By 乐正绫/Days

> Link 岁月成碑 - 网易云

posted @   Lucky_Glass  阅读(155)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
TOP BOTTOM
点击右上角即可分享
微信分享提示