【无脑美学!!!】浅谈点分治
同步发表在了我的公众号。
为什么点分治是“无脑美学”
点分治真是太牛了!!!

在树上做路径统计,朴素做法常常要枚举两点或许多路径,复杂度一下就 \(O(n^2)\),显然无法接受。
点分治的思想是:每次在重心处分割,把全局的暴力拆成若干规模均衡的小暴力;由于重心保证每块至多半树大小,分治深度 \(O(\log n)\),在每一层用线性或近线性代价做统计,整体就收敛到 \(O(n\log n)\) 甚至更优的级别。这就是“把该算的一口气算完,但保证每一口都不太大”的无脑美学。
1. 预备知识与术语
1.1 树的重心
重心的等价刻画包括:
-
删除该点后,每个连通块的大小都不超过原树的一半;
-
若重心不唯一,则至多两个且相邻;以重心为根时所有子树规模 \(\le \frac{n}{2}\);
-
到所有点距离和在重心处取最小(有两个重心时两者相同)。
这些性质是点分治正确性与复杂度的核心支点。
1.2 点分治
在当前连通块上找到重心 \(c\),只统计路径经过 \(c\) 的答案;然后把 \(c\) 删去,对各连通块递归。由重心性质,递归点分树的高度为 \(O(\log n)\)。许多问题在每层统计可以做到 \(O(\text{块大小})\),于是总复杂度 \(O(n\log n)\)。
由于中文谐音,大家普遍更喜欢叫“淀粉质”。
1.3 与其它树技术的关系
- HLD 更偏向多次在线区间查询/修改;点分治更擅长“一次性统计所有跨子树配对”的路径问题。两者经常互补。
- 点分治也能做“把到各层重心的距离预存起来”的动态/在线查询(如染色点最近距离),这是点分树的经典用法。
2. 复杂度与正确性一览
2.1 复杂度框架
因为重心的妙妙性质,分解层数 \(O(\log n)\)。如果在每层把所有与该层有关的节点(或边)线性地扫一遍、再配上排序/双指针/桶合并等近线性手段,那么“每层均摊 \(O(n)\)”的结论成立,故总计 \(O(n\log n)\)。
2.2 正确性直观
对任意一对点 \((u,v)\),要么其路径在当前层经过重心 \(c\),要么不经过。前者在处理 \(c\) 时被完整统计,后者全部落入某个子问题(删去 \(c\) 后的某个连通块)里继续统计。
3. 标准流程
3.1 找重心
常见写法是两步:先 DFS 求子树规模,再以“最大残块最小”为准则选择重心;
也可以用“从任意点出发,始终往子树规模 \(>\frac{n}{2}\) 的儿子走,直到没有”为止的线性走法。
3.2 收集与统计(以“距离限制”为例)
把重心 \(c\) 的每个儿子子树当作一个“桶”,从该儿子出发向下收集到 \(c\) 的距离。先把所有子树的距离汇总后做一次全局配对统计(如排序+双指针数对),再减去“同一子树内部”的配对(容斥),就得到路径经过 \(c\) 的贡献。这是许多经典问题的普遍套路。
3.3 清理与递归
统计完 \(c\) 后,标记 \(c\) 为“已删除”,对每个尚未删除的连通块递归,重复 3.1 ~ 3.2。整体形成一棵“点分树”。
3.4 常见细节
- 只在“当前连通块”里 DFS/收集,跳过已删点。
- 桶/标记要么局部开小数组+原地清,要么记录访问过的位置再回滚,避免全局大清空的多余开销。
- 多测时优先复用全局数组,按需清空头节点与边表。
下面给出一个模板级可跑示例代码:以“无权树上距离 \(\le K\) 的点对个数”为例,完整演示“找重心—收集—配对—容斥—递归”的标准流程与写法。其复杂度为 \(O(n\log n)\)。
模板示例 A:树上距离 \(\le K\) 的点对数(点分治)
简要题意概括:给定一棵 \(n\) 个点的无权树与整数 \(K\),统计所有点对 \((u,v)\) 使得树上距离 \(\mathrm{dist}(u,v)\le K\) 的对数。请输出答案。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 200000 + 5;
int n, K, ec, hd[N], to[N << 1], nx[N << 1];
int sz[N], vis[N];
int dt[N], tp[N];
void add(int u, int v)
{
to[++ec] = v;
nx[ec] = hd[u];
hd[u] = ec;
}
void dfs(int u, int p)
{
sz[u] = 1;
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
dfs(v, u);
sz[u] += sz[v];
}
}
int rt, bv;
void gcr(int u, int p, int t)
{
int mx = t - sz[u];
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
gcr(v, u, t);
mx = max(mx, sz[v]);
}
if (mx < bv)
{
bv = mx;
rt = u;
}
}
void col(int u, int p, int d, int &c)
{
if (d > K)
return;
dt[++c] = d;
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
col(v, u, d + 1, c);
}
}
ll cnt(int *a, int m)
{
sort(a + 1, a + m + 1);
ll ans = 0;
int i = 1, j = m;
while (i < j)
{
if (a[i] + a[j] <= K)
{
ans += j - i;
++i;
}
else
--j;
}
return ans;
}
ll sol(int s)
{
dfs(s, 0);
bv = 1e9;
gcr(s, 0, sz[s]);
int c = rt;
vis[c] = 1;
ll ans = 0;
int dc = 0;
for (int e = hd[c]; e; e = nx[e])
{
int v = to[e];
if (vis[v])
continue;
int tc = 0;
col(v, c, 1, tc);
for (int i = 1; i <= tc; i++)
tp[i] = dt[i];
ll sub = cnt(tp, tc);
ans -= sub;
for (int i = 1; i <= tc; i++)
dt[++dc] = tp[i];
}
dt[++dc] = 0;
ans += cnt(dt, dc);
for (int e = hd[c]; e; e = nx[e])
{
int v = to[e];
if (vis[v])
continue;
ans += sol(v);
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> K;
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
add(u, v);
add(v, u);
}
cout << sol(1) << "\n";
return 0;
}
代码要点
dfs+gcr两步找重心(3.1);vis表示该点已被“切掉”。重心选择用“最大残块最小”准则实现。col从子树收集到重心的距离;cnt通过排序+双指针统计“和 \(\le K\)”的对数(等价于跨子树的距离 \(\le K\))。容斥做法:总和cnt(dt)减去各子树内部cnt(tp)。- 主过程
sol:标记重心、做统计、递归到各连通块,形成点分树(3.2 ~ 3.3)。
4. 三类经典计数
4.1 距离恰为 \(K\) 的点对数(无权树)
核心套路:以当前重心 \(c\) 为桥,把跨不同子树的路径用频次数组配对成和恰为 \(K\);容斥去掉同子树配对。
简要题意概括:给定无权树与整数 \(K\),统计距离恰为 \(K\) 的点对数。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 200000 + 5;
int n, K, ec, hd[N], to[N << 1], nx[N << 1];
int sz[N], vis[N], b[N], stk[N], top;
int dt[N], tp[N];
ll ans;
void add(int u, int v)
{
to[++ec] = v;
nx[ec] = hd[u];
hd[u] = ec;
}
void dfs(int u, int p)
{
sz[u] = 1;
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
dfs(v, u);
sz[u] += sz[v];
}
}
int rt, bv;
void gcr(int u, int p, int t)
{
int mx = t - sz[u];
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
gcr(v, u, t);
mx = max(mx, sz[v]);
}
if (mx < bv)
{
bv = mx;
rt = u;
}
}
void col(int u, int p, int d, int &c)
{
if (d > K)
return;
dt[++c] = d;
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
col(v, u, d + 1, c);
}
}
void addb(int d)
{
if (!b[d])
stk[++top] = d;
b[d]++;
}
void clrb()
{
while (top)
{
b[stk[top--]] = 0;
}
}
ll sol(int s)
{
dfs(s, 0);
bv = 1e9;
gcr(s, 0, sz[s]);
int c = rt;
vis[c] = 1;
ll res = 0;
addb(0);
for (int e = hd[c]; e; e = nx[e])
{
int v = to[e];
if (vis[v])
continue;
int tc = 0;
col(v, c, 1, tc);
for (int i = 1; i <= tc; i++)
{
int x = dt[i];
if (x <= K && K - x >= 0)
res += b[K - x];
}
for (int i = 1; i <= tc; i++)
addb(dt[i]);
}
clrb();
for (int e = hd[c]; e; e = nx[e])
{
int v = to[e];
if (vis[v])
continue;
res += sol(v);
}
return res;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> K;
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
add(u, v);
add(v, u);
}
ans = sol(1);
cout << ans << "\n";
return 0;
}
要点:每层把“已合并的子树”距离放入桶 b[],对当前子树逐点查询 b[K−d];处理完即整体清桶,保证复杂度。
4.2 距离集合命题(多组 \(K_i\) 离线判存在)
思路与 4.1 类似,但把答案是否存在变成布尔合并:在某一层以 vis[dist]=true 逐子树滚动,当处理当前子树 tp[] 时,通过 vis[K−x] 检验所有查询。
4.3 距离模 \(m\) 的余数类统计(例:\(\bmod 3\))
把距离按 \(\bmod m\) 分桶:对子树 tp[] 统计与全局桶 cnt[r] 的配对,满足 \((r_{sub}+r_{all})\bmod m\) 落在指定集合即可。该类统计与和 \(\le K\) 不同,通常不需要排序,常数出色,适合在点分层上做线性配对。
5. 进阶:权值、限制与多维信息
5.1 边权和 \(\le K\) 的点对数
把“距离”换成“权和”,流程完全一致:在每个子树向下收集到重心的累加边权,之后用排序 + 双指针统计“和 \(\le K\)”。这正是许多教材在讲解权值版点分治时的标准写法;同理可做边权和 \(=K\)。
简要题意概括:给定一棵带权树与整数 \(K\),统计所有点对 \((u,v)\) 使路径边权和 \(\le K\)。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 200000 + 5;
int n, ec, hd[N], to[N << 1], nx[N << 1], wt[N << 1];
int sz[N], vis[N];
ll K, dt[N], tp[N];
void add(int u, int v, int w)
{
to[++ec] = v;
nx[ec] = hd[u];
wt[ec] = w;
hd[u] = ec;
}
void dfs(int u, int p)
{
sz[u] = 1;
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
dfs(v, u);
sz[u] += sz[v];
}
}
int rt, bv;
void gcr(int u, int p, int t)
{
int mx = t - sz[u];
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
gcr(v, u, t);
mx = max(mx, sz[v]);
}
if (mx < bv)
{
bv = mx;
rt = u;
}
}
void col(int u, int p, ll d, int &c)
{
if (d > K)
return;
dt[++c] = d;
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
col(v, u, d + wt[e], c);
}
}
ll cnt(ll *a, int m)
{
sort(a + 1, a + m + 1);
ll res = 0;
int i = 1, j = m;
while (i < j)
{
if (a[i] + a[j] <= K)
{
res += j - i;
i++;
}
else
j--;
}
return res;
}
ll sol(int s)
{
dfs(s, 0);
bv = 1e9;
gcr(s, 0, sz[s]);
int c = rt;
vis[c] = 1;
ll res = 0;
int dc = 0;
dt[++dc] = 0;
for (int e = hd[c]; e; e = nx[e])
{
int v = to[e];
if (vis[v])
continue;
int tc = 0;
col(v, c, wt[e], tc);
for (int i = 1; i <= tc; i++)
tp[i] = dt[i];
ll sub = cnt(tp, tc);
for (int i = 1; i <= tc; i++)
dt[++dc] = tp[i];
res -= sub;
}
res += cnt(dt, dc);
for (int e = hd[c]; e; e = nx[e])
{
int v = to[e];
if (vis[v])
continue;
res += sol(v);
}
return res;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> K;
for (int i = 1; i < n; i++)
{
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
add(v, u, w);
}
cout << sol(1) << "\n";
return 0;
}
5.2 更多路径属性的合并规则
- 颜色/奇偶:对子树统计“到重心路径上颜色出现向量/奇偶性”,跨子树按规则合并。
- 位或/位与/异或:可以用位集或字典树在每层配对;异或的典型写法是“到重心前缀异或”,跨子树找互补。
- 双维限制(如边数与权和):在每层对子树向量做“二分 + 单调性”或固定一维枚举。
6. 动态点分治与点分树
当题目变成多次在线修改/查询时,点分治的层序结构可以长期复用:为每个节点保存到其点分树祖先的距离;把答案函数写成“沿点分树自底向上合并”。每次修改与查询都只在 \(O(\log n)\) 个祖先上更新/取最小值。
简要题意概括:给定无权树,初始 \(1\) 号点为红色,支持两类操作:把某点染红;查询某点到任意红点的最小距离。输出所有查询结果。
如果你不会点分治,就只能 CDQ 分治+虚树+换根 DP,我们显然不能接受如此大的码量。而且此种做法是 \(O(n \log^2 n)\) 的,更加无法接受。

不妨选择码量小,常数小,单 \(\log\) 的点分治写法。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 200000 + 5, LG = 20, INF = 1e9;
int n, q, ec, hd[N], to[N << 1], nx[N << 1];
int up[LG][N], dep[N];
int sz[N], vis[N], par[N], best[N];
void add(int u, int v)
{
to[++ec] = v;
nx[ec] = hd[u];
hd[u] = ec;
}
void dfs0(int u, int p)
{
up[0][u] = p;
for (int k = 1; k < LG; k++)
up[k][u] = up[k - 1][up[k - 1][u]];
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p)
continue;
dep[v] = dep[u] + 1;
dfs0(v, u);
}
}
int lca(int u, int v)
{
if (dep[u] < dep[v])
swap(u, v);
int d = dep[u] - dep[v];
for (int k = 0; k < LG; k++)
if (d >> k & 1)
u = up[k][u];
if (u == v)
return u;
for (int k = LG - 1; k >= 0; k--)
if (up[k][u] != up[k][v])
{
u = up[k][u];
v = up[k][v];
}
return up[0][u];
}
int dst(int u, int v)
{
int w = lca(u, v);
return dep[u] + dep[v] - 2 * dep[w];
}
void dfs(int u, int p)
{
sz[u] = 1;
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
dfs(v, u);
sz[u] += sz[v];
}
}
int rt, bv;
void gcr(int u, int p, int t)
{
int mx = t - sz[u];
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
gcr(v, u, t);
mx = max(mx, sz[v]);
}
if (mx < bv)
{
bv = mx;
rt = u;
}
}
void build(int s, int p)
{
dfs(s, 0);
bv = 1e9;
gcr(s, 0, sz[s]);
int c = rt;
par[c] = p;
vis[c] = 1;
for (int e = hd[c]; e; e = nx[e])
{
int v = to[e];
if (vis[v])
continue;
build(v, c);
}
}
void upd(int u)
{
int x = u;
while (x)
{
best[x] = min(best[x], dst(x, u));
x = par[x];
}
}
int qry(int u)
{
int x = u, res = INF;
while (x)
{
res = min(res, best[x] + dst(x, u));
x = par[x];
}
return res;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> q;
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
add(u, v);
add(v, u);
}
dep[1] = 0;
dfs0(1, 1);
build(1, 0);
for (int i = 1; i <= n; i++)
best[i] = INF;
upd(1);
while (q--)
{
int t, u;
cin >> t >> u;
if (t == 1)
upd(u);
else
cout << qry(u) << "\n";
}
return 0;
}
复杂度:建点分树 \(O(n\log n)\);每次修改/查询沿点分树链长 \(O(\log n)\),用 LCA 取原树距离,整体 \(O((n+q)\log n)\)。
6.1 细节与常见坑
- 距离获取:如上代码用倍增 LCA 获取
dst(u,v),避免为每个重心开整图距离表导致的内存爆炸。 - 清理策略:动态题里
best[]只递减,无需清桶;静态计数题务必用“访问栈回滚”清桶,避免memset全清导致层上重复 \(O(n\log n)\) 外的额外常数。 - 正确性直观:任意查询点 \(u\) 到最近红点 \(x\) 的最短路,必经过 \(u\) 在点分树上的某个祖先 \(c\),于是
best[c]+dist(c,u)的最小值即答案。
7. 专题实战案例
7.1 CF 161D Distance in Tree(距离恰为 \(K\))
问题:给无权树与 \(K\),数距离恰为 \(K\) 的点对。
要点:以重心为桥,“桶配对”找 \(d_1+d_2=K\),对子树做容斥;整题 \(O(n\log n)\)。
7.2 IOI 2011 Race
问题:带权树,找权和恰为 \(K\) 的路径且使边数最少,不存在则 \(-1\)。
思路:点分治层上做跨子树合并。收集每个子树到重心的对儿 \((\text{sum}, \text{edges})\),用一张长为 \(K\!+\!1\) 的数组 bk[sum]=最少边数 做最优合并,查询 bk[K-sum] 即可;用访问栈回滚清理,常数更稳。
简要题意概括:给一棵带权树与 \(K\),求权和恰为 \(K\) 的路径最少边数,若无则 \(-1\)。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 200000 + 5, MK = 1000000 + 5, INF = 1e9;
int n, ec, hd[N], to[N << 1], nx_[N << 1], wt[N << 1];
int sz[N], vis[N], rt, bv;
int st[MK], tp, bk[MK];
ll K;
int dt[N], de[N];
void ad(int u, int v, int w)
{
to[++ec] = v;
nx_[ec] = hd[u];
wt[ec] = w;
hd[u] = ec;
}
void df(int u, int p)
{
sz[u] = 1;
for (int e = hd[u]; e; e = nx_[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
df(v, u);
sz[u] += sz[v];
}
}
void gc(int u, int p, int t)
{
int mx = t - sz[u];
for (int e = hd[u]; e; e = nx_[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
gc(v, u, t);
mx = max(mx, sz[v]);
}
if (mx < bv)
{
bv = mx;
rt = u;
}
}
void cl(int u, int p, int s, int d, int &c)
{
if (s > K)
return;
dt[++c] = s;
de[c] = d;
for (int e = hd[u]; e; e = nx_[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
cl(v, u, s + wt[e], d + 1, c);
}
}
int sol(int s)
{
df(s, 0);
bv = 1e9;
gc(s, 0, sz[s]);
int c = rt;
vis[c] = 1;
int ans = INF;
tp = 0;
if (bk[0] > 0)
{
bk[0] = 0;
st[++tp] = 0;
}
else if (bk[0] == INF)
{
bk[0] = 0;
st[++tp] = 0;
}
for (int e = hd[c]; e; e = nx_[e])
{
int v = to[e];
if (vis[v])
continue;
int tc = 0;
cl(v, c, wt[e], 1, tc);
for (int i = 1; i <= tc; i++)
{
int s1 = dt[i], d1 = de[i];
if (s1 <= K)
{
int s2 = (int)(K - s1);
if (bk[s2] < INF)
ans = min(ans, d1 + bk[s2]);
}
}
for (int i = 1; i <= tc; i++)
{
int s1 = dt[i], d1 = de[i];
if (bk[s1] > d1)
{
if (bk[s1] == INF)
st[++tp] = s1;
bk[s1] = d1;
}
}
}
while (tp)
{
bk[st[tp--]] = INF;
}
for (int e = hd[c]; e; e = nx_[e])
{
int v = to[e];
if (vis[v])
continue;
ans = min(ans, sol(v));
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> K;
for (int i = 1; i <= n; i++)
hd[i] = 0;
ec = 0;
for (int i = 1; i < n; i++)
{
int u, v, w;
cin >> u >> v >> w;
ad(u, v, w);
ad(v, u, w);
}
for (int i = 0; i < MK; i++)
bk[i] = INF;
int res = sol(1);
cout << (res >= INF ? -1 : res) << "\n";
return 0;
}
说明:数组 bk 长度按题面 \(K\le10^6\) 预留(每格存“到重心已合并部分的最少边数”),每层只回滚访问过的位置,避免整段清空。
7.3 Luogu P3806【模板】点分治 1(多次查询“是否存在距离=\(k\)”)
问题:带边权树,给 \(m\) 次 \(k\),回答是否存在路径权和恰为 \(k\)。数据范围 \(n\le10^4,m\le100,k\le10^7\)。
思路:点分治;在当前重心把子树距离收集到数组,然后对每个查询 \(k\) 做 vis[k-d] 判定(先判、后合并、容斥去重),用“访问栈回滚”清理。
简要题意概括:给带权树与若干 \(k\),问是否存在一条路径权和恰为 \(k\)。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 10000 + 5, Q = 100 + 5, MK = 10000000 + 5;
int n, m, ec, hd[N], to[N << 1], nx_[N << 1], wt[N << 1];
int sz[N], vis[N], rt, bv;
int qu[Q], ok[Q];
int mklen = 0;
unsigned char mk[MK];
int st[MK], tp;
int dt[N];
void ad(int u, int v, int w)
{
to[++ec] = v;
nx_[ec] = hd[u];
wt[ec] = w;
hd[u] = ec;
}
void df(int u, int p)
{
sz[u] = 1;
for (int e = hd[u]; e; e = nx_[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
df(v, u);
sz[u] += sz[v];
}
}
void gc(int u, int p, int t)
{
int mx = t - sz[u];
for (int e = hd[u]; e; e = nx_[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
gc(v, u, t);
mx = max(mx, sz[v]);
}
if (mx < bv)
{
bv = mx;
rt = u;
}
}
void cl(int u, int p, int s, int &c, int lim)
{
if (s > lim)
return;
dt[++c] = s;
for (int e = hd[u]; e; e = nx_[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
cl(v, u, s + wt[e], c, lim);
}
}
void addm(int x)
{
if (!mk[x])
{
mk[x] = 1;
st[++tp] = x;
}
}
void clr()
{
while (tp)
{
mk[st[tp--]] = 0;
}
}
void chk(int *a, int c)
{
for (int i = 1; i <= c; i++)
{
int x = a[i];
for (int j = 1; j <= m; j++)
{
int k = qu[j];
if (k >= x && mk[k - x])
ok[j] = 1;
}
}
}
void ins(int *a, int c)
{
for (int i = 1; i <= c; i++)
addm(a[i]);
}
void solve(int s)
{
df(s, 0);
bv = 1e9;
gc(s, 0, sz[s]);
int c = rt;
vis[c] = 1;
tp = 0;
addm(0);
for (int e = hd[c]; e; e = nx_[e])
{
int v = to[e];
if (vis[v])
continue;
int lim = 0;
for (int i = 1; i <= m; i++)
if (qu[i] > lim)
lim = qu[i];
int tc = 0;
cl(v, c, wt[e], tc, lim);
chk(dt, tc);
ins(dt, tc);
}
clr();
for (int e = hd[c]; e; e = nx_[e])
{
int v = to[e];
if (vis[v])
continue;
solve(v);
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++)
hd[i] = 0;
ec = 0;
for (int i = 1; i < n; i++)
{
int u, v, w;
cin >> u >> v >> w;
ad(u, v, w);
ad(v, u, w);
}
for (int i = 1; i <= m; i++)
cin >> qu[i], ok[i] = 0;
solve(1);
for (int i = 1; i <= m; i++)
cout << (ok[i] ? "AYE" : "NAY") << "\n";
return 0;
}
8. 与其它技术的对照与混合
- 点分治 vs HLD:多次在线区间型(路径加减、区间取 min/max/kth)更偏向 HLD;一次性统计跨不同子树配对的路径题点分治更自然。
- 点分治 vs DSU on tree:当统计是“以某点为根、向下只算一次”的聚合(如子树颜色计数),DSU 往往更优;跨子树配对需先查后合并+容斥,点分治更顺手。
- 混合技法:点分治外层 + 子问题内用二分、堆、字典树(异或)、或倍增 LCA 拿原树距离,均是常见组合;
9. 常见坑与 Debug 清单
- 重复计数:忘做子树内自配对的“容斥减法”,答案翻倍。
- 清理策略:
memset全清大桶会炸常数;请用访问栈回滚只清触及位置(上面两份代码已示范)。 - 层上排序次数:能不多排就不多排;如“和 \(\le K\)”用一次全局排序 + 双指针、子树内做减法。
- 最长链/退化链:树退化成链时,重心分治深度仍是 \(O(\log n)\),但若子过程没控常数,仍可能 TLE。
- 数据上界:IOI Race 的 \(K\le10^6\) 需大数组;Luogu P3806 的 \(k\le10^7\) 也要注意字节级内存。
- 多测复用:注意边表、标记、桶的复用与清空次序;若有 RE,多半是清理不彻底或越界。
- 选重心实现:推荐“
df求子树 +gc取最大残块最小”的二段式,或“沿大儿子走”的线性法,均与重心性质一致。
10. 常数优化与实现建议
- 分治层只清访问过的桶/位点。用访问栈或就地记录后回滚的方法,避免整段
memset导致额外 \(O(n\log n)\) 常数。 - 统一在当前层先查后并(容斥),并尽量把全局排序/双指针/一次桶合并收敛为每层一次,维持均摊 \(O(n)\);整体 \(O(n\log n)\)。
- 找重心的两种方法不多赘述,看自己习惯。
- 混合技术的边界:需要大量在线路径区间操作 → 倾向 HLD;一次性统计跨子树配对 → 点分治更自然。
- 复杂度:点分树深度 \(O(\log n)\),每层总访问量 \(O(n)\),若合并中引入平衡树/堆等,则在该层再乘以相应因子(例如加一个 \(\log n\))。
11. 模板库补充
11.A 基础骨架(仅构建点分治与递归,不做统计)
简要题意概括:读入一棵树,构建点分治骨架并返回 \(0\),便于在 sol() 的处理当前重心处插入任意统计逻辑。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 200000 + 5;
int n, ec, hd[N], to[N << 1], nx[N << 1];
int sz[N], vis[N], rt, bv;
void ad(int u, int v)
{
to[++ec] = v;
nx[ec] = hd[u];
hd[u] = ec;
}
void df(int u, int p)
{
sz[u] = 1;
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
df(v, u);
sz[u] += sz[v];
}
}
void gc(int u, int p, int t)
{
int mx = t - sz[u];
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
gc(v, u, t);
mx = max(mx, sz[v]);
}
if (mx < bv)
{
bv = mx;
rt = u;
}
}
ll sol(int s)
{
df(s, 0);
bv = 1e9;
gc(s, 0, sz[s]);
int c = rt;
vis[c] = 1;
ll ans = 0;
for (int e = hd[c]; e; e = nx[e])
{
int v = to[e];
if (vis[v])
continue;
ans += sol(v);
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
ad(u, v);
ad(v, u);
}
cout << sol(1) << "\n";
return 0;
}
11.B 余数类统计示例:无权树上“距离 % 3 == 0”的点对数量
思路:在当前重心 \(c\) 处,先把已合并部分的到 \(c\) 的距离模 \(3\) 计数放入桶 cnt[3](初始 cnt[0]=1 代表重心自身),对子树逐个收集 d%3,先用 cnt[(3-r)%3] 统计跨子树配对,再把该子树计数并入桶,最后递归到子块。复杂度 \(O(n\log n)\)。
简要题意概括:给定无权树,统计所有点对 \((u,v)\) 使得距离 \(\mathrm{dist}(u,v)\equiv 0\pmod 3\)。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 200000 + 5;
int n, ec, hd[N], to[N << 1], nx[N << 1];
int sz[N], vis[N], rt, bv, dt[N];
ll ans;
void ad(int u, int v)
{
to[++ec] = v;
nx[ec] = hd[u];
hd[u] = ec;
}
void df(int u, int p)
{
sz[u] = 1;
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
df(v, u);
sz[u] += sz[v];
}
}
void gc(int u, int p, int t)
{
int mx = t - sz[u];
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
gc(v, u, t);
mx = max(mx, sz[v]);
}
if (mx < bv)
{
bv = mx;
rt = u;
}
}
void cl(int u, int p, int d, int &c)
{
dt[++c] = d % 3;
for (int e = hd[u]; e; e = nx[e])
{
int v = to[e];
if (v == p || vis[v])
continue;
cl(v, u, d + 1, c);
}
}
ll go(int s)
{
df(s, 0);
bv = 1e9;
gc(s, 0, sz[s]);
int c = rt;
vis[c] = 1;
ll res = 0;
int cnt[3] = {0, 0, 0};
cnt[0] = 1;
for (int e = hd[c]; e; e = nx[e])
{
int v = to[e];
if (vis[v])
continue;
int tc = 0;
cl(v, c, 1, tc);
int add0 = 0, add1 = 0, add2 = 0;
for (int i = 1; i <= tc; i++)
{
int r = dt[i];
res += cnt[(3 - r) % 3];
if (r == 0)
add0++;
else if (r == 1)
add1++;
else
add2++;
}
cnt[0] += add0;
cnt[1] += add1;
cnt[2] += add2;
}
for (int e = hd[c]; e; e = nx[e])
{
int v = to[e];
if (vis[v])
continue;
res += go(v);
}
return res;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
ad(u, v);
ad(v, u);
}
ans = go(1);
cout << ans << "\n";
return 0;
}
说明:可推广到任意模 \(m\):把
cnt与子树计数扩展到长度 \(m\) 即可(依题面内存限制而定)。
12. 复杂度证明
- 点分树深度:若每次选择当前连通块的重心 \(c\),则删去 \(c\) 后,所有子块大小 \(\le\lfloor n/2\rfloor\)。于是任一节点在点分树上的层数 \(\le \lfloor\log_2 n\rfloor\)。
- 每层工作量:在某一固定层,所有递归块两两不交,其大小和为 \(n\)。若“处理当前重心”的统计在每块内是线性/近线性的(例如一次排序+双指针或常数模桶),则该层均摊 \(O(n)\)。
- 总复杂度:把上两条合并:层数 \(O(\log n)\) × 每层 \(O(n)\) ⇒ 总计 \(O(n\log n)\)。如在统计中对每个元素还需一次 \(\log n\) 的平衡树操作,则整体上界相应变为 \(O(n\log^2 n)\)。
13. 练习题单
- CF 342E
- IOI 2011 Race
- CF 766E
- CF 293E
- CF 161D
- 「岱陌算法杯」ROUND #3 (Div. 1 + Div. 2) D.巡逻路径

浙公网安备 33010602011771号