[COCI 2023/2024 #3] Slučajna Cesta 题解
前言
期望套期望,很有意思。来一发考场首 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]\)。所以有
值得注意的是,当 \(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\) 的情况数。所以有
到此为止,已经可以拿到这部分分了。注意到 \(\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\)。
然后就可以啦~
时间复杂度分析
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)\) 计算的,由于本题解方法不侧重这里,所以简略地推一推,其实也很简单:
配合上题目输入已经拓扑排序,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;
}
本文作者:XuYueming,转载请注明原文链接:https://www.cnblogs.com/XuYueming/p/18187874。
若未作特殊说明,本作品采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。