点分治:每层在重心处 DFS 收集各点到重心的距离,用排序加二分统计距离和不超过 K 的点对,再减去同一子树内部的点对完成容斥,总复杂度 O(N log^2 N)。
OJ: roj
题目 ID: 3118
难度:提高+/省选-
标签:点分治树二分
创建: 2026-10-01 18:17
更新: 2026-10-10 11:30
形式化题目
给定一棵 0 0 结束。
题目不保证
以样例
| 点对 | 路径 | 长度 | |
|---|---|---|---|
| 3 | ✅ | ||
| 1 | ✅ | ||
| 2 | ✅ | ||
| 2 | ✅ | ||
| 4 | ✅ | ||
| 5 | ❌ | ||
| 6 | ❌ | ||
| 3 | ✅ | ||
| 1 | ✅ | ||
| 4 | ✅ |
合法点对共 8 个,与样例输出一致。注意
暴力解法
思路
枚举每个点
代码
暴力解不单独给出:它就是正解代码去掉「求重心、分层、容斥」之后的退化形态——保留「从当前根 DFS 收集距离 + 统计距离和不超过
复杂度
- 时间:
(每个起点一次 DFS)。 - 空间:
。
瓶颈
瓶颈是每换一个起点就重扫整棵树:
正解
关键观察
观察 1:固定一个点
观察 2:按重心分层,不重不漏。 每次取当前分量的重心
观察 3:本层的多余计数恰好是「同一子树内部」的点对。 只按
思路
点分治的每一层做三件事:求重心、收集距离、容斥计数。
flowchart TD
A["分量 {0,1,2,3,4}<br>重心 0:统计所有经过 0 的点对"] --> B["删 0 后分量 {1}"]
A --> C["删 0 后分量 {3}"]
A --> D["分量 {2,4}<br>重心 2:统计所有经过 2 的点对"]
D --> E["删 2 后分量 {4}"]
图中每个方框是一层:先在重心处统计,再把删掉重心后的各分量作为下一层处理。样例树的重心依次是
求重心。 先从分量的代表点做一次迭代式 DFS,用 order 数组边遍历边增长代替手写栈,再由后往前累加子树大小。然后从代表点出发沿「重儿子」走:只要某个儿子的子树超过总点数一半,就走过去;走不动时所在点就是重心(它所有子树都不超过一半)。
收集距离。 从重心出发 DFS,把每个点到重心的距离记下来;超过
计数。 对升序数组
每个 bisect_right(D, K - d_i, 0, i + 1)。用二分而不是双指针,是因为本题
于是本层的贡献是
其中 sub[0] 是重心自身的空占位,merged 就是 sub[1:] 就是各个
用样例验证这一步(
自配对修正。 计数时把重心自己的距离 divide(adj, k) - n。
代码
/**
* 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
*/
// 树中点对距离:点分治 + 「汇总减各子树」容斥,统计距离不超过 k 的点对。
#include <cstdio>
#include <vector>
#include <algorithm>
typedef long long ll;
ll N, K;
std::vector<std::vector<int> > g; // g[u]:u 出发的边编号
std::vector<int> eto, ewt; // 边的另一端与边权
std::vector<char> removed;
std::vector<int> parent;
std::vector<ll> sz;
std::vector<ll> dists; // 扫描子树时收集的距离
std::vector<int> order;
void add_edge(int u, int v, int w) {
eto.push_back(v); ewt.push_back(w); g[u].push_back((int)eto.size() - 1);
eto.push_back(u); ewt.push_back(w); g[v].push_back((int)eto.size() - 1);
}
// 求 root 所在分量的重心:删掉它后,剩下的每块规模都不超过总量一半
int find_centroid(int root) {
int n = (int)g.size();
parent.assign(n, -1);
parent[root] = root;
order.clear();
order.push_back(root);
for (size_t idx = 0; idx < order.size(); idx++) { // 边遍历边增长,等价于手写栈
int u = order[idx];
for (size_t j = 0; j < g[u].size(); j++) {
int e = g[u][j];
int v = eto[e];
if (!removed[v] && v != parent[u]) {
parent[v] = u;
order.push_back(v);
}
}
}
for (size_t idx = order.size(); idx-- > 0;) { // 子节点一定排在父节点之后
int u = order[idx];
sz[u] = 1;
for (size_t j = 0; j < g[u].size(); j++) {
int e = g[u][j];
int v = eto[e];
if (!removed[v] && parent[v] == u) sz[u] += sz[v];
}
}
int cent = root;
ll total = sz[root];
while (true) {
int heavy = -1;
ll best = 0;
for (size_t j = 0; j < g[cent].size(); j++) {
int e = g[cent][j];
int v = eto[e];
if (!removed[v] && parent[v] == cent && sz[v] > best) {
heavy = v;
best = sz[v];
}
}
if (heavy < 0 || best * 2 <= total) return cent;
cent = heavy;
}
}
// 统计有序数组里有多少对 j <= i 满足 dist[j] + dist[i] <= k(含 i = j 的自配对)
ll count_le(std::vector<ll>& d, ll k) {
ll total = 0;
for (size_t i = 0; i < d.size(); i++) {
ll lim = k - d[i];
total += std::upper_bound(d.begin(), d.begin() + i + 1, lim) - d.begin();
}
return total;
}
ll divide() {
int n = (int)g.size();
removed.assign(n, 0);
parent.assign(n, 0);
sz.assign(n, 0);
std::vector<int> roots;
roots.push_back(0);
ll answer = 0;
while (!roots.empty()) {
int root = roots.back();
roots.pop_back();
if (removed[root]) continue; // 该点已在更早的层里当过重心
int cent = find_centroid(root);
// 从重心出发按子树收集距离(大于 k 的分支剪掉),sub[0] 是空占位(重心自身)
std::vector<std::vector<ll> > sub;
sub.push_back(std::vector<ll>());
for (size_t j = 0; j < g[cent].size(); j++) {
int e = g[cent][j];
int v = eto[e];
if (removed[v]) continue;
std::vector<std::pair<int, std::pair<int, ll> > > stk;
dists.clear();
stk.push_back(std::make_pair(v, std::make_pair(cent, (ll)ewt[e])));
while (!stk.empty()) {
int u = stk.back().first;
int par = stk.back().second.first;
ll d = stk.back().second.second;
stk.pop_back();
if (d > K) continue; // 权值非负,更深的点只会更远
dists.push_back(d);
for (size_t t = 0; t < g[u].size(); t++) {
int e2 = g[u][t];
int x = eto[e2];
if (x != par && !removed[x]) { // 只看本子树 DFS 的父节点
stk.push_back(std::make_pair(x, std::make_pair(u, d + ewt[e2])));
}
}
}
std::sort(dists.begin(), dists.end());
sub.push_back(dists);
}
std::vector<ll> merged;
merged.push_back(0);
for (size_t s = 1; s < sub.size(); s++) {
for (size_t t = 0; t < sub[s].size(); t++) merged.push_back(sub[s][t]);
}
std::sort(merged.begin(), merged.end());
answer += count_le(merged, K);
for (size_t s = 1; s < sub.size(); s++) {
answer -= count_le(sub[s], K); // 同一子树内部的配对要在本层扣掉
}
removed[cent] = 1; // 删掉重心,邻居各代表一个新分量
for (size_t j = 0; j < g[cent].size(); j++) {
int e = g[cent][j];
int v = eto[e];
if (!removed[v]) roots.push_back(v);
}
}
return answer;
}
int main() {
std::vector<ll> out;
while (scanf("%lld %lld", &N, &K) == 2) {
if (N == 0 && K == 0) break;
g.assign(N, std::vector<int>());
eto.clear();
ewt.clear();
for (ll i = 0; i < N - 1; i++) {
int u, v, w;
scanf("%d %d %d", &u, &v, &w);
add_edge(u, v, w);
}
// 计数包含 x = y 的自配对,而路径要求两个节点不同,故每个点各减一次
out.push_back(divide() - N);
}
for (size_t i = 0; i < out.size(); i++) {
printf("%lld\n", out[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-01 18:18
# update_at: 2026-10-01 18:27
import sys
from bisect import bisect_right
def count_le(sorted_dists: list[int], k: int) -> int:
"""统计有序数组里有多少对 j <= i 满足 dist[j] + dist[i] <= k(含 i = j 的自配对)。"""
return sum(bisect_right(sorted_dists, k - d, 0, i + 1) for i, d in enumerate(sorted_dists))
def find_centroid(adj: list[list[tuple[int, int]]], removed: list[bool], root: int) -> int:
"""求 root 所在分量的重心:它删掉后,剩下的每块规模都不超过总量一半。"""
n = len(adj)
parent = [-1] * n
parent[root] = root
order = [root]
for u in order: # order 边遍历边增长,等价于手写栈
for v, _ in adj[u]:
if not removed[v] and v != parent[u]:
parent[v] = u
order.append(v)
size = [0] * n
for u in reversed(order): # 子节点一定排在父节点之后
size[u] = 1
for v, _ in adj[u]:
if not removed[v] and parent[v] == u:
size[u] += size[v]
cent = root
total = size[root]
while True:
heavy, best = -1, 0
for v, _ in adj[cent]:
if not removed[v] and parent[v] == cent and size[v] > best:
heavy, best = v, size[v]
if heavy < 0 or best * 2 <= total: # 没有超过一半的重儿子:重心已找到
return cent
cent = heavy
def collect_by_subtree(
adj: list[list[tuple[int, int]]], removed: list[bool], cent: int, k: int
) -> list[list[int]]:
"""从重心出发按子树收集距离(大于 k 的分支剪掉),sub[0] 是空占位,代表重心自身。"""
sub: list[list[int]] = [[]]
for v, w in adj[cent]:
if removed[v]:
continue
dists: list[int] = []
stack = [(v, cent, w)]
while stack:
u, p, d = stack.pop()
if d > k: # 权值非负,更深的点只会更远
continue
dists.append(d)
stack += [(x, u, d + wx) for x, wx in adj[u] if x != p and not removed[x]]
sub.append(sorted(dists))
return sub
def divide(adj: list[list[tuple[int, int]]], k: int) -> int:
"""点分治:逐层在重心处用「汇总 - 各子树」容斥,返回合法点对数(含自配对)。"""
n = len(adj)
removed = [False] * n
roots = [0] # 待处理分量的代表点
answer = 0
while roots:
root = roots.pop()
if removed[root]: # 该点已在更早的层里当过重心
continue
cent = find_centroid(adj, removed, root)
sub = collect_by_subtree(adj, removed, cent, k)
# 重心自身的距离 0 必 <= k,先加进汇总集合;同一子树内部的配对要在本层扣掉
merged = sorted([0] + [d for ds in sub[1:] for d in ds])
answer += count_le(merged, k) - sum(count_le(ds, k) for ds in sub[1:])
removed[cent] = True # 分治层:删掉重心,邻居各代表一个新分量
roots += [v for v, _ in adj[cent] if not removed[v]]
return answer
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
while True:
n, k = next(data), next(data)
if n == 0 and k == 0:
break
adj: list[list[tuple[int, int]]] = [[] for _ in range(n)]
for _ in range(n - 1):
u, v, w = next(data), next(data), next(data)
adj[u].append((v, w))
adj[v].append((u, w))
# 计数包含 x = y 的自配对,而路径要求两个节点不同,故每个点各减一次
out.append(str(divide(adj, k) - n))
print("\n".join(out))
if __name__ == "__main__":
solve()复杂度
- 时间:每层分量总规模为
,层数 ;每层排序与二分各 ,总复杂度 。 时为 量级基本操作。 - 空间:邻接表、
removed、size、parent均为,每层的距离数组不跨层累积,总空间 。
总结
- 树上路径问题可以按「路径经过哪个点」分类统计;固定一个点
后, 变成两个到 的距离之和不超过 ,可在一趟 DFS 内批量判定。 - 点分治用重心保证递归深度
,用「删点后各分量独立递归」保证每个点对只被统计一次;本层多算的恰好是同一子树内部的点对,用容斥减掉。 - 计数用「排序 + 对每个
二分 」代替双指针,避免了边权为 、 大于所有距离时的边界陷阱,复杂度同阶。 - 实现上全部使用显式栈,不做递归调用,避免 Python 深树时的递归深度问题。