1 条题解
-
0
题目分析
给定一棵 个节点的有根树,节点编号为 ,根为 。树满足特殊性质:对于任意 ,有 ,其中 是节点 的父节点。
进行 次询问,每次询问区间 ,要求计算:
$$\left(\sum_{x=l}^r \sum_{y=l}^r [l \leq \text{LCA}(x,y) \leq r] \cdot a_x \cdot a_y\right) \bmod 998244353 $$核心思路
1. 问题转化
数学推导: 所求式子可以重写为:
$$\sum_{z=l}^r \left(\sum_{x \in \text{subtree}(z) \cap [l,r]} a_x\right)^2 $$其中 表示 的子树中所有节点。
直观理解:对于每个可能的LCA节点 ,统计以其为LCA的所有点对的权值乘积之和。
2. 关键观察
- 由于树的性质 ,节点编号大致按DFS序排列
- 每个节点 的子树在编号上是一个连续区间
- 对于询问 ,有效的 满足 且
算法框架
1. 整体架构
算法采用 重链剖分 + 分块 的混合策略:
- 重链剖分:快速处理树上路径和祖先查询
- 分块处理:平衡预处理和查询的复杂度
2. 关键数据结构
// 核心数据结构定义 int prv[N]; // 父节点数组 vector<int> e[N]; // 邻接表 int dfn[N], note[N], sign; // DFS序相关 int son[N], dep[N], mxd[N]; // 重链剖分信息 int top[N]; // 链顶节点 int pf[N][19]; // 倍增数组算法实现详解
1. 重链剖分预处理
算法原理: 重链剖分将树划分为若干条链,使得树上路径查询可以转化为 条链上的操作。
代码实现:
void dfs(int x) { mxd[x] = 0; // 构建倍增数组,用于快速查询k级祖先 pf[x][0] = prv[x]; for (int i = 1; i < 19; i++) pf[x][i] = pf[pf[x][i - 1]][i - 1]; // 确定重儿子(子树最深的儿子) for (int to : e[x]) { dep[to] = dep[x] + 1; dfs(to); if (gmax(mxd[x], mxd[to] + 1)) son[x] = to; } } void getdfn(int x) { note[dfn[x] = ++sign] = x; // 如果是链的起点,预处理整条链的信息 if (x != son[prv[x]]) { top[x] = x; // 存储从x向上到根路径上的节点 for (int i = 0, p = x; i <= mxd[x]; i++, p = prv[p]) f[x][i] = p; } else { top[x] = top[prv[x]]; // 继承父节点的链顶 } // 优先遍历重儿子,保证重链上节点DFS序连续 if (son[x]) getdfn(son[x]); for (int to : e[x]) if (to != son[x]) getdfn(to); }2. 分块预处理
算法原理: 将序列分成 大小的块,对每个块预处理子树平方和的前缀和,实现查询时的快速响应。
代码实现:
const int B = 700; // 块大小,平衡预处理和查询 void init() { // 建立分块 for (int i = 1, j = 1; i <= n; i += B, j++) { L[j] = i, R[j] = min(i + B - 1, n); for (int k = L[j]; k <= R[j]; k++) bl[k] = j; } // 对每个块预处理子树平方和 for (int i = 1; i <= bl[n]; i++) { getsum(1, R[i]); // 计算子树和 Ans[i].resize(R[i] + 1); S[i].resize(R[i] + 1); // 构建前缀平方和数组 for (int j = 1; j <= R[i]; j++) { Ans[i][j] = 1ll * sum[j] * sum[j] % mod; S[i][j] = sum[j]; (Ans[i][j] += Ans[i][j - 1]) % = mod; } } }3. 关键函数:k级祖先查询
算法原理: 结合倍增和重链剖分,实现 的k级祖先查询。
代码实现:
int kth(int x, int k) { if (k == 0) return x; // 第一步:使用倍增跳到2的幂次位置 int Lg = __lg(k); x = pf[x][Lg]; k -= (1 << Lg); // 第二步:在重链上直接跳跃 if (k <= dfn[x] - dfn[top[x]]) return note[dfn[x] - k]; // 在同一重链内 // 第三步:跳到链顶后继续处理 return f[top[x]][k - (dfn[x] - dfn[top[x]])]; }4. 查询处理核心逻辑
算法流程:
- 使用预处理结果快速计算完整块内的答案
- 对剩余部分暴力计算,但利用重链剖分加速LCA查询
代码实现:
int query(int l, int r) { int t = bl[r] - 1; // 最后一个完整块 int pos = min(r, G[l]); // 有效查询范围 li res = 0; // 步骤1:使用预处理的块内结果 if (l <= R[t]) res = (Ans[t][min(pos, R[t])] + mod - Ans[t][l - 1]) % mod; // 步骤2:暴力处理剩余部分,但使用优化 auto getpnt = [&](int x) { // 利用重链剖分快速找到在区间内的最近祖先 if (dep[x] == dep[l]) return x; int p = x, k = dep[x] - dep[l] - 1; if (k > 0) { int Lg = __lg(k); p = pf[p][Lg]; // 倍增跳跃 // 在重链上精确定位 int q = top[p]; if (dep[l] + 1 >= dep[q]) p = note[dfn[q] + dep[l] + 1 - dep[q]]; else p = f[q][dep[q] - (dep[l] + 1)]; } // 确保找到的祖先在查询区间内 if (prv[p] >= l) p = prv[p]; return p; }; // 对块外元素逐个处理 for (int i = max(l, R[t] + 1); i <= r; i++) { int p = getpnt(i); // 快速找到候选LCA // 动态维护子树和并更新答案 if (ns[p] == -1) { clr[++tot] = p; // 初始化:使用预处理值或从0开始 ns[p] = (p <= R[t]) ? S[t][p] : 0; } // 核心:利用公式 (sum + a)^2 = sum^2 + 2*sum*a + a^2 res += 2ll * ns[p] * a[i] + 1ll * a[i] * a[i]; ns[p] = (ns[p] + a[i]) % mod; // 更新子树和 } // 清空临时状态 for (int i = 1; i <= tot; i++) ns[clr[i]] = -1; tot = 0; return res % mod; }复杂度分析
时间复杂度
- 预处理阶段:
- 重链剖分:
- 倍增数组:
- 分块预处理:
- 单次查询:
- 块内部分:
- 块外部分:
- 总复杂度:
取 ,得到
空间复杂度
- 主要开销: 用于存储预处理信息
算法亮点总结
1. 混合优化策略
- 重链剖分解决树上路径问题
- 分块平衡预处理和查询开销
- 倍增提供快速祖先查询
2. 数学优化
- 利用平方和公式 增量更新
- 避免重复计算,提高效率
3. 工程优化
- 内存池技术减少动态分配
- DFS序连续性利用
- 临时状态快速清空
总结
本题通过巧妙的算法组合,成功解决了大规模树上区间统计问题。重链剖分提供了高效的树上操作基础,分块技术在预处理和查询之间找到了最佳平衡点,而数学优化则大大减少了计算量。这种"重武器+轻技巧"的组合策略,在处理复杂数据结构问题时具有很好的参考价值。
- 1
信息
- ID
- 3313
- 时间
- 2000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 15
- 已通过
- 1
- 上传者