树分块基础
树上分块
大部分时候,树上问题是由树剖,LCT,树分治来解决。但在某些情况下(比如你根本想不到这玩意该怎么搞的时候,或者有时数据宽松),树分块也是一种选择。
概述
和序列分块一样,树分块也是一种暴力。大致思想仍是将将树按“某种思路”划分进数个块中,然后维护块内整体信息的算法。
根据不同的题目,“某种思路”各不相同。一般情况下,树分块在处理树的路径等具有联通性质问题的时候表现强劲。
树上分块的方法很多,接下来介绍几种常用的方法。
dfs序分块法
如题所述,我们大力求树的 dfs 序,然后对 dfs 序进行分块,处理子树信息效果不错,但是不保证块内直径长度和联通性。
Size分块法
检查当前父节点所在块的大小,如果 \(<\sqrt n\) 就把当前节点加进去,如果不然就新开一个块。块大小最大 \(\sqrt n\) ,同时保证块内联通和直径大小。但是不保证块的数量(某种菊花图可以卡这玩意)。
给定一棵树,树上每个点有一个权值,需要支持修改点权和查路径最小值。
用这个方法分块,然后对每个点统计它到块内最浅的点的答案。
查询的时候将路径拆成按 \(lca\) 两部分,再将两部分拆成数块,统计即可。
注意LCA会多出一个零散块。
修改直接修改块内统计信息即可。
关键点法(树上撒点)
设置一个阈值 \(S\) ,随机找 \(\frac{n}{S}\) 个关键点,使得每个关键点到离它最近的祖先关键点的距离不超过 \(S\)。对于所有点找到它的第一个关键祖先,将它和关键祖先分为一块。可以保证块联通,期望直径长度为 \(S\),块大小为 \(\frac{n}{S}\),但是常数较大。
-
( \(From\) 神犇 \(\color{#A0F}{mrsrz}\) ) 确定性算法:严格保证每个关键点到离它最近的祖先关键点的距离。
我们每次选择一个深度最大的非关键点,如果这个点的 \(1\sim S\) 级祖先都不是关键点,那么把它的 \(S\) 级祖先设为关键点。由这个过程可知,距离不会超过 \(x\)。并且每标记一个关键点,至少有 \(S\) 个点不会被标记。关键点数量也是对的。
Count on a tree II
给定一棵 \(n\) 节点的树,树上节点带颜色。有 \(m\) 组询问,给出两互异节点 \(u,v\) ,求路径 \(u\to v\) 上有多少不同颜色。强制在线。
\(1\le n\le 4\times 10^4, 1\le m\le 10^5\)
我们在上面分块的基础上,考虑如何统计颜色数目。
先看一条由根到叶节点的路径上的数个关键点 \(x_1,x_2,x_3\dots x_k\) 。我们使用 bitset
来维护相邻两个关键点之间出现的颜色。然后,我们可以根据递推式:\(b_{x_i\to x_j}=b_{x_i\to x_{j-1}}\text{or }b_{x_{j-1}\to 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\to t,\ v_1\to t,\ u\to u_1,\ v\to v_1,\ u_0\to u_1,\ v_0\to 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\n",ans=nw.count());
}
return 0;
}
王室联邦分块法
我们 dfs ,把子树中大于 \(B\) 的分为一组,剩余的上传分到父亲那组。由于父亲那组大于 \(B\),加进去小于 \(3B\) 。每一组即比较平均了,\(B\) 的大小会影响空间和时间的优劣,需要根据题目给定的时间和空间,时间多空间小 \(B\) 就开大,空间多时间少 \(B\) 开小。
这样分块是为了莫队的排序,而不是预处理保存信息。比如,\((u,v)\) 转移到 \((a,b)\) ,由于 \(u\) 和 \(a\) 在一个组里面,即距离不太远,转移时间不太大。
王室联邦分块法可以保证每个块的大小和直径都不超过 \(2\sqrt 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\n",cnt);//分块数(划分的省数量)
for(int i=1;i<=n;i++)
printf("%d ",id[i]);
printf("\n");
for(int i=1;i<=cnt;i++)
printf("%d ",root[i]);
return 0;
}