树的重心
树的重心也叫做树的质心。其本质是一个点,删除这个点后,形成的子树中最大的节点数目最小。
解法
一遍dfs即可。dfs的时候记录一下当前节点 u u 的,同时记录他的所有儿子子树中的最大节点数目 mxchild m x c h i l d ,那么删除当前节点 u u 所形成的子树就是。维护这个答案的最小值即可求出树的重心。
性质
树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么他们的距离和一样。
把两个树通过一条边相连得到一个新的树,那么新的树的重心在连接原来两个树的重心的路径上 。
- 把一个树添加或删除一个叶子,那么它的重心最多只移动一条边的距离 。
点分治
先说一些可以参考的资料。
国家队论文:分治算法在树的路径问题中的应用
算法竞赛入门指南P392页,路径统计问题。
点分治用来解决树上的路径统计问题。复杂度为 nlogn n l o g n 。分治策略的思想是,因为对于一个确定的点 u u ,树上只有两种路径:要么经过,要么不经过 u u 。每次选取树的重心,统计经过树的重心的路径,此时计算完成后有不合法的路径计算进去,把这些不合法的路径删去后,再递归处理每一个子树。处理子树的方法是上面的方法一致的,找到子树的重心,然后依次处理。
通过如上的方法,可以处理出树上的所有路径。
有几个问题说明一下
- 为什么选取重心来进行分治?因为重心有较好的性质,可以保证分治的复杂度不退化。想想一下,如果我们对一条链进行分治,那么可能会造成的复杂度,这样显然是不可以接受的。
- 为什么会有重复的路径计算?设 dist[u] d i s t [ u ] 表示节点 u u 到当前分治质心的路径和,设为去除质心后的子树的的归属。例如: dist[1]=0,dist[2]=1,dist[6]=2 d i s t [ 1 ] = 0 , d i s t [ 2 ] = 1 , d i s t [ 6 ] = 2 , 又如 belong[5]=3,belong[6]=2,belong[9]=4 b e l o n g [ 5 ] = 3 , b e l o n g [ 6 ] = 2 , b e l o n g [ 9 ] = 4 。当以1为根的时候,我们希望统计出的是经过以1为根的路径。当我们遍历出所有点到1的距离,然后对于其中的任意两个点 u,v u , v ,计算 dist[u]+dist[v] d i s t [ u ] + d i s t [ v ] 即为他们的路径值。这句话对 belong[u]!=belong[v] b e l o n g [ u ] ! = b e l o n g [ v ] 是没问题的,反之是有问题的。如 dist[6]+dist[7] d i s t [ 6 ] + d i s t [ 7 ] 并不会经过点1。如何删除这些答案呢?那么就是递归到质心的子树中,遍历这些子树,把以子树为根的节点统计出的答案全部删掉即可,这样计算出来的就是经过1的路径条数了。
几个注意点
- 注意在找质心的时候,不要遍历已经进行过决策的顶点,所以要设置 visit v i s i t 数组。(本质是在除去质心的子树上找他们的质心)
- 找质心的时候,要设置全局的节点数目 subtreesize s u b t r e e s i z e 。他表示的是当前要找子树上的节点数目。
- 统计答案的时候,具体问题具体分析。
模板
void getroot(int u, int f) { // 找质心 sz[u] = 1; int mxchild = 0; for(int i = head[u]; i != -1; i = e[i].nxt ) { int v = e[i].to; if(v != f && !visit[v]) { getroot(v, u); sz[u] += sz[v]; mxchild = max(mxchild, sz[v]); } } int tmp = max(mxchild, subtreesize - sz[u]); if(tmp < nowmn) { nowmn = tmp; rt = u; } } void getdis(int u, int f, int dis) { // 算距离,dis是初始距离 dist[dis % 3] ++ ; for(int i = head[u]; i != -1; i = e[i].nxt) { int v = e[i].to; if(v != f && !visit[v]) getdis(v, u, (dis + e[i].w) % 3); } } ll getans(int u, int initdis) { // 统计答案 dist[0] = dist[1] = dist[2] = 0; getdis(u, u, initdis); return 2ll * dist[1] * dist[2] + 1ll * dist[0] * dist[0]; } void solve(int u) { ans += getans(u, 0); // 统计进过u的答案 visit[u] = true; for (int i = head[u]; i != -1; i = e[i].nxt) { int v = e[i].to; if (!visit[v]) { ans -= getans(v, e[i].w); // 删除在一颗子树中的答案 nowmn = INF; rt = 0; subtreesize = sz[v]; getroot(v, u); solve(rt); } } }