在说点分治之前先说一下序列分治,序列分治大家都知道吧,就是把序列从某个位置(一般是中间点)分成两部分,统计跨越两部分的答案再递归处理两部分。树的点分治的道理和序列分治很像,但树没有中点,该怎么分治呢?再对比序列分治,序列相当于一条链,而序列的中点就是这条链的重心,那么树的分治点就可以是这棵树的重心。回顾一下重心的性质:以树的重心为根,这棵树最大子树大小不大于整棵树大小的一半。这样就可以保证时间复杂度是O(nlogn)(因为最多logn层啊qwq)。
当以一个点为当前重心时,处理的答案是跨区域的(也就是统计任意两个子树之间经过重心产生的答案)。在这里介绍两种常用的统计方法:
1、统计所有子树中任意两点满足的方案数(不管两个点是否在同一棵子树里),再用单步容斥减掉在同一棵子树中的答案数。但这种方法的前提条件是统计答案满足逆运算(即总答案-不符合答案=符合答案)。
2、将当前子树里的点和之前遍历的子树中的所有点统计答案,这样避免了不符合的答案,也不需要满足逆运算,有的题只能用这种方法来统计。
最后说一下点分治题求解的主要过程:
1、找重心并以重心为根对子树dfs答案信息(例如路径长或者满足条件节点数)。
2、统计答案。
3、递归分治处理每棵子树。
代码时间
找重心
void getroot(int x,int fa) { size[x]=1; mx[x]=0; for(int i=head[x];i;i=next[i]) { if(to[i]!=fa&&!vis[to[i]]) { getroot(to[i],x); size[x]+=size[to[i]]; mx[x]=max(mx[x],size[to[i]]); } } mx[x]=max(mx[x],num-size[x]); if(!root||mx[x]<mx[root]) { root=x; } }
分治过程
void partition(int x) { vis[x]=1; ans+=calc(x,0); for(int i=head[x];i;i=next[i]) { if(!vis[to[i]]) { ans-=calc(to[i],val[i]); num=size[to[i]]; root=0; getroot(to[i],0); partition(root); } } }
变量名解释:
size[i],i的子树大小;mx[i],以i为重心最大子树大小;num,当前子树总大小;mx[0]初始成INF。
推荐练习题:
BZOJ2152聪聪可可
BZOJ3697采药人的路径
BZOJ2599Race
BZOJ1316树上的查询
BZOJ4016最短路径树问题
BZOJ4182Shopping
BZOJ3451Normal
BZOJ3672购票
原文地址:https://www.cnblogs.com/Khada-Jhin/p/9184417.html