把玩家路径按 lca 拆成上坡、下坡两段,观察到时转化为两个深度常量等式,用 Tarjan 离线 LCA + 树上差分(子树内桶计数)统计每个观察点看到的键值数。
OJ: roj
题目 ID: 3639
难度:提高+/省选-
标签:树上差分LCADFS序NOIP
创建: 2026-10-02 12:11
更新: 2026-10-10 11:30
形式化题目
给一棵
点
样例 1 的树与三条路径(点旁括号内是
graph TD
A["1(W=0)"] --> B["2(W=2)"]
B --> D["3(W=5)"]
A --> C["4(W=1)"]
C --> E["5(W=2)"]
C --> F["6(W=3)"]
玩家 1 走 × 表示不在路径上):
| 点 |
玩家 1: |
玩家 2: |
玩家 3: |
答案 |
|---|---|---|---|---|
| 1 (0) | 0 ✓ | 0 ✓ | 1 | 2 |
| 2 (2) | × | 1 | 0 | 0 |
| 3 (5) | × | 2 | × | 0 |
| 4 (1) | 1 ✓ | × | 2 | 1 |
| 5 (2) | 2 ✓ | × | × | 1 |
| 6 (3) | × | × | 3 ✓ | 1 |
表格的行是观察点、列是玩家,格子是到达时刻;时刻恰好等于行首
正解
思路
朴素做法与瓶颈
最直接的做法:对每个玩家沿路径逐点走一遍,在每个点
瓶颈在于:每个玩家把贡献平摊到路径上每个点,而询问(每个观察点)却只要一个总数。要让"发贡献"变成
关键转化:把"时刻相等"写成两条深度等式
玩家路径经过折点
- 上坡段
(含两端):点 在这一段上的时刻是 。“时刻等于 ” 即
- 下坡段
(不含 ):设 为全程长度,点 的时刻是 ,化简后“时刻等于 ” 即
两个等式的形状完全一致:左边是只跟点
点
被玩家看到 ⟺ 在路径上,且点键 (上坡)或 (下坡)出现在该路径放进某个桶里的标记中。
graph LR
subgraph 路径与两段
S["S 发:键 dep[S]"] -->|"上坡 S..L 含两端"| L2["L"]
L2 -->|"下坡 L..T 不含 L"| T2["T 发:键 dep[S]-2dep[L]"]
end
图中每个箭头是一段路径:上坡段的贡献由
从"路径逐点"到"子树统计"
把视角反过来:每个玩家只做两次"发标记"——
- 上坡标记(键
)挂在点 ; - 下坡标记(键
)挂在点 。
则"点
- 上坡:
⟺ 在 子树内,且 不是( 在子树外);折点 自身的到达时刻属于下坡等式,所以上坡段在 处要扣掉——把一个键同为 的"减标记"挂在 。 为根时没有 ,标记自然丢失,无需特判。 - 下坡:
⟺ 在 子树内且 也在 子树内——把"减标记"直接挂在 ,同一次子树统计顺便完成"且"条件。
于是每个观察点的答案就是子树内四个桶的二分计数:
graph TD
U["u(观察点)"] --- P["fa[L]:上坡减标记挂这里"]
U --- L3["L:下坡减标记挂这里"]
U --- S3["S:上坡加标记挂这里"]
U --- T3["T:下坡加标记挂这里"]
观察重点:
LCA 与实现
- LCA 用 Tarjan 离线:询问挂在两个端点上,DFS 进入点
时检查另一端是否已进入,是则答案为该端当前并查集代表;回溯离开 时把 并入父亲。整个 LCA 求解与 DFS 融为一体, 。 - Python 无递归深度保障,用邻居迭代器栈写迭代 DFS(栈顶孩子优先 = 真先序),同时拿到先序
order。 - 子树统计:先序时间戳
tin+ 子树大小size,把子树变成区间[tin[u], tin[u]+size[u]-1];四个桶内排序tin后,每个点两次二分即可,总。 - 键值范围:
, ,桶用 defaultdict(list)免去手工偏移。
代码
/**
* 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 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int find_set(vector<int>&fa,int x){while(fa[x]!=x){fa[x]=fa[fa[x]];x=fa[x];}return x;}
int count_in_bucket(map<int, vector<int> > &buckets,int key,int lo,int hi){auto it=buckets.find(key); if(it==buckets.end()) return 0; vector<int>&v=it->second; return upper_bound(v.begin(),v.end(),hi)-lower_bound(v.begin(),v.end(),lo);}
int main(){ios::sync_with_stdio(false); cin.tie(nullptr); int n,m; if(!(cin>>n>>m)) return 0; vector<vector<int> > adj(n+1); for(int i=0;i<n-1;i++){int u,v;cin>>u>>v; adj[u].push_back(v); adj[v].push_back(u);} vector<int> weight(n+1); for(int i=1;i<=n;i++) cin>>weight[i]; vector<int>s(m),t(m),lca(m); vector<vector<int> > at_s(n+1), at_t(n+1); for(int i=0;i<m;i++){cin>>s[i]>>t[i]; at_s[s[i]].push_back(i); at_t[t[i]].push_back(i);} vector<int> parent(n+1), depth(n+1), anc(n+1), order; vector<char> vis(n+1); for(int i=0;i<=n;i++) anc[i]=i; vector<int> it(n+1); vis[1]=1; order.push_back(1); for(int z=0;z<(int)at_s[1].size();z++){int i=at_s[1][z]; if(vis[t[i]]) lca[i]=find_set(anc,t[i]);} for(int z=0;z<(int)at_t[1].size();z++){int i=at_t[1][z]; if(vis[s[i]]) lca[i]=find_set(anc,s[i]);}
vector<int> st; st.push_back(1); while(!st.empty()){int u=st.back(); if(it[u]<(int)adj[u].size()){int v=adj[u][it[u]++]; if(!vis[v]){vis[v]=1; parent[v]=u; depth[v]=depth[u]+1; order.push_back(v); for(int z=0;z<(int)at_s[v].size();z++){int i=at_s[v][z]; if(vis[t[i]]) lca[i]=find_set(anc,t[i]);} for(int z=0;z<(int)at_t[v].size();z++){int i=at_t[v][z]; if(vis[s[i]]) lca[i]=find_set(anc,s[i]);} st.push_back(v);}} else {st.pop_back(); if(u!=1) anc[find_set(anc,u)]=parent[u];}}
vector<int> tin(n+1), sz(n+1,1); for(int i=0;i<(int)order.size();i++) tin[order[i]]=i; for(int i=(int)order.size()-1;i>=0;i--){int u=order[i]; if(u!=1) sz[parent[u]]+=sz[u];}
map<int, vector<int> > up_add,up_sub,down_add,down_sub; for(int i=0;i<m;i++){int d0=depth[s[i]]; int k=d0-2*depth[lca[i]]; up_add[d0].push_back(tin[s[i]]); down_add[k].push_back(tin[t[i]]); if(parent[lca[i]]) up_sub[d0].push_back(tin[parent[lca[i]]]); down_sub[k].push_back(tin[lca[i]]);} for(map<int,vector<int> >::iterator itb=up_add.begin();itb!=up_add.end();++itb) sort(itb->second.begin(),itb->second.end()); for(map<int,vector<int> >::iterator itb=up_sub.begin();itb!=up_sub.end();++itb) sort(itb->second.begin(),itb->second.end()); for(map<int,vector<int> >::iterator itb=down_add.begin();itb!=down_add.end();++itb) sort(itb->second.begin(),itb->second.end()); for(map<int,vector<int> >::iterator itb=down_sub.begin();itb!=down_sub.end();++itb) sort(itb->second.begin(),itb->second.end());
for(int u=1;u<=n;u++){int lo=tin[u], hi=lo+sz[u]-1, du=depth[u]; int ans=count_in_bucket(up_add,du+weight[u],lo,hi)+count_in_bucket(down_add,weight[u]-du,lo,hi)-count_in_bucket(up_sub,du+weight[u],lo,hi)-count_in_bucket(down_sub,weight[u]-du,lo,hi); if(u>1) cout<<' '; cout<<ans;} cout<<'\n'; 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 12:11
# update_at: 2026-10-02 12:11
import sys
from bisect import bisect_left, bisect_right
from collections import defaultdict
def find(anc: list[int], x: int) -> int:
"""并查集查找(路径折半):x 当前能摸到的最高"已结束子树"代表。"""
while anc[x] != x:
anc[x] = anc[anc[x]]
x = anc[x]
return x
def subtree_count(buckets: defaultdict[int, list[int]], key: int, lo: int, hi: int) -> int:
"""buckets[key] 里 tin 落在 [lo, hi] 的个数:u 的子树内有多少个 key 类标记。"""
marks = buckets.get(key)
if marks is None:
return 0
return bisect_right(marks, hi) - bisect_left(marks, lo)
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
n = int(next(data)); m = int(next(data))
adj: list[list[int]] = [[] for _ in range(n + 1)]
for _ in range(n - 1):
u = int(next(data)); v = int(next(data))
adj[u].append(v); adj[v].append(u)
weight = [0] + [int(next(data)) for _ in range(n)] # W_j:结点 j 的观察时刻
# 每条路径拆成三段事件:上坡差分 +,折点父亲差分 -;下坡差分 +,折点差分 -
s = [0] * m; t = [0] * m; lca = [0] * m
at_start: list[list[int]] = [[] for _ in range(n + 1)] # Tarjan 询问挂在两端点
at_term: list[list[int]] = [[] for _ in range(n + 1)]
for i in range(m):
a = int(next(data)); b = int(next(data))
s[i] = a; t[i] = b
at_start[a].append(i); at_term[b].append(i)
# 迭代 DFS 得真先序 order;回溯时把子树并入并查集(Tarjan 离线 LCA)
parent = [0] * (n + 1)
depth = [0] * (n + 1)
anc = list(range(n + 1))
visited = [False] * (n + 1)
it: list = [None] * (n + 1) # 每个点自己的邻居迭代器,代替递归
order: list[int] = []
def walk(u: int) -> None:
"""进入 u(真先序);若某条询问的另一端已进入,立刻取 LCA。"""
order.append(u)
for i in at_start[u]:
other = t[i]
if visited[other]:
lca[i] = find(anc, other)
for i in at_term[u]:
other = s[i]
if visited[other]:
lca[i] = find(anc, other)
visited[1] = True
walk(1)
it[1] = iter(adj[1])
stack = [1]
while stack:
u = stack[-1]
pushed = False
for v in it[u]:
if not visited[v]:
visited[v] = True
parent[v] = u
depth[v] = depth[u] + 1
it[v] = iter(adj[v])
walk(v)
stack.append(v)
pushed = True
break
if not pushed: # u 的子树结束:把 u 向父亲收拢
stack.pop()
if u != 1:
anc[find(anc, u)] = parent[u]
tin = [0] * (n + 1) # 先序时间戳,子树 = 一段连续区间
for idx, u in enumerate(order):
tin[u] = idx
size = [1] * (n + 1)
for u in reversed(order): # 逆先序保证算父亲时孩子已齐
if u != 1:
size[parent[u]] += size[u]
# 两类差分,键是"观察等式"里的常量,桶里存路径端点的 tin:
# 上坡(S..L,含两端)标记 dep[S];下坡((L..T])标记 dep[S] - 2*dep[L]
up_add: defaultdict[int, list[int]] = defaultdict(list)
up_sub: defaultdict[int, list[int]] = defaultdict(list)
down_add: defaultdict[int, list[int]] = defaultdict(list)
down_sub: defaultdict[int, list[int]] = defaultdict(list)
for i in range(m):
d = depth[s[i]]
k = d - 2 * depth[lca[i]] # 下坡等式的常量
up_add[d].append(tin[s[i]])
down_add[k].append(tin[t[i]])
if parent[lca[i]]: # 上坡去掉折点:减在折点父亲处(根没有父亲,标记自然丢失)
up_sub[d].append(tin[parent[lca[i]]])
down_sub[k].append(tin[lca[i]])
for buckets in (up_add, up_sub, down_add, down_sub):
for marks in buckets.values():
marks.sort() # 排序后子树计数变成一次二分
ans = [0] * (n + 1)
for u in range(1, n + 1):
du = depth[u]
lo = tin[u]; hi = lo + size[u] - 1
ans[u] = (
subtree_count(up_add, du + weight[u], lo, hi)
+ subtree_count(down_add, weight[u] - du, lo, hi)
- subtree_count(up_sub, du + weight[u], lo, hi)
- subtree_count(down_sub, weight[u] - du, lo, hi)
)
print(' '.join(map(str, ans[1:])))
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:DFS + Tarjan 离线 LCA
,差分标记 ,子树统计 ,总计 ,与 C++ 正解同阶。 - 空间复杂度:
(邻接表、并查集、四个标记桶);本实现 Python 对象开销较大(最大组实测约 350 MB),按约定允许 MLE;官方 1 s 限时下 Python 侧可能 TLE(本地最大组 0.87 s),算法阶数不受影响。
总结
- 看到"路径上点的到达时刻是否等于点权",先按 lca 把路径拆成上坡、下坡两段,各写出一个左边只依赖点、右边只依赖路径的等式,统计问题立刻变成"点的键是否出现在路径桶里"。
- 路径逐点贡献 → 树上差分:加标记挂段端点,减标记挂段边界外一格(
与 ),"折点算不算"这类边界由挂点位置自然表达。 - 询问"子树内有多少个某键标记"可以用 DFS 进出场合差分,也可以用先序
tin+ 子树区间 + 桶内二分;后者在 Python 里更省状态。

