树分块基础

树上分块

大部分时候,树上问题是由树剖,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\) 前四种都是零散块,暴力跳即可。后两块我们已经预处理了,直接取并。

求颜色个数直接调用答案 bitsetcount() 成员函数。

时间复杂度 \(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;
}

posted @ 2021-04-22 21:01  RemilaScarlet  阅读(804)  评论(1编辑  收藏  举报