1 条题解
-
0
「PKUWC2018」随机游走 题解
问题描述
给定一棵 个节点的树,从起点 出发随机游走(每次等概率选择相邻边)。 次询问,每次给定集合 ,求首次覆盖 中所有节点的期望游走步数(起点 初始已覆盖)。答案对 取模。
关键约束:(支持状态压缩),(需预处理所有子集答案)。
核心思路
解决该问题需结合 Min-Max 容斥、树形 DP 求期望 和 子集和变换,核心是将“覆盖所有节点的期望”转化为“首次击中子集的期望”的组合,再通过预处理实现快速查询。
一、Min-Max 容斥:转化期望问题
“覆盖 中所有节点的时间”等价于“ 中所有节点首次被访问的时间的最大值”(记为 , 是节点 的首次访问时间)。
$$\mathbb{E}[\max_{u \in S} X_u] = \sum_{\emptyset \neq T \subseteq S} (-1)^{|T|+1} \cdot \mathbb{E}[\min_{u \in T} X_u] $$
Min-Max 容斥公式将“最大值的期望”转化为“子集最小值的期望”的线性组合:其中 表示“首次访问到 中任意一个节点的期望步数”(记为 , 是起点)。
二、树形 DP:计算
对于任意子集 (记为
mask
),需计算 (从 出发首次击中 的期望步数)。由于树无环,可通过 树形 DP 避免高斯消元,将每个节点的期望表示为父节点期望的线性函数,高效递推求解。1. 期望方程建立
设 为从节点 出发首次击中 的期望步数:
- 若 (已在目标子集),则 ;
- 若 , 有 个邻居,每次等概率选择一个,故:$$E[u] = 1 + \frac{1}{d} \sum_{v \in \text{adj}[u]} E[v] $$( 是当前步数, 是邻居期望的平均值)。
2. 线性函数递推
由于树的层级结构,将 表示为父节点 的线性函数 ( 是常数项, 是父节点系数):
- 若 :,;
- 若 :设 为 的度数,,。代入期望方程整理得:$$A[u] = \frac{d + sA}{d - sB}, \quad B[u] = \frac{1}{d - sB} $$(除法通过模逆元计算, 是质数,逆元为 )。
3. 根节点求解
起点 无父节点( 不存在),故 ,即 。
三、子集和变换:预处理所有询问答案
根据 Min-Max 容斥公式,需计算每个子集 的答案:
$$\text{Ans}(S) = \sum_{\emptyset \neq T \subseteq S} (-1)^{|T|+1} \cdot E_T(x) $$通过 符号调整 和 子集和变换 预处理所有 的答案:
- 符号调整:对每个子集 ,令 (代码中通过“偶数大小子集乘 ”实现);
- 子集和变换:对 做“子集前缀和”,使 ,此时 即为 (直接对应询问答案)。
代码解析
1. 模运算封装
MInt
类封装了模 的加减乘除(含逆元),避免手动处理模运算细节。2. 树形 DP 计算
对每个
mask
(子集 ),通过 DFS 递推每个节点的 和 ,最终 (即 ):for (int mask = 0; mask < 1 << n; mask++) { auto dfs0 = [&](auto &&self, int u, int p = -1) -> void { Z sA = 0, sB = 0; int d = adj[u].size(); for (auto v : adj[u]) { if (v == p) continue; self(self, v, u); sA += A[v]; sB += B[v]; } if (mask >> u & 1) { // u ∈ T,E[u] = 0 A[u] = B[u] = 0; } else if (d == sB) { // 理论上不会出现(无可行路径) A[u] = B[u] = 0; } else { // 线性函数系数计算 A[u] = (d + sA) / (d - sB); B[u] = Z(1) / (d - sB); } }; dfs0(dfs0, x); f[mask] = A[x]; // E_T(x) = A[x] }
3. 符号调整与子集和变换
// 符号调整:f[mask] = E_T(x) * (-1)^(|mask|+1) for (int i = 0; i < 1 << n; i++) { if (__builtin_parity(i) == 0) { // 偶数大小子集,乘 -1 f[i] *= -1; } } // 子集和变换:f[mask] = sum_{T⊆mask} f[T] for (int i = 0; i < n; i++) { for (int j = 0; j < 1 << n; j++) { if (j >> i & 1) { // j 包含 i,累加 j^i(去掉 i 的子集) f[j] += f[j ^ (1 << i)]; } } }
4. 处理询问
将询问的集合 转为二进制
mask
,直接输出 :while (q--) { int mask = 0, k; cin >> k; for (int i = 0; i < k; i++) { int x; cin >> x; mask |= 1 << (x - 1); // 转为 0-based } cout << f[mask] << "\n"; }
复杂度分析
- 预处理:
- 树形 DP:(,);
- 子集和变换:(同树形 DP 量级)。
- 询问:(,每次直接查预处理结果)。
总复杂度高效,可满足题目约束。
总结
本题的核心是通过 Min-Max 容斥将复杂的“全覆盖期望”转化为可计算的“子集击中期望”,再利用树的结构特性通过线性函数递推避免高斯消元,最后通过子集和变换预处理所有答案,实现快速查询。该思路充分利用了 的状态压缩优势,是小节点数树上期望问题的经典解法。
- 1
信息
- ID
- 3512
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者