树上分块
大部分时候,树上问题是由树剖,LCT,树分治来解决。但在某些情况下(比如你根本想不到这玩意该怎么搞的时候,或者有时数据宽松),树分块也是一种选择。
概述
和序列分块一样,树分块也是一种暴力。大致思想仍是将将树按“某种思路”划分进数个块中,然后维护块内整体信息的算法。
根据不同的题目,“某种思路”各不相同。一般情况下,树分块在处理树的路径等具有联通性质问题的时候表现强劲。
树上分块的方法很多,接下来介绍几种常用的方法。
dfs序分块法
如题所述,我们大力求树的 dfs 序,然后对 dfs 序进行分块,处理子树信息效果不错,但是不保证块内直径长度和联通性。
Size分块法
检查当前父节点所在块的大小,如果 (<sqrt n) 就把当前节点加进去,如果不然就新开一个块。块大小最大 (sqrt n) ,同时保证块内联通和直径大小。但是不保证块的数量(某种菊花图可以卡这玩意)。
给定一棵树,树上每个点有一个权值,需要支持修改点权和查路径最小值。
用这个方法分块,然后对每个点统计它到块内最浅的点的答案。
查询的时候将路径拆成按 (lca) 两部分,再将两部分拆成数块,统计即可。
注意LCA会多出一个零散块。
修改直接修改块内统计信息即可。
关键点法(树上撒点)
设置一个阈值 (S) ,随机找 (frac{n}{S}) 个关键点,使得每个关键点到离它最近的祖先关键点的距离不超过 (S)。对于所有点找到它的第一个关键祖先,将它和关键祖先分为一块。可以保证块联通,期望直径长度为 (S),块大小为 (frac{n}{S}),但是常数较大。
-
( (From) 神犇 (color{#A0F}{mrsrz}) ) 确定性算法:严格保证每个关键点到离它最近的祖先关键点的距离。
我们每次选择一个深度最大的非关键点,如果这个点的 (1sim S) 级祖先都不是关键点,那么把它的 (S) 级祖先设为关键点。由这个过程可知,距离不会超过 (x)。并且每标记一个关键点,至少有 (S) 个点不会被标记。关键点数量也是对的。
Count on a tree II
给定一棵 (n) 节点的树,树上节点带颜色。有 (m) 组询问,给出两互异节点 (u,v) ,求路径 (u o v) 上有多少不同颜色。强制在线。
(1le nle 4 imes 10^4, 1le mle 10^5)
我们在上面分块的基础上,考虑如何统计颜色数目。
先看一条由根到叶节点的路径上的数个关键点 (x_1,x_2,x_3dots x_k) 。我们使用 bitset
来维护相邻两个关键点之间出现的颜色。然后,我们可以根据递推式:(b_{x_i o x_j}=b_{x_i o x_{j-1}} ext{or }b_{x_{j-1} o x_j}),处理出两两之间的 bitset
。处理的复杂度 (O(frac{n^2}{S}+frac{n^3}{S^2}))。
考虑如何求答案。
我们设 (t=lca(u,v)) ,分别求出 (u,v) 祖先中,离 (u,v) 最近的关键点 (u_0,v_0) 以及离 (t) 最近且在 (t) 子树内的关键点 (u_1,v_1)。整个路径被划为六块:(u_1 o t, v_1 o t, u o u_1, v o v_1, u_0 o u_1, v_0 o v_1) 前四种都是零散块,暴力跳即可。后两块我们已经预处理了,直接取并。
求颜色个数直接调用答案 bitset
的 count()
成员函数。
时间复杂度 (O(frac{n^2}{S}+frac{n^3}{S^2}+frac{nm}{w}+mS)),空间复杂度 (O(frac{n^3}{S^2}))
我们发现这里的时间随 (S) 线性增长,空间随 (S) 平方下降,所以我们可以通过调 (S) 的大小来卡空间。
#include <bits/stdc++.h>
using namespace std;
const int N=4e4+5,S=1000;
bitset<N> bs[42][42],nw;
vector<int> vec;
int head[N],ver[N<<1],nxt[N<<1],tot=0;
void add(int x,int y)
{
ver[++tot]=y; nxt[tot]=head[x]; head[x]=tot;
ver[++tot]=x; nxt[tot]=head[y]; head[y]=tot;
}
int n,m;
int poi[N];
int sz[N],dpt[N],maxd[N],fa[N],son[N],tp[N];
int id[N],cnt=0;
int sta[N],top,gg[N],FF[N];
void dfs(int x)//找关键点
{
sz[x]=1;
maxd[x]=dpt[x];
for(int i=head[x];i;i=nxt[i])
{
int y=ver[i];
if(dpt[y]) continue;//判父节点
dpt[y]=dpt[x]+1;fa[y]=x;
dfs(y);
sz[x]+=sz[y];
if(maxd[y]>maxd[x]) maxd[x]=maxd[y];
if(sz[son[x]]<sz[y]) son[x]=y;
}
if(maxd[x]-dpt[x]>=S)
id[x]=++cnt,maxd[x]=dpt[x];//标记关键点
}
void dfs2(int x)//预处理bitset
/*利用栈来构建路径上关键点的序列*/
{
for(int i=head[x];i;i=nxt[i])
{
int y=ver[i];
if(dpt[y]>dpt[x])
{
if(id[y])
{
int ip=id[sta[top]],in=id[y];//找到栈顶的下一个相邻点
for(int t=y;t!=sta[top];t=fa[t])
bs[ip][in].set(poi[t]);//暴力统计颜色
nw=bs[ip][in];
for(int j=1;j<top;++j)//栈内其他关键点的处理
{
bitset<N> &bt=bs[id[sta[j]]][in];
bt=bs[id[sta[j]]][ip];
bt|=nw;
}
FF[y]=sta[top]; gg[y]=gg[sta[top]]+1;//记录关键点的前驱和深度
sta[++top]=y;//放入栈内
}
dfs2(y);
if(id[y]) --top;//回溯
}
}
}
void dfs3(int x)//树剖
{
if(son[x]) tp[son[x]]=tp[x],dfs3(son[x]);
for(int i=head[x];i;i=nxt[i])
{
int y=ver[i];
if(y!=son[x]&&dpt[y]>dpt[x])
dfs3(tp[y]=y);
}
}
inline int LCA(int x,int y)
{
while(tp[x]!=tp[y])
if(dpt[tp[x]]>dpt[tp[y]]) x=fa[tp[x]];
else y=fa[tp[y]];
return dpt[x]<dpt[y]?x:y;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&poi[i]),vec.push_back(poi[i]);
sort(vec.begin(),vec.end()),vec.erase(unique(vec.begin(),vec.end()),vec.end());
for(int i=1;i<=n;i++)
poi[i]=lower_bound(vec.begin(),vec.end(),poi[i])-vec.begin();//离散化
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
}
dfs(dpt[1]=1);
if(!id[1]) id[1]=++cnt;
top=1;
sta[top]=gg[1]=1;
dfs2(1),dfs3(1);
int ans=0;
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
u^=ans; nw.reset();
int lca=LCA(u,v);
while(u!=lca&&!id[u]) nw.set(poi[u]),u=fa[u];
while(v!=lca&&!id[v]) nw.set(poi[v]),v=fa[v];//寻找离u,v最近的关键点
if(u!=lca)
{
int tmp=u;
while(dpt[FF[tmp]]>=dpt[lca]) tmp=FF[tmp];//寻找离lca最近的关键点
if(tmp!=u) nw|=bs[id[tmp]][id[u]];
while(tmp!=lca) nw.set(poi[tmp]),tmp=fa[tmp];//暴力统计
}
if(v!=lca)
{
int tmp=v;
while(dpt[FF[tmp]]>=dpt[lca]) tmp=FF[tmp];
if(tmp!=v) nw|=bs[id[tmp]][id[v]];
while(tmp!=lca) nw.set(poi[tmp]),tmp=fa[tmp];
}
nw.set(poi[lca]);//记得统计LCA;
printf("%d
",ans=nw.count());
}
return 0;
}
王室联邦分块法
我们 dfs ,把子树中大于 (B) 的分为一组,剩余的上传分到父亲那组。由于父亲那组大于 (B),加进去小于 (3B) 。每一组即比较平均了,(B) 的大小会影响空间和时间的优劣,需要根据题目给定的时间和空间,时间多空间小 (B) 就开大,空间多时间少 (B) 开小。
这样分块是为了莫队的排序,而不是预处理保存信息。比如,((u,v)) 转移到 ((a,b)) ,由于 (u) 和 (a) 在一个组里面,即距离不太远,转移时间不太大。
王室联邦分块法可以保证每个块的大小和直径都不超过 (2sqrt N−1),但是不保证块联通
『SCOI2005』王室联邦
本题就是这个分块做法的来源。
见代码,算法执行完成分块也就完成了。
#include <bits/stdc++.h>
using namespace std;
const int N=1e4;
int n,B;
int head[N], ver[N<<1],nxt[N<<1],tot=0;
void add(int x,int y)
{
ver[++tot]=y; nxt[tot]=head[x]; head[x]=tot;
ver[++tot]=x; nxt[tot]=head[y]; head[y]=tot;
}
int sta[N],top=0;//栈
int id[N],root[N],cnt=0;//每个点所在分块,每个块的关键点(首都),计数器
void dfs(int x,int f)
{
int nw=top;//由于这是全局栈,所以要记录当前栈顶
for(int i=head[x];i;i=nxt[i])
{
int y=ver[i];
if(y==f) continue;
dfs(y,x);
if(top-nw>=B)//如果当前栈内点数够
{
root[++cnt]=x;
while(top!=nw) id[sta[top--]]=cnt;//分到一个块里面去
}
}
sta[++top]=x;
}
int main()
{
scanf("%d%d",&n,&B);
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
}
dfs(1,0);
if(cnt==0) root[++cnt]=1;
while(top) id[sta[top--]]=cnt;//剩余节点的处理
printf("%d
",cnt);//分块数(划分的省数量)
for(int i=1;i<=n;i++)
printf("%d ",id[i]);
printf("
");
for(int i=1;i<=cnt;i++)
printf("%d ",root[i]);
return 0;
}