[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]\)。所以有

\[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;
}
posted @ 2024-05-12 15:50  XuYueming  阅读(17)  评论(0编辑  收藏  举报