当前位置: 首页 > news >正文

点分治之砍树大师

从“分治”到“点分治”

在算法世界里,“分治”几乎是最经典的套路:把一个大问题拆成若干规模较小的子问题,递归解决,再把答案拼接。归并排序、快速幂,都是这样耳熟能详的例子。

数组有天然的“中点”,可以左右对半分;而无根树由节点与边连接,我们也可以选择一个节点,将树分为多个部分分治,这就叫做“点分治”。

不过,树没有一个显眼的“中间位置”。如果随便选择一个点砍掉,得到的子树大小可能极不均衡,递归效率就会大打折扣。这时,就需要一个“树上的中点”——它能保证我们每次分裂后,剩下的子树都不会太大。这个“中点”,正是重心(centroid)

于是我们就得到了“点分治”——在树上实现分治的一种方式。核心套路是:

  1. 找出树的重心。
  2. 处理所有“经过重心”的答案。
  3. 删除重心,递归处理每个子树。

每一步看似简单,合在一起就是一个强大的框架。

点分治的思想与重心

我们来看一个需要点分治解决的经典问题:

给定一棵 无根带权树(边权为正整数),有 \(M\) 个询问,每个询问给出一个整数 \(k\),问树上有多少对点 \(\{u, v\}\) 满足 距离(u, v) = k

总的来说,点分治用分治的方法来统计路径,他的思想是:

  • 选一个点作为根节点。

  • 这样一来,所有路径要么两端来自两个子树并经过根节点(一端刚好就在根节点的也可以算作这个情况),要么两端都在同一个子树内,那么它没有经过根节点而完整地落在某个子树里,这就交给下一层递归去处理。

  • 只处理前一种情况,然后我们递归处理每一个子树,后一种情况最终一定在某个子树里被统计到。这样,每一条路径在整个递归中只会被统计一次,不会重复,不会漏掉。

为什么要经过重心?

重心的定义:如果在无根树中选择某个节点以他为根,使得所有子树大小的最大值达到最小,那么这个点就是树的重心。重心可能不唯一,但一定存在。重心就像是树的“平衡点”,它让树的分治递归变得可行且优雅,根据定义每个子树至多是原树的一半。

每层递归都会处理一棵规模为 \(N\) 的树,工作量是 \(O(N)\)。重心的性质保证子树最大规模 ≤ N/2,所以树的规模会对半缩小。递归深度约 \(O(\log N)\)。总体复杂度就是 \(O(N \log N)\)

简单来说,每一次迭代都选取中心为根节点,才能保证子树规模快速减小。

标准流程

  1. 找重心:DFS 统计子树大小,选出“最大子树最小”的点作为重心。
  2. 收集信息:从重心出发,DFS 收集到各子树的路径信息。
  3. 合并答案:用数据结构(桶、哈希、bitset……)处理跨子树的路径组合。
  4. 递归下去:删除重心,在子树里继续重复上面的过程。注意,新的子树要重新找他自己的重心而不是原重心的子节点。

这四步就是点分治的主干骨架。不同问题,只是在第 2、3 步处理信息的方式不一样。

典型应用:树上距离 = k 的点对计数

回到上文经典问题。朴素做法枚举点对或者暴力搜索都是平方起步,肯定不行。点分治可以 \(O(N \log N)\) 高效解决问题。

用点分治怎么做?

在当前重心 \(c\),考虑所有经过 \(c\) 的路径。这些路径可以拆成“两段”:dist(u,c) + dist(v,c),我们只要在不同子树之间做“配对”即可。

  1. 以重心为根:从重心出发,DFS 子树,收集 dist(c, x)

  2. 配对查询:设当前子树的距离集合为 D_sub,全局已有距离集合为 freq(来自之前处理过的子树)。

    • 对每个 \(d \in D_{sub}\),对每个询问 \(k\),若 freq[k - d] > 0,则说明存在路径 (x, y) 距离为 \(k\)
  3. 更新集合:处理完配对后,把 D_sub 的元素加入 freq,供后面子树使用。

  4. 递归:删去重心,继续对子树点分治。

图的存储

我们用邻接表保存树:

struct Edge {int to, w;
};
vector<vector<Edge>> g;  // g[u] 存储节点 u 的所有邻边

重心查找

重心 = 删除后,剩余子树最大规模最小的点。

int n; 
vector<int> sz, max_sub;
vector<bool> vis;int find_centroid(int u, int fa, int tot, int &best, int &root) {sz[u] = 1;max_sub[u] = 0;for (auto [v, w] : g[u]) {if (v == fa || vis[v]) continue;find_centroid(v, u, tot, best, root);sz[u] += sz[v];max_sub[u] = max(max_sub[u], sz[v]);}max_sub[u] = max(max_sub[u], tot - sz[u]);if (max_sub[u] < best) {best = max_sub[u];root = u;}return root;
}

收集距离(getDis)

DFS 收集从重心出发的所有距离。

void get_dist(int u, int fa, int d, vector<int> &dis) {dis.push_back(d);for (auto [v, w] : g[u]) {if (v == fa || vis[v]) continue;get_dist(v, u, d + w, dis);}
}

点分治

  • 用一个哈希表/数组来保存 freq(已有子树的距离)。
  • 先查后加 避免同子树内重复配对。
unordered_map<int,int> freq;  // 距离频次数组
vector<int> ks;               // 询问集合
vector<long long> ans;        // 每个询问的答案void solve_centroid(int u, int tot) {// 1. 找重心int best = 1e9, root = -1;find_centroid(u, -1, tot, best, root);u = root;vis[u] = true;// 2. 处理经过重心的路径freq.clear();freq[0] = 1;  // 重心自身到重心的距离为 0for (auto [v, w] : g[u]) {if (vis[v]) continue;vector<int> dis;get_dist(v, u, w, dis);// 查询阶段(先查)for (int d : dis) {for (int i = 0; i < (int)ks.size(); i++) {// 这里每一个询问就分别查询了,假定询问不多。毕竟本文主题是点分治int k = ks[i];if (freq.count(k - d)) ans[i] += freq[k - d];}}// 更新阶段(再加)for (int d : dis) freq[d]++;}// 3. 递归到子树for (auto [v, w] : g[u]) {if (!vis[v]) solve_centroid(v, sz[v] < sz[u] ? sz[v] : tot - sz[u]);}
}

参考主函数

int main() {ios::sync_with_stdio(false);cin.tie(nullptr);int m;cin >> n >> m;g.assign(n+1, {});sz.assign(n+1, 0);max_sub.assign(n+1, 0);vis.assign(n+1, false);for (int i = 1; i < n; i++) {int u, v, w;cin >> u >> v >> w;g[u].push_back({v, w});g[v].push_back({u, w});}ks.resize(m);ans.assign(m, 0);for (int i = 0; i < m; i++) cin >> ks[i];solve_centroid(1, n);for (int i = 0; i < m; i++)cout << ans[i] << "\n";
}

效率

点分治的复杂度经常让人觉得“魔法般的高效”。我们来直观理解一下。

  • 每层递归: 每层总共处理一棵大小为 \(N\) 的树,做的事就是找重心、收集距离、更新答案,复杂度 O(N)。
  • 重心的性质:删掉重心后,剩余每个子树大小 ≤ N/2。
  • 递归层数:因此子树的规模每次都对半缩小,层数就是 O(log N)。总体复杂度就是 O(N log N)

点分治还能做什么

点分治的强大之处在于它的“框架性”:

  • 找重心 → 收集信息 → 合并答案 → 递归。
  • 不同问题,只需更换“收集信息 & 合并答案”的逻辑

有了这一套思路,还可以解决很多其他问题:

  • 计数问题的各种变种:距离在区间 [L, R] 的点对个数或者存在性或距离和满足某种模数约束(如 % m = 0)。只需在配对阶段稍作修改。
  • 统计其他各种路径问题:有些“求解具有某性质的路径数量”,经过转化可能也可以使用点分治处理。
  • 动态问题:当树节点被染色/标记时,实时维护“距离最近的某种颜色节点”,在线处理多次插入/查询的“树上最近红点”问题。
http://www.sczhlp.com/news/79193/

相关文章:

  • 怎么做切片网站重庆哪里有做淘宝网站推广的
  • 成都网站建设今明互联特色的南昌网站建设
  • 西安做视频网站公司某公司的网站建设的资金预算书
  • 兼职做美工摄影去哪个网站快飞建站
  • 长沙网站制作案例网站页面设计说明怎么写
  • 临淄关键词网站优化首选公司外贸网站如何传产品
  • 给网站做路由做的新网站到首页又下去了
  • 拉链网站源码wordpress的优势和
  • Uniapp_笔记(持续更新)
  • 非暴力沟通_读书笔记
  • 石家庄做网站排名soho怎么做网站
  • 婚庆网站开发计划书h5企业网站开发
  • 兼职网站开发团队工作项目总结项目网项目平台
  • 天津做网站找谁建设管理部门网站查询
  • 网站建设合同 英文范文广西建设工程质检安全网站
  • 网站开发网站设计上海集团网站建设价格
  • 做网站招标注册万维网网站
  • 网站建设珠海 新盈科技wordpress pluto主题
  • wordpress 全站404wordpress制作html5
  • 如何把php做的网站做成app如何编辑html网页
  • notepad++
  • 容器镜像仓库registry和repository的区别
  • 宁波做网站多少钱大埔县住房城乡规划建设局网站
  • 怎么添加网站背景音乐专业移动微网站建设
  • 怎么查网站备案接入商如何加速wordpress
  • 绿园区住房和城乡建设局网站百度应用商店下载安装
  • 桂林模板网站建设超大尺寸哔哩哔哩网站
  • 概念理论——唯一性加密量子密匙
  • 网页网站设计公司排名html网页设计模板和源代码
  • 做网站的图片要求大小上海网站建设乐云seo