前言

期望套期望,很有意思。来一发考场首 A,近 \(\Theta(n)\) 的算法。

题目链接:洛谷

题意简述

一棵树,每条边随机设有方向。对于所有 \(i\),从 \(i\) 开始随机游走,直到不能移动,所行走的距离的期望的期望。

前一个期望是局面确定时,随机游走的期望距离;后一个期望是所有局面下的期望距离。

题目分析

看到有 \(n \leq 1000\) 的部分分,很容易猜到这是换根 DP,这一档部分分是用来每个点跑一边 DFS 的。那么先来看看部分分。

枚举根

不妨先来考虑求以 \(yzh\) 为根时,从 \(yzh\) 开始随机游走的距离的期望的期望。

考虑设 \(f_i\) 表示以点 \(i\) 为根的答案。因为若从 \(\operatorname{fa}[i]\) 走到了 \(i\),那么 \(i\) 不可能返回到 \(\operatorname{fa}[i]\),所以这是一个形式相同,规模更小的子问题。注意到,这前提是以 \(yzh\) 为根的情况。

根据期望的定义式 \(\operatorname{E} = \sum \limits _ {S \subseteq \Omega} \operatorname{val}[S] \cdot \operatorname{P}[S]\)。本题中,\(\Omega\) 是 \(\operatorname{son}[i]\),\(\operatorname{val}[S]\) 指这样一个局面,若 \(j \in S\),\(i\) 和 \(j\) 之间的边的方向为 \(i \rightarrow j\),否则是 \(i \leftarrow j\)。从 \(i\) 开始随机游走,就是等概率从 \(S\) 中选出 \(j\),然后向 \(j\) 走。易知所有局面出现概率相等,即 \(\operatorname {P}[S] = \cfrac{1}{2 ^ {|\operatorname{son}[i]|}}\),故 \(f_i = \cfrac{1}{2 ^ {|\operatorname{son}[i]|}} \sum \limits _ {S \subseteq \operatorname{son}[i]} \operatorname{val}[S]\)。又有 \(\operatorname{val}[S] = \cfrac{1}{|S|} \sum \limits _ {j \in S} f_j + \operatorname{value}[i]\)。所以有

\[f_i = \operatorname{value}[i] + \cfrac{1}{2 ^ {|\operatorname{son}[i]|}} \sum \limits _ {S \subseteq \operatorname{son}[i]} \cfrac{1}{|S|} \sum \limits _ {j \in S} f_j
\]

值得注意的是,当 \(S = \varnothing\) 时,\(\cfrac{1}{|S|}\) 没意义,但是这种情况不会对答案产生贡献,所以只用考虑 \(S \neq \varnothing\) 的情况。

套路化地拆贡献,分别考虑每个 \(j \in \operatorname{son}[i]\) 的贡献。那么 \(j\) 产生的贡献就是包含 \(j\) 的所有情况的 \(\cfrac{1}{|S|}\) 之和 \(\operatorname{sum}\),与 \(f_j\) 的乘积。发现对于所有 \(j\) 来说,前者是一样的,都是 \(\large \operatorname{sum} = \sum \limits _ {i = 1} ^ {|\operatorname{son}[i]|} \cfrac{C_{|\operatorname{son}[i]| - 1} ^ {i - 1}}{i}\),其中 \(i\) 表示包含 \(j\) 的集合 \(S\) 的大小,组合数表示包含 \(j\),且大小为 \(i\) 的 \(S\) 的情况数。所以有

\[\Large f_i = \operatorname{value}[i] + \cfrac{1}{2 ^ {|\operatorname{son}[i]|}} \left (\sum \limits _ {i = 1} ^ {|\operatorname{son}[i]|} \cfrac{C_{|\operatorname{son}[i]| - 1} ^ {i - 1}}{i} \right) \left( \sum \limits _ {j \in \operatorname{son}[i]} f_j \right)
\]

到此为止,已经可以拿到这部分分了。注意到 \(\sum |\operatorname{son}[i]| = n - 1\),所以配合线性推逆元等奇技淫巧,一次 dfs 的时间复杂度是 \(\Theta(n)\) 的,总体时间复杂度 \(\Theta(n^2)\) 的。

换根 DP

换根 DP 的第一遍 dfs 和部分分相同,考虑第二遍 dfs。

在第二遍 dfs 时,考虑根从 \(yzh \rightarrow xym\),那么首先要把 \(f_{yzh}\) 中来自 \(xym\) 的贡献去掉,再累加到 \(f_{xym}\) 的求和里,同时注意维护换根时对 \(\operatorname{son}\) 的影响。推式子也很好推,考虑记 \(\operatorname{cal}(n) = \sum \limits _ {i = 1} ^ {n} \cfrac{C_{n - 1} ^ {i - 1}}{i}\),用 \(f'\) 表示第一遍 dfs 求出的 \(f\),\(F\) 表示以 \(xym\) 为根时的 \(f\)。

\[
F_{yzh} = \operatorname{val}[yzh] + \cfrac{1}{2^{|\operatorname{son}[yzh]| - [yzh = 1]}} \cdot \operatorname{cal}(|\operatorname{son}[yzh]| - [yzh = 1]) \cdot \left(\cfrac{2 ^ {|\operatorname{son}[yzh]| + [yzh \neq 1]}(f_{yzh} - \operatorname{val}[yzh])}{\operatorname{cal}(|\operatorname{son}[yzh]| + [yzh \neq 1])} - f'_{xym} \right)

\]

\[
f_{xym} = \operatorname{val}[xym] + \cfrac{1}{2^{|\operatorname{son}[xym]| + 1}} \cdot \operatorname{cal}(|\operatorname{son}[xym]| + 1) \cdot\left(2 ^ {|\operatorname{son}[xym]|} \cdot \operatorname{cal}(|\operatorname{son}[xym]|) \cdot (f'_{xym} - \operatorname{val}[xym]) + F_{yzh} \right)

\]

然后就可以啦~

时间复杂度分析

yzh 问为什么时间复杂度是 \(\Theta(n)\) 的,那么接下来回答下她的问题。

发现本来时间复杂度是 \(\Theta(n)\) 的,但是有求 \(\operatorname{cal}\) 函数的逆元,会多一个 \(\log P\)。但是,加上记忆化后,时间复杂度是 \(\Theta(n + \sqrt{n}\log P)\) 的,考虑证明一棵树本质不同的儿子数是 \(\Theta(\sqrt{n})\) 的。

证明:

反证。假设一棵树有 \(k \gt \sqrt{2n}\) 个本质不同的孩子数,分别为 \(s_1, s_2, \cdots, s_k\)。那么有 \(n = \sum \limits _ {i = 1} ^ {k} s_i \geq \sum \limits _ {i = 1} ^ {k} i = \cfrac{(1 + k)k}{2} \gt n\),矛盾,所以一棵树本质不同的儿子数是 \(\Theta(\sqrt{n})\) 的。

在本题的数据范围下,可以近似看做线性算法。

代码

略去了快读快写,实现也很容易。

// #pragma GCC optimize(3)
// #pragma GCC optimize("Ofast", "inline", "-ffast-math")
#pragma GCC target("avx", "sse2", "sse3", "sse4", "mmx")
#include <iostream>
#include <cstdio>
#define debug(a) cerr << "Line: " << __LINE__ << " " << #a << endl
#define print(a) cerr << #a << "=" << (a) << endl
#define file(a) freopen(#a".in", "r", stdin), freopen(#a".out", "w", stdout)
#define main Main(); signed main(){ return ios::sync_with_stdio(0), cin.tie(0), Main(); } signed Main
using namespace std; char ST; constexpr const int mod = 1e9 + 7; constexpr inline int pow(const int a, const int p, const int mod = mod){
int res = 1, base = a % mod, b = p;
while (b){
if (b & 1) res = 1ll * res * base % mod;
base = 1ll * base * base % mod, b >>= 1;
}
return res;
} constexpr inline int inv(const int a, const int mod = mod){
return pow(a, mod - 2, mod);
} constexpr const int inv2 = inv(2, mod); inline int add(int a, int b){
return a + b >= mod ? a + b - mod : a + b;
} inline int mul(int a, int b){
return 1ll * a * b % mod;
} int n, val[1000010], frac[1000010], ans[1000010];
int Inv[1000010], ifrac[1000010], pw2[1000010], ipw2[1000010];
int f[1000010]; inline int C(int n, int m){
return mul(frac[n], mul(ifrac[m], ifrac[n - m]));
} struct Graph{
struct node{
int to, nxt;
} edge[1000010];
int eid, head[1000010];
inline void add(int u, int v){
edge[++eid] = {v, head[u]};
head[u] = eid;
}
inline node & operator [] (const int x){
return edge[x];
}
} xym; int g[1000010], ig[1000010];
int calsum(int son){
if (g[son] != -1) return g[son];
int sum = 0;
for (int i = 1; i <= son; ++i)
sum = add(sum, mul(Inv[i], C(son - 1, i - 1)));
return g[son] = sum;
}
int icalsum(int son){
if (ig[son] != -1) return ig[son];
return ig[son] = inv(calsum(son));
} int soncnt[1000010]; void dfs(int now, int fa){
// 这一遍维护子树信息
for (int i = xym.head[now], to; to = xym[i].to, i; i = xym[i].nxt){
if (to == fa) continue;
++soncnt[now], dfs(to, now), f[now] = add(f[now], f[to]);
}
f[now] = mul(f[now], calsum(soncnt[now]));
f[now] = mul(f[now], ipw2[soncnt[now]]);
f[now] = add(f[now], val[now]);
} void redfs(int now, int fa){
// 这一次搞一搞父亲方向的子树
ans[now] = f[now];
for (int i = xym.head[now], to; to = xym[i].to, i; i = xym[i].nxt){
if (to == fa) continue;
f[to] = add(
mul(
mul(
add(
mul(
mul(
pw2[soncnt[to]],
add(
f[to],
mod - val[to]
)
),
icalsum(soncnt[to])
),
add(
mul(
mul(
add(
mul(
mul(
pw2[soncnt[now] + (now != 1)],
add(
f[now],
mod - val[now]
)
),
icalsum(soncnt[now] + (now != 1))
),
mod - f[to]
),
calsum(soncnt[now] - (now == 1))
),
ipw2[soncnt[now] - (now == 1)]
),
val[now]
)
),
calsum(soncnt[to] + 1)
),
ipw2[soncnt[to] + 1]
),
val[to]
);
redfs(to, now);
}
} char ED; signed main(){
#if XuYueming
fprintf(stderr, "Memory: %2.lfMb\n", (&ED - &ST) / 1024.0 / 1024.0);
#endif read(n);
for (int i = 2, fa; i <= n; ++i) read(fa), xym.add(fa, i);
ifrac[0] = frac[0] = 1, ig[0] = g[0] = -1, pw2[0] = ipw2[0] = 1;
for (int i = 1; i <= n; ++i)
read(val[i]),
frac[i] = mul(frac[i - 1], i),
pw2[i] = mul(pw2[i - 1], 2), ipw2[i] = mul(ipw2[i - 1], inv2),
Inv[i] = i == 1 ? 1 : mod - mul(mod / i, Inv[mod % i]),
ifrac[i] = mul(ifrac[i - 1], Inv[i]), g[i] = ig[i] = -1;
dfs(1, 0), redfs(1, 0);
for (int i = 1; i <= n; ++i) write(ans[i], '\n');
return 0;
}

后记

考后认真思考一番,发现瓶颈在算 \(\operatorname{cal}\) 的逆元,而我们目的仅是去掉某一贡献,故可以额外记 \(g\) 表示 \(f\) 在算 \(\operatorname{cal}\) 之前的值。并且 \(\operatorname{cal}\) 其实是可以继续用数学的方式 \(\Theta(1)\) 计算的,由于本题解方法不侧重这里,所以简略地推一推,其实也很简单:

\[\begin{aligned}

\operatorname{cal}(n) & = \sum \limits _ {i = 1} ^ {n} \cfrac{C_{n - 1} ^ {i - 1}}{i} \\
&= \sum \limits _ {i = 1} ^ {n} \cfrac{(n - 1)!}{i \cdot (i - 1)! \cdot (n - i)!} \\
&= \sum \limits _ {i = 1} ^ {n} \cfrac{n \cdot (n - 1)!}{i! \cdot n \cdot (n - i)!} \\
&= \sum \limits _ {i = 1} ^ {n} \cfrac{C_n^i}{n} \\
&= \cfrac{\sum \limits _ {i = 0} ^ {n} C_n^i - C_n^0}{n} \\
&= \cfrac{2^n - 1}{n}

\end{aligned}
\]

配合上题目输入已经拓扑排序,dfs 换成循环,能跑到 Rank1,代码也很短,注意到实现中 cal 数组含义是 \(\cfrac{\operatorname{cal}(n)}{2^n}=\cfrac{1}{n}-\cfrac{1}{n\cdot2^n}\)。

#pragma GCC target("avx", "sse2", "sse3", "sse4", "mmx")
#include <cstdio> constexpr const int MAX = 1 << 24, yzh_i_love_you = 1314520736; char buf[MAX], *p1 = buf, *p2 = buf, obuf[MAX], *o = obuf;
#ifndef XuYueming
#define getchar() ((p1 == p2) && (p2 = (p1 = buf) + fread(buf, 1, MAX, stdin), p1 == p2) ? EOF : *p1++)
#endif constexpr inline bool isdigit(const char c) { return c >= '0' && c <= '9'; }
template <typename T> inline void read(T &x) { x = 0; char c = 0; for (;!isdigit(c); c = getchar()); for (; isdigit(c); c = getchar()) x = (x << 3) + (x << 1) + (c ^ 48); }
template <typename T> inline void write(T &x){ static short Stack[50], top = 0; do Stack[++top] = x % 10, x /= 10; while (x); while (top) *o++ = Stack[top--] | 48; } constexpr const int mod = 1e9 + 7;
constexpr const int inv2 = 500000004;
constexpr inline int add(int a, int b){ return a + b >= mod ? a + b - mod : a + b; }
constexpr inline int mul(int a, int b){ return 1ll * a * b % mod; } int n, fa[1000001], soncnt[1000001], val[1000001], cal[1000001], Inv[1000001], ipw2[1000001], f[1000001], g[1000001]; signed main(){
read(n);
for (register int i = 2; i <= n; ++i) read(fa[i]), ++soncnt[fa[i]];
Inv[0] = ipw2[0] = 1;
for (register int i = 1; i <= n; ++i) read(val[i]), ipw2[i] = mul(ipw2[i - 1], inv2), Inv[i] = i == 1 ? 1 : mod - mul(mod / i, Inv[mod % i]), cal[i] = add(Inv[i], mod - mul(Inv[i], ipw2[i]));
for (register int i = n; i >= 1; --i) f[i] = add(val[i], mul(g[i], cal[soncnt[i]])), g[fa[i]] = add(g[fa[i]], f[i]);
for (register int i = 2; i <= n; ++i) g[i] = add(g[i], add(val[fa[i]], mul(add(g[fa[i]], mod - f[i]), cal[soncnt[fa[i]] - 1]))), f[i] = add(val[i], mul(g[i], cal[++soncnt[i]]));
for (register int i = 1; i <= n; ++i) write(f[i]), *o++ = '\n';
return fwrite(obuf, 1, o - obuf, stdout), 0;
}

[COCI 2023/2024 #3] Slučajna Cesta 题解的更多相关文章

  1. 百度之星初赛A 今夕何夕

    今夕何夕 今天是2017年8月6日,农历闰六月十五. 小度独自凭栏,望着一轮圆月,发出了"今夕何夕,见此良人"的寂寞感慨. 为了排遣郁结,它决定思考一个数学问题:接下来最近的哪一年 ...

  2. Bzoj索引

    1001 : http://ideone.com/4omPYJ1002 : http://ideone.com/BZr9KF1003 : http://ideone.com/48NJNh1004 : ...

  3. Hsql中In没有1000的限制

    SELECT * FROM user , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ...

  4. HDU 6112 今夕何夕

    今夕何夕 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submis ...

  5. 日期求星期(java)-蓝桥杯

    日期求星期问题(java)-蓝桥杯 1:基姆拉尔森计算公式(计算星期) 公式: int week = (d+2*m+3*(m+1)/5+y+y/4-y/100+y/400)%7; 此处y,m,d指代年 ...

  6. 解决Nginx重启时提示nginx: [emerg] bind() to 0.0.0.0:80错误

    Nginx是一款轻量级的Web服务器,特点是占有内存少,并发能力强,因而使用比较广泛,蜗牛今天在一个VPS上重启Nginx时提示“nginx: [emerg] bind() to 0.0.0.0:80 ...

  7. 2017"百度之星"程序设计大赛 - 初赛(A) 01,05,06

    小C的倍数问题    Time Limit: 2000/1000 MS (Java/Others)  Memory Limit: 32768/32768 K (Java/Others) Problem ...

  8. HDU 2021 发工资咯:)(最水贪心)

    传送门: http://acm.hdu.edu.cn/showproblem.php?pid=2021 发工资咯:) Time Limit: 2000/1000 MS (Java/Others)    ...

  9. 百度之星2017初赛A-1005-今夕何夕

    今夕何夕 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submis ...

  10. [SinGuLaRiTy] 2017 百度之星程序设计大赛 初赛A

    [SinGuLaRiTy-1036] Copyright (c) SinGuLaRiTy 2017. All Rights Reserved. 小C的倍数问题 Time Limit: 2000/100 ...

随机推荐

  1. hbase的优缺点

    一. 一个关于hbase介绍全面的博客地址 https://www.csdn.net/gather_22/MtTaEgysNjYwOS1ibG9n.html 优点: 1,方便高效的压缩数据. 2,支持 ...

  2. 记录一次学习mongodb的20个常用语句

    // 查询当前数据库 db // // 查看所有数据库 show dbs// 创建数据库 use db_name// 删除数据库 db.dropDatabase()// 创建集合 db.createC ...

  3. 降维(三)LLE与其他降维技术

    LLE 局部线性嵌入,Locally Linear Embedding(LLE)是另一个功能强大的非线性降维(nonlinear dimensional reduction,NLDR)技术.它是一个流 ...

  4. 生产环境部署Nginx服务器双机热备部署-keepalived(多种模式教程)

    前言:今天演示下生产环境keepalived的部署方式,安装模式有很多,比如说主备模型和双主模型,主备分:抢占模式 和 非抢占模式.这里我会一一展开说具体怎么配置 一.双节点均部署Nginx: 第一步 ...

  5. Node.js中的模块

    CommonJS模块 CommonJS是一种规范,它定义了JavaScript 在服务端运行所必备的基础能力,比如:模块化.IO.进程管理等.其中,模块化方案影响深远,其对模块的定义如下: 1,模块引 ...

  6. FPGA bit转bin文件

    首先科普一下 什么是bitstream文件 FPGA比特流(bitstream)是一种用于配置可编程逻辑器件的数据格式,特别是现场可编程门阵列(FPGA).比特流包含了硬件逻辑电路.路由信息以及芯片上 ...

  7. 数据库中的空值处理(reader.IsDBNull(index))

    数据库中空值的处理 -> 准备一张新表 create table nullTable ( id int primary key, name nvarchar(10) ) insert into ...

  8. 2 - 【RocketMQ 系列】CentOS 7.6 安装部署RocketMQ

    二.开始安装部署RocketMQ 官方网站:https://rocketmq.apache.org/ 各版本要求: 1.版本选取 下载地址: https://github.com/apache/roc ...

  9. linux信号机制(初识版)

    转载 https://www.zhihu.com/question/24913599/answer/2584544572 信号是操作系统内核为我们提供用于在进程间通信的机制,内核可以利用信号来通知进程 ...

  10. 拥抱未来:GPT-4将如何改变我们的世界

    随着人工智能技术的迅猛发展,我们正迎来一个全新的智能时代.在这个时代的前沿,GPT-4作为开拓者和领航者,正在重新定义人机交互.创意创新和个性化服务的标准.无论是在商业领域.教育场景还是科研领域,GP ...