This commit is contained in:
Zengtudor 2024-11-12 12:09:23 +08:00
parent dfdf354730
commit 90dd8348fe
9 changed files with 201678 additions and 57 deletions

View File

@ -1,3 +0,0 @@
int main(){
}

View File

@ -1,71 +1,421 @@
这个问题涉及树的遍历和动态更新,需要我们计算路径得分的期望。由于每个修改都可能影响某条路径的期望得分,因此要高效地处理初始计算和后续更新。
### 问题分析
### 解题思路:
该问题涉及在树结构上进行随机行走,并求解路径的期望得分。核心是维护每个节点的得分期望贡献,尤其在节点被修改时,能够快速更新。由于树结构和动态更新的复杂性,问题需要高效的数据结构和算法支持。
1. **树的构建**
- 给定父节点数组,可以构建一棵以 $1$ 为根的有根树。用一个邻接表来存储子节点信息。
### 解决思路
2. **初始状态下期望得分的计算**
- 使用深度优先搜索DFS计算期望得分。
- 从根节点出发,递归计算每个节点的得分期望,遵循给定的转移概率 $\frac{w_v}{\text{sum}_u}$。
- 对于一个节点 $u$,其期望得分 $E(u)$ 可以递归计算为:
\[
E(u) = a_u + \sum_{v \in \text{children of } u} \frac{w_v}{\text{sum}_u} \cdot E(v)
\]
1. **期望线性性**
- 可以将整个树上的路径得分期望表示为每个节点的期望贡献之和,即
$$
E = \sum_{u=1}^{n} P(u) \times a_u
$$
其中 $P(u)$ 是从根节点到节点 $u$ 的概率。
3. **处理修改操作**
- 每次修改某个节点 $u$ 的 $w$ 或 $a$ 值后,需要更新与 $u$ 相关的所有路径的期望。
- 直接重新运行DFS在大规模数据$n, q \leq 100,000$)中代价过高,因此需使用某种增量更新方法。
- 可以使用**树链剖分**或**树状数组**来高效更新受影响的子树或路径。
2. **概率计算**
- 使用 DFS 来预处理从根节点到每个节点 $u$ 的概率 $P(u)$。如果 $p_u$ 是 $u$ 的父节点,且 $sum_{p_u}$ 是 $p_u$ 所有子节点 $w$ 的和,那么
$$
P(v) = P(p_u) \times \frac{w_v}{sum_{p_u}}
$$
对于根节点,$P(1) = 1$。
4. **取模运算**
- 由于结果需要对 $998244353$ 取模,所有的运算,包括除法,都需要用模逆元处理
- 对于 $p$ 是素数,$x$ 的逆元为 $x^{p-2} \bmod p$,使用快速幂算法计算。
3. **修改更新**
- 每次修改节点的 $w$ 或 $a$ 会影响其自身及所有子节点的期望贡献。因此,需要更新整个子树的 $P(v)$ 值
- 如果修改了一个非叶节点的 $w$ 值,所有直接或间接影响的子节点 $v$ 的 $P(v)$ 需要重新计算。
### 具体步骤:
4. **数据结构**
- 使用 DFS 序和线段树来维护每个节点的 $P(v)$ 和其对应的期望贡献。这样,在修改时,可以在 $O(\log n)$ 的时间复杂度内实现区间更新。
- 使用线段树支持区间乘法和加法,以便在 $P(v)$ 更新时调整其子树。
1. **预处理**
- 构建树并计算初始期望得分 $E(1)$。
- 用 DFS 遍历每个节点计算期望得分的初始值。
### 实现步骤
2. **更新操作**
- 修改 $u$ 的 $w$ 或 $a$ 值后,调整 $u$ 的父节点期望,并递归更新到根节点
- 使用自底向上的路径更新机制,通过父节点的影响逐步更新
1. **DFS 预处理**
- 用 DFS 遍历整棵树,计算初始的 $P(v)$,并记录每个节点的进入和离开时间(用于 DFS 序)
- 记录每个节点的 $sum_u$,用于计算 $P(v)$
3. **实现细节**
- 采用链式前向星或邻接表存储树结构。
- 使用分块思想或其他树状数据结构如**树链剖分**,以便快速定位并更新路径。
2. **线段树维护**
- 构建一个支持区间乘法和加法的线段树,初始化节点贡献 $P(v) \times a_v$。
- 在线段树中,维护两个操作:
- 区间乘法,用于当 $w$ 被修改时更新概率 $P(v)$。
- 单点更新,用于修改 $a_v$ 值。
### 复杂度分析:
3. **修改操作**
- 修改 $w_u$ 时,更新其父节点 $sum_{p_u}$ 并通过 DFS 序更新子树中所有节点的 $P(v)$。
- 修改 $a_u$ 时,仅需单点更新其贡献。
- **时间复杂度**:初始期望计算为 $O(n)$,每次修改和更新平均为 $O(\log n)$ 或更低,具体取决于使用的数据结构。
- **空间复杂度**$O(n)$ 用于存储树结构和权值数组
4. **输出期望**
- 每次修改后,遍历所有节点并计算新的期望值,输出对 $998244353$ 取模的结果
### 实现关键点:
### 伪代码实现
- **DFS 计算期望**
```cpp
void dfs(int u) {
E[u] = a[u];
for (int v : children[u]) {
dfs(v);
E[u] += (w[v] * E[v]) / sum_u; // 注意使用模逆元
}
}
```
```cpp
// 假设有一个支持区间乘法和单点更新的线段树类 SegmentTree
- **更新期望**
```cpp
void update(int u, int new_w, int new_a) {
w[u] = new_w;
a[u] = new_a;
// 使用树状数组或其他机制更新相关路径的期望
}
```
void dfs(int u, double prob) {
P[u] = prob;
for (int v : children[u]) {
dfs(v, prob * (w[v] / sum_w[u]));
}
}
### 注意事项:
void update_w(int u, int new_w) {
int p = parent[u];
double ratio = new_w / (double)w[u];
w[u] = new_w;
// 更新其父节点的 sum_w
sum_w[p] = sum_w[p] - old_w + new_w;
segmentTree.update_multiply(subtree[u], ratio);
}
- 确保处理浮点除法时使用模运算。
- 根据问题的规模,考虑使用平衡树或线段树来优化更新操作。
void update_a(int u, int new_a) {
a[u] = new_a;
segmentTree.update_point(u, P[u] * a[u]);
}
这个解法需要巧妙地结合树结构和概率论来计算路径上的期望,并进行高效的更新。
// 主程序
int main() {
input();
dfs(1, 1.0);
segmentTree.build(P, a);
output_initial_expectation();
for (int i = 0; i < q; ++i) {
int u, new_w, new_a;
cin >> u >> new_w >> new_a;
update_w(u, new_w);
update_a(u, new_a);
output_current_expectation();
}
}
```
### 复杂度分析
- **预处理**$O(n)$,用于 DFS 和初始化线段树。
- **每次修改**$O(\log n)$,因为线段树支持高效的区间乘法和单点更新。
- **总复杂度**$O((n + q) \log n)$,对于 $n, q \leq 10^5$ 是可行的。
### 优化细节
- 采用快速幂和乘法逆元来高效计算取模运算。
- 使用树链剖分等高级树分解技术进一步优化 DFS 序区间更新。
这样,这道题的复杂性和实现细节就明确了。
编写一个支持区间乘法的线段树需要在每个节点中维护一个乘法懒标记。通过懒标记机制,在进行区间乘法更新时,可以保证修改操作的复杂度为 $ O(\log n) $。
以下是实现一个支持区间乘法和区间求和的线段树的思路:
### 线段树的设计思路
1. **节点信息**
- 每个节点保存区间和 `sum`
- 每个节点保存区间乘法懒标记 `mul_lazy`,用于延迟更新。
2. **懒标记传播**
- 在访问节点时,将懒标记向下传播到子节点,确保更新操作只需在 $ O(\log n) $ 时间内完成。
3. **更新操作**
- 乘法更新操作会将某个区间内的值都乘以一个给定数。
- 传播时,将父节点的乘法懒标记应用到子节点,并更新子节点的懒标记。
### 实现步骤
1. **初始化**
- 初始化线段树和懒标记为 1乘法恒等元
2. **下推操作**`push_down`
- 将懒标记从当前节点向子节点传播。
- 更新子节点的和以及懒标记。
3. **更新操作**`update_range`
- 更新指定区间的乘法值。
4. **查询操作**`query_range`
- 查询指定区间的和。
### 代码实现
以下是一个 C++ 实现的示例:
```cpp
#include <vector>
const int MOD = 998244353;
int mod_mult(int a, int b) {
return (1LL * a * b) % MOD;
}
class SegmentTree {
private:
std::vector<int> tree, mul_lazy;
int n;
void push_down(int node, int start, int end) {
if (mul_lazy[node] != 1) {
tree[node] = mod_mult(tree[node], mul_lazy[node]);
if (start != end) {
mul_lazy[node * 2] = mod_mult(mul_lazy[node * 2], mul_lazy[node]);
mul_lazy[node * 2 + 1] = mod_mult(mul_lazy[node * 2 + 1], mul_lazy[node]);
}
mul_lazy[node] = 1;
}
}
void update_range(int node, int start, int end, int l, int r, int value) {
push_down(node, start, end);
if (start > r || end < l) {
return; // Out of range
}
if (start >= l && end <= r) {
mul_lazy[node] = mod_mult(mul_lazy[node], value);
push_down(node, start, end);
return;
}
int mid = (start + end) / 2;
update_range(node * 2, start, mid, l, r, value);
update_range(node * 2 + 1, mid + 1, end, l, r, value);
tree[node] = (tree[node * 2] + tree[node * 2 + 1]) % MOD;
}
int query_range(int node, int start, int end, int l, int r) {
push_down(node, start, end);
if (start > r || end < l) {
return 0; // Out of range
}
if (start >= l && end <= r) {
return tree[node];
}
int mid = (start + end) / 2;
int left_sum = query_range(node * 2, start, mid, l, r);
int right_sum = query_range(node * 2 + 1, mid + 1, end, l, r);
return (left_sum + right_sum) % MOD;
}
public:
SegmentTree(int size) : n(size) {
tree.resize(4 * n, 0);
mul_lazy.resize(4 * n, 1);
}
void build(const std::vector<int>& arr, int node, int start, int end) {
if (start == end) {
tree[node] = arr[start] % MOD;
} else {
int mid = (start + end) / 2;
build(arr, node * 2, start, mid);
build(arr, node * 2 + 1, mid + 1, end);
tree[node] = (tree[node * 2] + tree[node * 2 + 1]) % MOD;
}
}
void update_range(int l, int r, int value) {
update_range(1, 0, n - 1, l, r, value);
}
int query_range(int l, int r) {
return query_range(1, 0, n - 1, l, r);
}
};
```
### 使用方法
1. 初始化线段树并构建:
```cpp
int n = 10;
std::vector<int> arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
SegmentTree segTree(n);
segTree.build(arr, 1, 0, n - 1);
```
2. 更新区间乘法:
```cpp
segTree.update_range(2, 5, 3); // 将索引 2 到 5 的值都乘以 3
```
3. 查询区间和:
```cpp
int result = segTree.query_range(2, 5);
```
### 说明
- **懒标记初始化**:在构造函数中,`mul_lazy` 初始为 1这是因为乘法的单位元是 1。
- **乘法取模**:保证所有乘法操作对 $ MOD $ 取模,以避免溢出。
这种实现能够高效地进行区间乘法更新和区间和查询,满足题目对于修改和查询的性能要求。
如果采用朴素方法实现该问题,虽然在大规模数据下效率较低,但仍能帮助理解问题并实现一些基本功能。朴素方法的核心思想是直接用递归计算从根节点到所有叶节点的期望得分,每次修改后重新计算。这种方法在小规模数据下仍然可行。
### 思路
1. **初始计算**
- 通过深度优先搜索DFS遍历树计算从根节点到每个叶节点的路径得分及其概率。
- 用这些路径的加权得分求得初始期望值。
2. **修改操作**
- 每次修改节点的权值和得分时,重新计算整个树的期望得分。
- 由于每次修改都需要重新遍历树,复杂度是 $ O(n) $,因此当 $ n $ 和 $ q $ 较大时会超时。
### 实现步骤
1. 使用 DFS 遍历整个树来计算期望。
2. 在每次修改操作后,重新运行 DFS 计算新期望。
### 代码实现
以下是实现的 C++ 示例代码:
```cpp
#include <iostream>
#include <vector>
const int MOD = 998244353;
struct Node {
int w, a;
std::vector<int> children;
};
std::vector<Node> tree;
int n;
// 递归计算从当前节点开始的期望得分
double dfs(int u, double prob) {
if (tree[u].children.empty()) {
// 叶节点,返回其得分乘以概率
return prob * tree[u].a;
}
double sum_u = 0;
for (int v : tree[u].children) {
sum_u += tree[v].w;
}
double expected_score = 0;
for (int v : tree[u].children) {
double new_prob = prob * (tree[v].w / sum_u);
expected_score += dfs(v, new_prob);
}
// 当前节点的得分也要加上
return expected_score + prob * tree[u].a;
}
// 修改节点的权值和得分
void modify_node(int u, int new_w, int new_a) {
tree[u].w = new_w;
tree[u].a = new_a;
}
int main() {
std::cin >> n;
tree.resize(n + 1);
// 输入父节点信息,构建树结构
for (int i = 2; i <= n; ++i) {
int parent;
std::cin >> parent;
tree[parent].children.push_back(i);
}
// 输入每个节点的权值
for (int i = 1; i <= n; ++i) {
std::cin >> tree[i].w;
}
// 输入每个节点的得分
for (int i = 1; i <= n; ++i) {
std::cin >> tree[i].a;
}
// 初始计算期望得分
double initial_expectation = dfs(1, 1.0);
std::cout << static_cast<int>(initial_expectation) % MOD << std::endl;
int q;
std::cin >> q;
// 每次修改后重新计算期望得分
for (int i = 0; i < q; ++i) {
int u, new_w, new_a;
std::cin >> u >> new_w >> new_a;
modify_node(u, new_w, new_a);
double new_expectation = dfs(1, 1.0);
std::cout << static_cast<int>(new_expectation) % MOD << std::endl;
}
return 0;
}
```
### 说明
- **递归计算**`dfs` 函数从根节点开始递归,计算从根到每个叶节点的期望得分,使用参数 `prob` 表示当前路径的累积概率。
- **修改操作**`modify_node` 函数用于修改节点的 `w``a` 值。每次修改后重新调用 `dfs` 更新期望得分。
- **复杂度**:每次修改需要 $ O(n) $ 时间重新计算期望,所以总复杂度是 $ O(q \times n) $。
### 优化空间
在较小的数据集上,朴素方法可以正常工作。但随着 $ n $ 和 $ q $ 增大,需要用更高效的数据结构(如线段树)来加速更新和查询,以减少重复计算。
在这类问题中,确实涉及到模意义下的除法操作,因为在计算概率时,我们需要用到分数,而在模运算中直接进行除法是不允许的。因此,我们需要用**模逆元**来实现模意义下的除法。
### 模逆元简介
给定一个数 $ a $ 和一个模 $ m $$ a $ 在模 $ m $ 意义下的逆元 $ a^{-1} $ 满足:
$$
a \cdot a^{-1} \equiv 1 \pmod{m}
$$
如果我们想计算 $ \frac{a}{b} $ 在模 $ m $ 意义下的结果,可以将其表示为:
$$
\frac{a}{b} \equiv a \cdot b^{-1} \pmod{m}
$$
其中 $ b^{-1} $ 是 $ b $ 的模逆元。
### 计算模逆元
对于模数 $ m $ 是质数的情况,我们可以使用**费马小定理**来计算模逆元。费马小定理指出:
$$
b^{m-1} \equiv 1 \pmod{m}
$$
因此:
$$
b^{-1} \equiv b^{m-2} \pmod{m}
$$
对于模 $ 998244353 $,可以通过快速幂算法来计算 $ b^{998244351} $ 来求得 $ b $ 的逆元。
### 快速幂算法实现
以下是 C++ 代码,用于计算 $ a $ 的 $ b $ 次幂在模 $ m $ 下的结果:
```cpp
// 快速幂计算 a^b % m
long long mod_pow(long long a, long long b, long long m) {
long long result = 1;
a = a % m;
while (b > 0) {
if (b % 2 == 1) {
result = (result * a) % m;
}
a = (a * a) % m;
b /= 2;
}
return result;
}
// 计算 b 在模 m 下的逆元
long long mod_inverse(long long b, long long m) {
return mod_pow(b, m - 2, m);
}
```
### 应用到问题中
在计算路径概率时,如果需要计算 $ \frac{w_v}{sum_u} $,可以通过乘以 $ sum_u $ 的模逆元来实现:
```cpp
// 计算概率时使用模逆元
long long sum_u_inverse = mod_inverse(sum_u, 998244353);
long long prob = (w_v * sum_u_inverse) % 998244353;
```
这样,我们就能在模 $ 998244353 $ 的意义下正确地进行除法操作,从而保证计算结果符合题意。

View File

@ -0,0 +1,102 @@
#include <algorithm>
#include <cstdint>
#include <iostream>
#include <ranges>
#include <vector>
using i64 = int64_t;
using i32 = int32_t;
template<class ...Args>
void input(Args&&...args){
(std::cin>>...>>args);
}
template<class T>
T input(){
T tmp;
std::cin>>tmp;
return tmp;
}
template<class ...Args>
void print(Args...args){
(std::cout<<...<<args);
}
const i64 max_n = 100000 + 5, mod{998244353};
i64 w[max_n], a[max_n];
std::vector<i64> sons[max_n];
i64 mod_pow(i64 b, i64 e, const i64 m = mod){
b %= mod;
i64 res {1};
while(e!=0){
if(e&1){
res = (res * b) % mod;
}
b = (b * b) % mod;
e>>=1;
}
return res % mod;
}
i64 inverse(const i64 b, const i64 m = mod){
return mod_pow(b, m-2, m);
}
i64 times_mod(const i64 a, const i64 b, const i64 m = mod){
return ((a % m) * inverse(b)) % m;
}
i64 dfs(const i64 u, const i64 prob){
if(sons[u].empty()){
return (prob * a[u]) % mod;
}
const i64 sum_u = [&u]()->i64{
i64 ret{};
for(const i64 v:sons[u]){
ret = (ret + w[v]) % mod;
}
return ret;
}();
const i64 exp_score = [&prob, &sum_u, &u]()->i64{
i64 ret {};
for(const auto v:sons[u]){
// const i64 new_prob = prob * w[v] / sum_u;
const i64 new_prob = times_mod(prob * w[v], sum_u);
ret = (ret + dfs(v, new_prob)) % mod;
}
return ret;
}();
return (exp_score + prob * a[u]) % mod;
}
int main(){
const i64 n = input<i64>();
for(const i64 i:std::ranges::views::iota(2,n+1)){
const i64 father = input<i64>();
sons[father].push_back(i);
}
std::for_each_n(w+1, n,[](i64 &wi){
input(wi);
});
std::for_each_n(a+1, n, [](i64 &ai){
input(ai);
});
print(dfs(1, 1), '\n');
const i64 q = input<i64>();
for(const auto _ : std::ranges::views::iota(0ll,q)){
const i64 u{input<i64>()};
w[u] = input<i64>();
a[u] = input<i64>();
print(dfs(1,1),'\n');
}
}

View File

@ -0,0 +1,5 @@
142606350
199648884
15
14
623902734

View File

@ -0,0 +1,9 @@
5
1 2 1 4
4 2 2 5 4
5 1 5 5 5
4
4 3 5
2 1 5
1 2 4
2 5 4

View File

@ -0,0 +1,147 @@
420073041
49916562
678106962
573268758
190001817
878167545
592136533
884607562
607487527
719905061
133536999
532358867
149034111
118662330
150377816
980513181
498915225
28060872
793317792
428148517
527516472
314751936
824324291
648780196
44512054
413449561
357301244
835232862
560528403
519964614
527774346
106351624
493988109
88446900
740307206
260194681
354363306
311623486
76571543
214303059
434868970
606656968
108704799
369058577
943974456
78602051
399628612
929014106
10981475
126760155
972630682
827436902
317194824
34820943
377332463
408168954
396418336
723943021
908070236
646178062
764152921
579589201
685304213
196457663
281574862
319300824
295167524
718206426
224401868
723476666
557541727
570865809
775927289
980074782
326455940
489583050
62972366
265349140
328614099
868056732
64698898
987639565
760333714
208254812
165046701
636934198
833378247
271970552
816621176
660236095
269165835
876942794
342187932
581644114
643277136
990608669
690072504
202897650
908733893
725531263
243876378
886380510
62662830
778835106
972784931
981403802
83097387
135482980
681818673
760528212
278700302
538312719
793855722
34870169
395810424
382459720
646833256
362100097
129485759
557337814
752252711
847685194
68894248
995906251
722834286
970495811
170778044
978733573
276532756
838185304
247689968
259043003
257728137
314660975
764323418
136623146
270673192
436574148
920333550
833351641
229807625
113061600
795822393
457164237
54258695
132517261
216939997

1005
src/20241111/climb/climb2.in Normal file

File diff suppressed because one or more lines are too long

100001
src/20241111/climb/climb3.ans Normal file

File diff suppressed because it is too large Load Diff

100005
src/20241111/climb/climb3.in Normal file

File diff suppressed because one or more lines are too long