点分树(治)
点分用于处理树上路径/信息问题。它是分治的拓展版本。
实际上,如果树是链,则分治法每次将序列从正中间劈开,正中间就是重心。
如果计算跨过一个点的路径的时间复杂度较小,则可以考虑点分。
点分治是每次取整棵树的重心后再分治,这样子可以保证时间复杂度不超过$nlogn*计算复杂度$
对于路径题目,计算跨过重心的有2种方式:
1.容斥:计算子树间(有可能和自身组合)的答案,然后在做子树时容斥一下。
2.动态计算:每次插入一棵子树,在另一颗子树上查询当前的子树能对答案产生多少贡献。
这样子就不用容斥。
实际上,这2种方法都有用处,要具体题目具体分析。
动态计算法可以用于要求的信息没有逆元的题目上。
如果把分治每个子树的重心向当前重心连边,则得到点分树,高度不超过log_2n
点分树的性质是对于每个点,它子树上的点互相组成的路径上包含这个点。
这个性质可以用来解决一些带查询的点分治问题(如scoi2016 幸运数字)
如果每个点维护它的子树信息,则可以在一些问题上支持修改,如震波。
这种题目要先从点分的角度思考,然后再考虑是否能动态点分。
边分治:每次选择一条边,然后找到两边节点数最相近的边。
这样子对于点分的好处是每次只要处理两边,但是这样子也有局限性。
实际上,如果原图是菊花树的话,这样子就会被卡成n^2级别,需要加虚点让树更加平衡。
加点的方法类似kruskal重构树。
也有边分树的概念。边分树是:一个点向分出它的边连边,这样子会连出一个二叉树。
二叉树可以使用类似线段树合并的方式维护,也就是边分树合并。
总结:点/边分树实际上就是分治,枚举重心使时间复杂度不会退化。
实际上,点分/边分治的形态可以把它和其他知识点联系起来,这样子就产生了[CTSC2018]暴力写挂,[WC2014]紫荆花之恋两道题。
例题:
[集训队]聪聪可可
这道题可以直接使用点分治,按照模3同余合并即可。
bzoj2870(没做)
震波
动态点分的一道练手题。
这道题普通点分可以这样做(没有修改):
设当前询问点为x
求出当前重心后,枚举每棵子树。如果当前点连通块没有询问点时返回。
枚举每个连通块,递归计算距离<=k的点的点权和。
实际上我们接下来要求的是除了当前连通块,距离x的点<=k的点的点权和。
设d为子树重心->现在重心的距离。则实际上我们要求现在重心c的除了递归到的子树距离<=k-d的和。
这个可以容斥。把当前点距离<=k-d的值减去递归点距离<=k-d的值即可。
每次不会递归到不包含x的连通块,连通块的大小至少会/2,所以根据等比数列求和,时间复杂度O(n)。
这道题要求距离一个点<=k的点的点权和,所以可以使用线段树维护一下一个点子树的点权。
如果有修改的话,在点分树的每个点维护2棵线段树,线段树的每个下标存储距离=d的点。
每次修改时跳点分树并且修改距离,查询时维护上面提到的信息即可。
[ZJOI2015]幻想乡战略游戏(没做)
树上游戏
这道题有O(n)做法,但是为了练习点分治,所以使用点分做这道题。
我们设当前的连通块的重心为c,则我们要统计过c的路径对每一个当前连通块x的贡献。
假设x在重心的子树y里。
设sum表示除x子树内对当前点的贡献,cv[x]表示除了x的y子树中对x的贡献
贡献分为2部分:
1.另一个点z->c的子树上的颜色对x的贡献。
2.c->x对x的贡献。
第一部分可以维护一个桶c[x]表示x颜色的出现次数。
使用一个dfs统计。在更新c时可以顺便算出cv。
第二个dfs统计sum。
十分抽象,建议看代码。
#include<bits/stdc++.h> using namespace std; #define int long long #define N 200010 int h[N],vi[N],ms[N],rt,v[N],n,nxt[N],ec,c[N],sz[N],ans[N],cc[N],ss,cv[N]; void add(int x,int y){v[++ec]=y;nxt[ec]=h[x];h[x]=ec;} void gr(int x,int fa){ ms[x]=0; sz[x]=1; for(int i=h[x];i;i=nxt[i]) if(v[i]!=fa&&!vi[v[i]]){ gr(v[i],x); sz[x]+=sz[v[i]]; ms[x]=max(ms[x],sz[v[i]]); } ms[x]=max(ms[x],n-sz[x]); if(ms[x]<ms[rt])rt=x; } void d1(int x,int fa,int op){ if(!cc[c[x]]){ ss+=sz[x]*op; cv[c[x]]+=sz[x]*op; } cc[c[x]]++; for(int i=h[x];i;i=nxt[i]) if(v[i]!=fa&&!vi[v[i]]) d1(v[i],x,op); cc[c[x]]--; } void d2(int x,int fa){ if(!cc[c[x]])ss+=n-cv[c[x]]; cc[c[x]]++; ans[x]+=ss; for(int i=h[x];i;i=nxt[i]) if(v[i]!=fa&&!vi[v[i]]) d2(v[i],x); cc[c[x]]--; if(!cc[c[x]])ss-=n-cv[c[x]]; } void dfs(int x){ vi[x]=1; d1(x,0,1); ans[x]+=ss; for(int i=h[x];i;i=nxt[i]) if(!vi[v[i]]){ cv[c[x]]-=sz[v[i]]; n-=sz[v[i]]; ss-=sz[v[i]]; cc[c[x]]=1; d1(v[i],x,-1); cc[c[x]]=0; d2(v[i],x); cc[c[x]]=1; d1(v[i],x,1); ss+=sz[v[i]]; cc[c[x]]=0; cv[c[x]]+=sz[v[i]]; n+=sz[v[i]]; } d1(x,0,-1); for(int i=h[x];i;i=nxt[i]) if(!vi[v[i]]){ n=sz[v[i]]; rt=0; gr(v[i],x); gr(rt,0); dfs(rt); } } signed main(){ cin>>n; int tn=n; for(int i=1;i<=n;i++) cin>>c[i]; for(int i=1;i<n;i++){ int x,y; cin>>x>>y; add(x,y); add(y,x); } ms[0]=1e9; gr(1,0); gr(rt,0); dfs(rt); for(int i=1;i<=tn;i++) cout<<ans[i]<<' '; }
[CTSC2010]珠宝商
这道题实际上不是个点分治题。
暴力1:直接枚举一个点,然后使用sam上跑统计答案。
暴力2:建出原串的后缀树,可以用sam建出反串后构建。
考虑一个更"高级"的暴力,每次将贡献在lca处统计。
这个暴力会遍历lca的子树,统计一个点在原字符串上作为前半部分和后半部分的贡献。实际上就是要求以原串每个点开头/结尾的串。
可以枚举每个子树上的点然后打标记,最后在sam上下推标记(遍历整个sam)
暴力2显然可以用点分优化。但是每次下推标记的时间复杂度还是瓶颈。
实际上,当子树大小较小时可以使用sam在原串上跑来统计答案,子树大小较大时可以使用暴力2,这样子被推标记的次数就不会很多了。
时间复杂度降到nsqrt(n)
[ZJOI2007]捉迷藏(没用点分做)
[HNOI2015]开店(不熟)
[SCOI2016]幸运数字
这道题是树上路径问题,显然可以考虑点分。
实际上,原题可以任意选出点,得到这个点的权值,答案是所有权值的异或,所以可以考虑使用线性基。
题目就是让我们维护一条链的线性基。
这个用树剖/倍增的时间复杂度是3个log的,有点卡常。
但是可以使用点分。使用2个dfs。第一个dfs构建点分树。接下来每个询问的2个值在点分树上跳lca,把询问挂在lca上。
第二个dfs统计答案。维护当前分治重心->每个点的线性基,每次询问的时候把2个对应的线性基合并即可。
合并的方法就是把一个线性基的元素插入到另一个里面。
这样子的时间复杂度比倍增法更优越,但是跑的更慢,我不知道是为什么。
[WC2018]通道
这道题可以使用随机化爬山通过,但是使用这种方法通过此题意义不大。
直径有一个重要的性质:如果边权是非负的,把2个点集合并起来,新的直径肯定是在这2个点集直径的4个端点内产生。
原因是(这个网上大多没讲):在一个点集中,距离一个点的最远点一定是直径的端点(如果不是,则可以挪移直径的端点)
实际上,在新树中,如果直径的端点不在这4个点产生,则距离某个和它在同一个原点集的点的最远点不是直径的端点,则与上述假设矛盾。
现在设$d_k(i,j)$表示第k颗树上的距离,$dep_k(i)$表示第k颗树上点i的深度
对第一棵树进行边分治,假设2边的连通块被染色成黑,白,现在的边是(a,b),题目转化成要找到一个点对(c,d),让$d_2(c,d)+d_3(c,d)+d_1(a,c)+d_1(c,b)$最大。
建立当前边分块左边,右边在t2上的虚树。考虑t2上每一个点l作为lca对答案的贡献。则问题转化成让$dep_2(c)+dep_2(d)-dep_2(l)+d_3(c,d)+d_1(a,c)+d_1(c,b)$最大
实际上就是让$(dep_2(c)+d_1(a,c))+(dep_2(d)+d_1(c,b))+d_3(c,d)-dep_2(l)$最大,后面的$dep_2(l)$是常数。
前面的2项可以在t3上新建点t向c/d连边$(dep_2(c)+d_1(a,c))(dep_2(d)+d_1(c,b))$,设f(i,0)表示i点为白色的最优解,f(i,1)表示i点为黑色的最优解。
每次用每个节点组合更新答案。
时间复杂度O(nlog^2n),可以使用离线+基数排序+rmqlca优化到nlogn
河童重工(没做)
这道题的mst显然不能暴力建。
[CTSC2018]暴力写挂
[清华集训2016]汽水
路径相关问题要使用点分。
先二分一下绝对值的最小值,设为x,则限制条件是:$|frac{s}{m}-k|<=x$,s是路径权值和,m是边数。
再把绝对值拆掉,变成$-x<=frac{s}{m}-k<=x$
同乘m,得到$-xm<=s-km<=xm$
考虑$-xm<=s-km$,移项可得到$0<=sum a[i]-km+xm$和$0<=sum (a[i]-k+x)$
右边可得$s-km-xm<=0$和$sum (a[i]-k-x)<=0$
如果令v[i]=a[i]-k,可以得到$sum (v[i]-x)<=0$和$0<=sum (v[i]+x)$
点分一下。如果要符合条件,就要存在一条路径满足$sum (v[i]-x)<=0$和$0<=sum (v[i]+x)$
考虑计算$sum (v[i]-x)<=0$的条件,
实际上,不用整体套一个二分,可以在点分的时候二分。每次把左端点设1,右端点设为ans-1
如果我们在判定v[i]-x的话,可以把权值从小到大排序,使用双指针,这样子另外一边符合要求的是一个区间[r,n],设左边的指针是l,则随着l的增长,r不会增加。注意在寻找最小值时,最小值不能来自现在这颗子树,要记录不同于当前子树的最小值进行转移。
先二分一下绝对值的最小值,设为x,则限制条件是:$|frac{s}{m}-k|<=x$,s是路径权值和,m是边数。
再把绝对值拆掉,变成$-x<=frac{s}{m}-k<=x$
同乘m,得到$-xm<=s-km<=xm$
考虑$-xm<=s-km$,移项可得到$0<=sum a[i]-km+xm$和$0<=sum (a[i]-k+x)$
右边可得$s-km-xm<=0$和$sum (a[i]-k-x)<=0$
如果令v[i]=a[i]-k,可以得到$sum (v[i]-x)<=0$和$0<=sum (v[i]+x)$
点分一下。如果要符合条件,就要存在一条路径满足$sum (v[i]-x)<=0$和$0<=sum (v[i]+x)$
考虑计算$sum (v[i]-x)<=0$的条件,
实际上,不用整体套一个二分,可以在点分的时候二分。每次把左端点设1,右端点设为ans-1
如果我们在判定v[i]-x的话,可以把权值从小到大排序,使用双指针,这样子另外一边符合要求的是一个区间[r,n],设左边的指针是l,则随着l的增长,r不会增加。注意在寻找最小值时,最小值不能来自现在这颗子树,要记录不同于当前子树的最小值进行转移。
[xr2]永恒
[WC2014]紫荆花之恋
这道题有路径,显然可以考虑点分。
实际上,当没有修改时,假设现在分治中心为c,设d[i]表示i->c的距离,则问题转化为要求点对d[i]+d[j]<=r[i]+r[j]的点的个数。移项得到d[i]-r[i]<=r[j]-d[j]。可以插入每个d[i]-r[i],查询<=r[j]-d[j]的个数,使用平衡树解决。
有修改的情况树就是不是固定的。在每个节点上维护一个平衡树。可以直接把修改的2个节点在点分树上连边,容易发现这样子新树(原点分树插入新点的结果)还是一个点分树。
加入边后,在点分树上插入点的所有父亲的平衡树都要插入权值。
这样子时间复杂度是错的,因为可能插入的是一条链,这样子每次会在所有节点的平衡树上都插入一个数,时间复杂度会退化。
可以仿照替罪羊树的思想,当一颗子树占整颗点分树的大小太大,就重新分治,重构点分树和它的平衡树。
可以仿照替罪羊树证明复杂度是nlog^2n
这道题的思想十分经典,值得学习。