大家好,今天我们来聊聊淀粉质点分治。
又开了个有点大的坑(
点分治是一种针对树上路径问题的强有力的算法。一般看到”求树上任意两点之间的路径xxx的最大值“或”求树上任意两点之间的路径xxx的和“,那这题八成就是点分治了。
那么点分治究竟是个什么玩意儿呢?我们不妨先来看道例题:P3806 【模板】点分治1
首先暴力地枚举所有点对显然是会超时的,我们可以尝试进行一些优化。
考虑随便定一个根,将这棵无根树变成一棵有根树。这样所有路径可分为两类:
- 经过根节点的路径
- 不经过根节点的路径。
对于第一类路径,显然它的两个端点 (u,v) 应当位于根节点的两个不同子树中。我们记 (dis_i) 为根节点到节点 (i) 路径的长度。故 (u,v) 之间的距离就是 (dis_u+dis_v)。我们的目标就是找出是否存在某两个位于根节点的不同子树中的节点 (u,v) 满足 (dis_u+dis_v=k),考虑开个桶 (c_i) 维护 (dis_u=i) 的 (u) 是否存在,这样可以在 (mathcal O(| ext{size}|)) 的时间内搞定第一类路径,其中 ( ext{size}) 为树的大小。
对于第二类路径,可以通过递归根节点的子树将类路径转化为第一类路径。
但是很遗憾的是,这个算法随便一叉就能把它卡到 (n^2),比如说当树退化成一条链的时候,需要递归 (n) 次,每次递归时子树大小都是 (mathcal O(n)) 级别的。
这时注意到我们的算法中有个漏洞,那就是“随便定一个根”。还是回到一条链的情况,如果我们不简简单单地以 (1) 号节点,而是以链的中点为根。并且每次递归的时候也以对应的链的中点为根,那么递归的深度就只有 (log n) 级别,总复杂度也不过 (nlog n)。这样看来,选好这个根节点至关重要,如果我们碰巧选对了根节点,那可以大大降低算法的复杂度。
那么这个根究竟该怎么选呢?考虑每次递归的时候以当前子树的重心为根,这样去掉根节点之后子树的大小就可以尽量平衡了。
这样复杂度是否就对了呢?
还真是。
注意到重心的一个性质,那就是删掉重心后子树大小不超过 (dfrac{n}{2}),这样每次递归子树大小减半,递归深度就只有 (log n) 了。总复杂度 (nlog n)。
口胡起来非常容易,然而一写代码,漏洞百出。
下面给出模板题的代码,易错点都已用注释的形式标出:
int mx[MAXN+5],siz[MAXN+5],cent=0;//mx表示去掉点x之后剩余子树大小的最大值,siz表示当前子树的最大值,cent表示重心编号
bool vis[MAXN+5];
int dis[MAXN+5];
int sub[MAXN+5],subn=0;
int all[MAXN+5],alln=0;
bitset<MAXW+5> hav;
bool ans[MAXQ+5];
void findroot(int x,int f,int tot){//找重心,tot表示整棵子树的大小
siz[x]=1;mx[x]=0;//易错点1!注意清空
for(int e=hd[x];e;e=nxt[e]){
int y=to[e];if(vis[y]||y==f) continue;//易错点2!不能只习惯性判y==f,还要判y是否未被递归到
findroot(y,x,tot);chkmax(mx[x],siz[y]);siz[x]+=siz[y];//
} chkmax(mx[x],tot-siz[x]);
if(mx[x]<mx[cent]) cent=x;//更新重心
}
void getdis(int x,int f){//求出根节点到子树内每个点的距离dis[i]
sub[++subn]=x;
for(int e=hd[x];e;e=nxt[e]){
int y=to[e],z=cst[e];if(vis[y]||y==f) continue;//同上
dis[y]=dis[x]+z;getdis(y,x);
}
}
void solve(int x){//计算经过x的路径的答案
alln=0;all[++alln]=x;dis[x]=0;hav[0]=1;//注意清空
for(int e=hd[x];e;e=nxt[e]){
int y=to[e],z=cst[e];if(vis[y]) continue;
subn=0;dis[y]=z;getdis(y,x);
for(int i=1;i<=subn;i++) for(int j=1;j<=qu;j++)
if(q[j]>=dis[sub[i]]&&hav[q[j]-dis[sub[i]]]) ans[j]=1;
for(int i=1;i<=subn;i++) hav[dis[sub[i]]]=1;//易错点3:先算贡献再更新桶,否则会出现u,v属于同一棵子树,它们之间路径的长度不是dis[u]+dis[v],却被累加进答案的情况
for(int i=1;i<=subn;i++) all[++alln]=sub[i];
}
for(int i=1;i<=alln;i++) hav[dis[all[i]]]=0;//易错点4!不要直接memset!
}
void divcent(int x,int tot){
vis[x]=1;solve(x);
for(int e=hd[x];e;e=nxt[e]){
int y=to[e];if(vis[y]) continue;
cent=0;int szy=(siz[y]<siz[x])?siz[x]:(tot-siz[x]);//算y子树的大小
findroot(y,x,szy);divcent(cent,szy);//易错点5!这里的cent很容易习惯性写成y
}
}
int main(){
/*读入*/
mx[0]=INF;/*易错点6!记得把mx[0]初始化成INF*/findroot(1,0,n);divcent(cent,n);
}
由于点分治模板太容易出错了,故最近刷了好几道模板题增强熟练程度,几乎把点分治能踩的坑都踩了一遍。
P2634 [国家集训队]聪聪可可
P4149 [IOI2011]Race
P4178 Tree
SP1825 FTOUR2 - Free tour II
(愿上帝不要惩罚我刷水题/kk)
下面才是真正有含金量的题→