树上差分把每条路径的 z 类物品拆成端点 +1、LCA 及其父亲 -1,再逆序自底向上做动态开点权值线段树合并,每个节点 O(log) 直接读出计数最大的种类。
OJ: roj
题目 ID: 3189
难度:提高+/省选-
标签:图论树上差分线段树合并
创建: 2026-10-02 00:20
更新: 2026-10-10 13:30
形式化题目
给一棵
数据范围:
直接正解
思路
第一步:把"路径覆盖"转成"点上差分"。
如果每次操作都真的沿路径走一遍,单次最坏
- 在
、 处给种类 记 ; - 在
与它的父亲处给种类 记 。
之后让每个点汇总整个子树的差分值,得到的恰好是"多少条路径覆盖了这个点、各是什么种类"。验证一下正确性:对点
flowchart LR
A["一次操作 (x,y,z)"] --> B["x 处 z:+1"]
A --> C["y 处 z:+1"]
A --> D["L=lca(x,y) 处 z:-1"]
A --> E["father(L) 处 z:-1"]
B & C & D & E --> F["子树求和后<br>路径上每点得到 z:+1<br>路径外每点为 0"]
第二步:每个点要的是"众数种类",不是计数和。
子树求和本身好办:逆 BFS 序遍历,把每个点的差分表并进父亲即可。难点在于每个点存的是"各种类各多少袋",而
观察合并过程:一个点的表 = 自己的差分表 + 所有儿子的表。如果用每种一个下标的权值线段树来存这张表,合并两棵线段树时,只在"两边同时存在的区间"才递归,代价是
第三步:线段树节点上存什么才能 O(1) 拿众数?
这是本题最容易踩的坑。若节点只存子树计数和 sum,查询众数时每层走向 sum 更大的孩子,是错误的:sum 是区间和,右边可能有 2 个种类各 1 袋(和为 2),左边只有 1 个种类 3 袋(和为 3),但若右边和比左边大就会走错方向。正确做法是节点额外维护:
mx:该节点子树内单个叶子的最大计数;best:取到该最大计数的叶子编号(即种类)。
插入差分点时沿路径更新叶子,再从叶子往上回推所有祖先的 (mx, best);合并时用两棵树孩子的 (mx, best) 重算父亲。这样每个点的答案在查询时是 mx[根] > 0 时答案就是 kinds[best[根]]。并列取编号最小,靠"比较时取左孩子优先 + 叶子编号即离散化后的种类顺序"天然实现。
整体流程:
| 步骤 | 做法 | 代价 |
|---|---|---|
| 建树 + 求 LCA | BFS 定父/深,倍增表 up[k][v] |
|
| 处理操作 | 每条路径差分成 4 个单点 |
|
| 汇总答案 | 逆 BFS 序:先取该点线段树根的 best,再 merge 进父亲 |
flowchart TB
subgraph ops["m 次操作"]
Q1["(x1,y1,z1)"]
Q2["(x2,y2,z2)"]
Q3["..."]
end
subgraph nodes["每个树节点挂一棵动态开点线段树"]
R1["点 x1: z1 +1"]
R2["点 y1: z1 +1"]
R3["点 L 与 father(L): z1 -1"]
end
ops -->|"每条路径拆 4 个差分点"| nodes
nodes -->|"逆 BFS 序 merge 进父亲"| ANS["每点 O(1) 读 best<br>输出计数最大的种类"]
一个实现细节:线段树的叶子数
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 13:30
* update_at: 2026-10-10 13:30
*/
// 雨天的尾巴:树上差分 + 动态开点权值线段树合并求路径众数。
// 路径 (x,y) 拆成 x+1、y+1、lca-1、lca父-1 四个差分点,
// 逆 BFS 序把子树线段树并到父亲,每个点取众数(并列取编号最小的种类)。
#include <algorithm>
#include <cstdio>
#include <vector>
using namespace std;
typedef long long ll;
const int MAXN = 100005;
const int MAXLOG = 19; // 2^17 > 100000
const int MAXNODE = 7200010; // 4m 次插入,每次最多 ceil(log2 K)+1 = 18 个点
int n, m, K;
int parent[MAXN];
int depth[MAXN];
int ordArr[MAXN];
int up[MAXLOG][MAXN];
int roots[MAXN];
int ansArr[MAXN];
int qx[MAXN], qy[MAXN], qz[MAXN];
vector<int> adj[MAXN];
vector<int> kinds; // 离散化后的物品种类(升序)
// 动态开点权值线段树的静态池,0 号点是空树哨兵
int lc[MAXNODE];
int rc[MAXNODE];
int mxc[MAXNODE];
int nodeCnt = 1; // 0 号留空
int newNode() {
int id = nodeCnt++;
return id; // 池已清零
}
void pull(int node) { // 由左右孩子重算 node 的最大单类计数
if (mxc[lc[node]] >= mxc[rc[node]]) mxc[node] = mxc[lc[node]];
else mxc[node] = mxc[rc[node]];
}
int insertNode(int root, int pos, int delta) { // 给叶子 pos 加 delta
if (root == 0) root = newNode();
int cur = root, l = 0, r = K - 1;
int path[MAXLOG + 2], pc = 0; // 自顶向下记录路径,最后统一回推
path[pc++] = cur;
while (l < r) {
int mid = (l + r) >> 1;
if (pos <= mid) {
int nxt = lc[cur];
if (nxt == 0) { nxt = newNode(); lc[cur] = nxt; }
r = mid;
cur = nxt;
} else {
int nxt = rc[cur];
if (nxt == 0) { nxt = newNode(); rc[cur] = nxt; }
l = mid + 1;
cur = nxt;
}
path[pc++] = cur;
}
mxc[cur] += delta; // 到达叶子:叶子上的计数本身就是它的 mx
for (int i = pc - 2; i >= 0; --i) pull(path[i]);
return root;
}
int mergeTree(int a, int b) { // 把 b 破坏式并入 a,返回新根
if (a == 0 || b == 0) return a | b;
if (lc[a] == 0 && rc[a] == 0) { // a 是叶子,b 同区间也必是叶子
mxc[a] += mxc[b];
return a;
}
lc[a] = mergeTree(lc[a], lc[b]);
rc[a] = mergeTree(rc[a], rc[b]);
pull(a);
return a;
}
int queryMode(int root) { // 众数种类;空树返回 0(并列取左 = 编号更小的种类)
if (root == 0 || mxc[root] == 0) return 0;
int cur = root, l = 0, r = K - 1;
while (l < r) {
int mid = (l + r) >> 1;
if (mxc[lc[cur]] >= mxc[rc[cur]]) { cur = lc[cur]; r = mid; }
else { cur = rc[cur]; l = mid + 1; }
}
return kinds[l];
}
int lca(int u, int v, int rows) {
if (depth[u] < depth[v]) { int t = u; u = v; v = t; }
int diff = depth[u] - depth[v];
for (int k = 0; k < rows; ++k) {
if (diff >> k & 1) u = up[k][u];
}
if (u == v) return u;
for (int k = rows - 1; k >= 0; --k) {
if (up[k][u] != up[k][v]) { u = up[k][u]; v = up[k][v]; }
}
return up[0][u];
}
int main() {
if (scanf("%d %d", &n, &m) != 2) return 0; // 空输入安全返回
for (int i = 0; i < n - 1; ++i) {
int a, b;
scanf("%d %d", &a, &b);
adj[a].push_back(b);
adj[b].push_back(a);
}
// 从 1 号点 BFS(order 边遍历边增长,等价于队列)
for (int i = 0; i <= n; ++i) parent[i] = 0;
int ordCnt = 0;
ordArr[ordCnt++] = 1;
for (int i = 0; i < ordCnt; ++i) {
int u = ordArr[i];
for (int j = 0; j < (int)adj[u].size(); ++j) {
int v = adj[u][j];
if (v != parent[u]) {
parent[v] = u;
depth[v] = depth[u] + 1;
ordArr[ordCnt++] = v;
}
}
}
// 倍增表:up[k][v] 是 v 向上跳 2^k 步到的祖先,0 号点是哨兵
int bitlen = 0;
while ((1 << bitlen) <= n - 1) ++bitlen; // (n-1).bit_length()
int rows = 1 + (bitlen > 1 ? bitlen : 1);
if (rows > MAXLOG) rows = MAXLOG;
for (int x = 0; x <= n; ++x) up[0][x] = parent[x];
for (int k = 1; k < rows; ++k) {
for (int x = 0; x <= n; ++x) up[k][x] = up[k - 1][up[k - 1][x]];
}
for (int i = 0; i < m; ++i) {
scanf("%d %d %d", &qx[i], &qy[i], &qz[i]);
kinds.push_back(qz[i]);
}
sort(kinds.begin(), kinds.end());
kinds.erase(unique(kinds.begin(), kinds.end()), kinds.end());
// 叶子数 K 必须同时 >= 种类数(装下差分点)和 n-1 的位长
K = kinds.size();
if (bitlen > K) K = bitlen;
if (K == 0) K = 1;
for (int i = 0; i < m; ++i) {
int x = qx[i], y = qy[i];
int kind = lower_bound(kinds.begin(), kinds.end(), qz[i]) - kinds.begin();
int l = lca(x, y, rows);
roots[x] = insertNode(roots[x], kind, 1);
roots[y] = insertNode(roots[y], kind, 1);
roots[l] = insertNode(roots[l], kind, -1);
if (parent[l]) roots[parent[l]] = insertNode(roots[parent[l]], kind, -1);
}
// 逆 BFS 序:u 的树已含全部子树差分时先取答案,再并入父节点
for (int i = n - 1; i >= 0; --i) {
int u = ordArr[i];
ansArr[u] = queryMode(roots[u]);
if (parent[u]) roots[parent[u]] = mergeTree(roots[parent[u]], roots[u]);
}
for (int i = 1; i <= n; ++i) printf("%d\n", ansArr[i]);
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 00:20
# update_at: 2026-10-02 00:20
import sys
def lca(u: int, v: int, depth: list[int], up: list[list[int]]) -> int:
"""返回 u,v 的最近公共祖先(二进制提升表 up)。"""
if depth[u] < depth[v]: # 统一让 u 更深
u, v = v, u
diff = depth[u] - depth[v]
for k in range(len(up)):
if diff >> k & 1: # u 上提 diff 的每个二进制位
u = up[k][u]
if u == v:
return u
for k in range(len(up) - 1, -1, -1): # 从高位跳,落到最近公共祖先的下一层
if up[k][u] != up[k][v]:
u, v = up[k][u], up[k][v]
return up[0][u]
def bfs_order(adj: list[list[int]]) -> tuple[list[int], list[int], list[int]]:
"""从 1 号点 BFS,返回 (访问顺序, 父节点, 深度);order 边遍历边增长,等价于队列。"""
size = len(adj)
parent = [0] * size # 0 号点是根的哨兵父节点
depth = [0] * size
order = [1]
for u in order:
for v in adj[u]:
if v != parent[u]:
parent[v] = u
depth[v] = depth[u] + 1
order.append(v)
return order, parent, depth
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m = next(data), next(data)
adj: list[list[int]] = [[] for _ in range(n + 1)]
for _ in range(n - 1):
a, b = next(data), next(data)
adj[a].append(b)
adj[b].append(a)
order, parent, depth = bfs_order(adj)
up = [parent] # up[k][v]:v 向上跳 2^k 步到的祖先,0 号点是哨兵
for _ in range(max(1, (n - 1).bit_length())):
prev_up = up[-1]
up.append([prev_up[x] for x in prev_up])
qx = [0] * m
qy = [0] * m
qz = [0] * m
for i in range(m):
qx[i] = next(data)
qy[i] = next(data)
qz[i] = next(data)
kinds = sorted(set(qz)) # 离散化:叶子编号 -> 物品种类
rank = {z: i for i, z in enumerate(kinds)} # 物品种类 -> 叶子编号
# 叶子数 K 必须同时 >= 种类数(装下所有差分点)和 n-1 的位长(merge 时两树区间一致)
K = max(len(kinds), (n - 1).bit_length())
# 动态开点权值线段树(数组版),0 号点是空树哨兵;闭包访问比全局快
# mx[v]:v 子树内最大的单类计数(不是区间和!);best[v]:取到该最大值的叶子编号
left = [0]
right = [0]
mx = [0]
best = [0]
def pull(node: int) -> None:
"""由左右孩子重算 node 的 (mx, best);并列取左孩子 = 编号更小的种类。"""
lc, rc = left[node], right[node]
if mx[lc] >= mx[rc]:
mx[node] = mx[lc]
best[node] = best[lc]
else:
mx[node] = mx[rc]
best[node] = best[rc]
def insert(root: int, pos: int, delta: int) -> int:
"""在 root 的树上给叶子 pos 加 delta(差分点),缺路径就动态开点。"""
if not root:
left.append(0)
right.append(0)
mx.append(0)
best.append(0)
root = len(mx) - 1
cur, l, r = root, 0, K - 1
path = [cur] # 自顶向下记录路径,最后统一回推 (mx, best)
while l < r:
mid = (l + r) >> 1
if pos <= mid:
nxt = left[cur]
if not nxt:
left.append(0)
right.append(0)
mx.append(0)
best.append(0)
nxt = len(mx) - 1
left[cur] = nxt
r = mid
else:
nxt = right[cur]
if not nxt:
left.append(0)
right.append(0)
mx.append(0)
best.append(0)
nxt = len(mx) - 1
right[cur] = nxt
l = mid + 1
cur = nxt
path.append(cur)
mx[cur] += delta # 到达叶子:叶子上的计数本身就是它的 mx
best[cur] = l
for node in reversed(path[:-1]): # 只回推内部节点,叶子的 mx 刚刚手工赋值
pull(node)
return root
def merge(a: int, b: int) -> int:
"""把线段树 b 破坏式并入 a,返回新根;两树同区间,叶子性一致。"""
if not a or not b:
return a | b
if not left[a] and not right[a]: # a 是叶子 <=> 区间长 1,b 同区间也必是叶子
mx[a] += mx[b] # 叶子计数相加,种类不变
return a
left[a] = merge(left[a], left[b])
right[a] = merge(right[a], right[b])
pull(a)
return a
def mode_kind(root: int) -> int:
"""返回树中计数最大的物品种类;并列取编号最小,空树返回 0。"""
return kinds[best[root]] if mx[root] > 0 else 0
# 树上差分:路径 (x,y) 拆成 x+1、y+1、lca-1、lca父-1
roots = [0] * (n + 1)
for i in range(m):
x, y, kind = qx[i], qy[i], rank[qz[i]]
l = lca(x, y, depth, up)
roots[x] = insert(roots[x], kind, 1)
roots[y] = insert(roots[y], kind, 1)
roots[l] = insert(roots[l], kind, -1)
if parent[l]:
roots[parent[l]] = insert(roots[parent[l]], kind, -1)
# 逆 BFS 序:u 的树已含全部子树差分时先取答案,再并入父节点
ans = [0] * (n + 1)
for u in reversed(order):
ans[u] = mode_kind(roots[u])
if parent[u]:
roots[parent[u]] = merge(roots[parent[u]], roots[u])
sys.stdout.write('\n'.join(map(str, ans[1:])) + '\n')
if __name__ == "__main__":
solve()复杂度
- 时间:
。LCA 倍增 ;差分点插入 ;线段树合并总摊还 ;答案查询 /点。 - 空间:
个线段树节点(约 级别)+ 倍增表 ,在 下约几十 MB,可以接受。
总结
- 树上"路径批量加减、最后逐点查询"先差分到点上,再子树汇总,是固定套路。
- "每点维护多种类计数、要众数"用权值线段树 + 线段树合并,空间从
压到 。 - 众数查询不能依赖区间和,线段树节点必须显式维护
(区间最大计数, 取到最大值的叶子)二元组——这是本题最隐蔽的错误来源。 - 合并要求两棵树区间结构一致,所以叶子总数要取
而不是种类数。