1 条题解
-
0
算法标签
- DFS 序 + 线段树/树状数组
- 欧拉序处理
- 区间更新 + 单点查询
题目分析
这个问题需要动态维护二叉树的遍历顺序,其中每个节点有一个标记值 控制该节点在遍历中的位置(在子节点之前/之间/之后访问)。
关键观察
-
遍历顺序的递归定义:
- 如果 :顺序为
[当前节点] + [左子树遍历] + [右子树遍历] - 如果 :顺序为
[左子树遍历] + [当前节点] + [右子树遍历] - 如果 :顺序为
[左子树遍历] + [右子树遍历] + [当前节点]
- 如果 :顺序为
-
子树大小计算: 对于节点 ,其子树大小 是固定的(不随 变化),但节点在遍历中的位置取决于:
- 祖先节点的 值(决定当前节点在祖先的子树中的相对位置)
- 左子树的大小(如果 或 ,当前节点在左子树之后)
-
动态维护挑战: 修改一个节点的 值会影响其所有后代节点在遍历中的位置,因此需要高效的数据结构。
解决方案
使用 DFS 序 + 线段树 来维护:
-
预处理:
- 对树进行 DFS,记录每个节点的入序 和出序
- 计算每个节点的子树大小
- 记录父节点信息
-
数据结构设计: 维护两棵线段树:
- 标记树:存储每个节点的 值,支持区间赋值
- 偏移树:存储由于祖先节点的 值变化导致的遍历位置偏移量
-
位置计算: 节点 的位置 = 1 + 所有在 之前被访问的节点数量 可以通过在 DFS 序上维护前缀和来快速计算
题解代码
#include <bits/stdc++.h> using namespace std; const int MAXN = 100005; int n, q; int L[MAXN], R[MAXN]; int x[MAXN]; // 每个节点的标记值,初始为-1 // DFS序相关 int in[MAXN], out[MAXN], dfs_time; int parent[MAXN]; vector<int> children[MAXN]; // 子树大小 int sz[MAXN]; // 线段树:区间赋值,单点查询 struct SegmentTree { int tree[4 * MAXN]; int lazy[4 * MAXN]; void build(int idx, int l, int r) { lazy[idx] = -2; // 无标记 if (l == r) { tree[idx] = -1; // 初始值为-1 return; } int mid = (l + r) / 2; build(idx * 2, l, mid); build(idx * 2 + 1, mid + 1, r); } void push_down(int idx, int l, int r) { if (lazy[idx] != -2) { tree[idx] = lazy[idx]; if (l != r) { lazy[idx * 2] = lazy[idx]; lazy[idx * 2 + 1] = lazy[idx]; } lazy[idx] = -2; } } void update(int idx, int l, int r, int ql, int qr, int val) { push_down(idx, l, r); if (ql > r || qr < l) return; if (ql <= l && r <= qr) { lazy[idx] = val; push_down(idx, l, r); return; } int mid = (l + r) / 2; update(idx * 2, l, mid, ql, qr, val); update(idx * 2 + 1, mid + 1, r, ql, qr, val); } int query(int idx, int l, int r, int pos) { push_down(idx, l, r); if (l == r) return tree[idx]; int mid = (l + r) / 2; if (pos <= mid) return query(idx * 2, l, mid, pos); else return query(idx * 2 + 1, mid + 1, r, pos); } } seg_x; void dfs(int u, int p) { parent[u] = p; in[u] = ++dfs_time; sz[u] = 1; if (L[u] != 0) { children[u].push_back(L[u]); dfs(L[u], u); sz[u] += sz[L[u]]; } if (R[u] != 0) { children[u].push_back(R[u]); dfs(R[u], u); sz[u] += sz[R[u]]; } out[u] = dfs_time; } // 计算节点u在其子树中的相对位置 int get_relative_position(int u) { int pos = 1; // 从1开始计数 int current_x = seg_x.query(1, 1, n, u); if (L[u] != 0) { if (current_x == -1) { // 当前节点在最前,先加自己 pos += 0; // 自己还没算 } else { // 当前节点在中间或后面,左子树在前 pos += sz[L[u]]; } } if (R[u] != 0 && current_x != 1) { // 如果当前节点不在最后,右子树在当前位置之后 pos += sz[R[u]]; } return pos; } // 计算节点u在整个遍历中的绝对位置 int get_absolute_position(int u) { int pos = 1; // 从1开始 // 从u向上到根,计算每个祖先的贡献 vector<int> path; int cur = u; while (cur != 0) { path.push_back(cur); cur = parent[cur]; } reverse(path.begin(), path.end()); for (int i = 0; i < path.size(); i++) { int node = path[i]; if (node == u) { // 到达目标节点,加上在其子树中的相对位置 pos += get_relative_position(node) - 1; break; } int current_x = seg_x.query(1, 1, n, node); // 左子树的贡献 if (L[node] != 0 && L[node] != u && in[L[node]] <= in[u] && in[u] <= out[L[node]]) { // u在左子树中 if (current_x == -1) { // 当前节点在最前,左子树在节点之后 pos += 1; // 跳过当前节点 } // 继续在左子树中搜索 continue; } // 右子树的贡献 if (R[node] != 0 && R[node] != u && in[R[node]] <= in[u] && in[u] <= out[R[node]]) { // u在右子树中 if (current_x == -1) { pos += 1 + (L[node] != 0 ? sz[L[node]] : 0); } else if (current_x == 0) { pos += (L[node] != 0 ? sz[L[node]] : 0) + 1; } // current_x == 1: 右子树在最前,已在前面计算 continue; } // u不在当前节点的直接子树中,加上整个左子树 if (L[node] != 0 && in[u] > out[L[node]]) { pos += sz[L[node]]; } // 加上当前节点(如果在u之前) if (current_x != 1 && node != u) { pos += 1; } // 加上整个右子树(如果在u之前) if (R[node] != 0 && in[u] > out[R[node]] && current_x != 1) { pos += sz[R[node]]; } } return pos; } int main() { ios::sync_with_stdio(false); cin.tie(0); cin >> n >> q; for (int i = 1; i <= n; i++) { cin >> L[i] >> R[i]; x[i] = -1; // 初始值 } // DFS预处理 dfs_time = 0; dfs(1, 0); // 初始化线段树 seg_x.build(1, 1, n); // 处理查询 for (int i = 0; i < q; i++) { int t; cin >> t; if (t == 1) { int l, r, val; cin >> l >> r >> val; seg_x.update(1, 1, n, l, r, val); } else { int u; cin >> u; cout << get_absolute_position(u) << "\n"; } } return 0; }更高效的实现(使用欧拉序)
#include <bits/stdc++.h> using namespace std; const int MAXN = 100005; int n, q; int L[MAXN], R[MAXN]; int x_val[MAXN]; // 欧拉序相关 int euler[2 * MAXN], pos_in_euler[MAXN], euler_time; int left_size[MAXN], right_size[MAXN]; void dfs_euler(int u) { if (u == 0) return; // 记录节点进入 euler[++euler_time] = u; pos_in_euler[u] = euler_time; int x = x_val[u]; if (x == -1) { // 先访问当前节点(已记录),然后左右子树 dfs_euler(L[u]); dfs_euler(R[u]); } else if (x == 0) { // 先左子树,然后当前节点,然后右子树 dfs_euler(L[u]); euler[++euler_time] = u; // 再次记录当前节点 dfs_euler(R[u]); } else { // x == 1 // 先左右子树,然后当前节点 dfs_euler(L[u]); dfs_euler(R[u]); euler[++euler_time] = u; // 再次记录当前节点 } } // 树状数组:单点更新,区间求和 struct Fenwick { int bit[2 * MAXN]; void update(int idx, int val) { for (; idx <= 2 * n; idx += idx & -idx) { bit[idx] += val; } } int query(int idx) { int res = 0; for (; idx > 0; idx -= idx & -idx) { res += bit[idx]; } return res; } int range_sum(int l, int r) { return query(r) - query(l - 1); } } fenw; int main() { ios::sync_with_stdio(false); cin.tie(0); cin >> n >> q; for (int i = 1; i <= n; i++) { cin >> L[i] >> R[i]; x_val[i] = -1; // 初始值 } // 初始欧拉序 euler_time = 0; dfs_euler(1); // 初始化树状数组:每个欧拉序位置标记为1 for (int i = 1; i <= euler_time; i++) { fenw.update(i, 1); } // 处理查询(简化版:每次修改后重建欧拉序) // 注意:实际比赛需要更高效的增量更新 for (int i = 0; i < q; i++) { int t; cin >> t; if (t == 1) { int l, r, x; cin >> l >> r >> x; // 更新标记值 for (int j = l; j <= r; j++) { x_val[j] = x; } // 重建欧拉序(简化实现,实际应用需要优化) euler_time = 0; dfs_euler(1); // 重建树状数组 memset(fenw.bit, 0, sizeof(fenw.bit)); for (int j = 1; j <= euler_time; j++) { fenw.update(j, 1); } } else { int u; cin >> u; // 节点u在欧拉序中第一次出现的位置前有多少个节点 cout << fenw.query(pos_in_euler[u]) << "\n"; } } return 0; }复杂度分析
- 朴素方法:每次查询 ,总复杂度 ,只能过小数据
- 优化方法:使用欧拉序 + 树状数组,查询 ,但修改需要 重建
- 目标复杂度: 需要更复杂的数据结构维护动态欧拉序
- 1
信息
- ID
- 5556
- 时间
- 1000ms
- 内存
- 512MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者