您的位置:首页 > 其它

关于树分块算法的一些研究

2016-05-13 21:23 309 查看

前言

树分块这种算法,本蒟蒻没有查询到太多资料。

而gty系列中,出现过如“gty的妹子树”这样的题目,流传着一种“树分块”做法。

然而,网络流传的树分块算法都可以被菊花图卡掉。

流传的方法:一个结点的父亲所在块未满,就让该结点加入其父亲所在块,否则自成一块。

至于这个方法最早的出处我就不清楚了。

本文将介绍正统的树分块算法(其实也是笔者yy的)。

先把结论放上:其实树分块并没有什么卵用。至于为什么没用,往下读呗。

可能树分块有什么特别的用处本蒟蒻不知道,有问题请留言提出!以下都是我的主观臆断。

正统分块方法

正统的分块方法应该是如“王室联邦”一题的分块方法。树块具体定义如下:

1、除根节点所在块以外,每一块内深度最小的结点的父亲相同。这个父亲被称之为该块的块顶,其中特别的根节点也是块顶。

2、每一块内非深度最小的结点的父亲一定与其处于同一块中。

3、B<=每块大小<=3B。B是你定义的一个常数(B就是决定块大小和块个数的,修改B值会影响算法的最终复杂度)

也就是说,对于块顶不在块内的结点,加上块顶这个块就联通。

如何满足这三点要求可以看代码,接着会进行解释。

这是王室联邦那道题的代码,算法执行完后便完成了分块。

#include<cstdio>
#include<algorithm>
#define fo(i,a,b) for(i=a;i<=b;i++)
using namespace std;
const int maxn=1000+10;
int h[maxn],go[maxn*2],next[maxn*2],size[maxn],belong[maxn],cap[maxn],s[maxn];
int i,j,k,l,t,n,m,c,tot,top,cnt;
void add(int x,int y){
go[++tot]=y;
next[tot]=h[x];
h[x]=tot;
}
void dfs(int x,int y){
s[++top]=x;
int t=h[x];
while (t){
if (go[t]!=y){
dfs(go[t],x);
size[x]+=size[go[t]];
if (size[x]>=c){
size[x]=0;
cap[++cnt]=x;
while (s[top]!=x) belong[s[top--]]=cnt;
}
}
t=next[t];
}
size[x]++;
}
void dg(int x,int y,int z){
if (belong[x]) z=belong[x];else belong[x]=z;
int t=h[x];
while (t){
if (go[t]!=y) dg(go[t],x,z);
t=next[t];
}
}
int main(){
scanf("%d%d",&n,&c);
if (n<c){
printf("0\n");
return 0;
}
fo(i,1,n-1){
scanf("%d%d",&j,&k);
add(j,k);add(k,j);
}
dfs(1,0);
if (!cnt) cap[cnt=1]=1;
dg(1,0,cnt);
printf("%d\n",cnt);
fo(i,1,n) printf("%d%c",belong[i],(i==n)?'\n':' ');
fo(i,1,cnt) printf("%d%c",cap[i],(i==cnt)?'\n':' ');
}


belong表示一个结点的所属块,cap表示一个块的块顶。size[x]表示以x为根的子树内,所属块块顶不在以x为根的子树内的结点有多少个。容易知道,这些结点最终会和x处于同一块。

易知这样执行算法可以满足树块定义的前两条,我们来证明为何其满足第三条。第三条定义也是能够保证算法复杂度的很重要的一条。

先看第一遍深搜,我们可以得到结论size[x]<B,不然就会继续分成一块。然后因为每次一达到B就会分成一块,所以第一遍深搜后每块大小至少为B至多为2B。

因为size[x]<B,所以第二遍深搜每块最多被塞进B个结点。

那么就满足条件了!

然后我们定义块树,块i所代表的结点的父亲设为块i的块顶所属块,那么可以建出块树。

对于通常可以使用树分块的题目,都是进行子树询问。那么因为size[x]<B,所以剩余部分不超过B个,直接暴力。然后接下来在块树内快速访问所有块顶都在子树内的块。

这种分块也是可以动态的,支持加点操作的话,比如随便弄个动态树链剖分就好了(大雾。

一道题目

一颗N个结点的树,逆序对(i,j)定义为i是j的祖先且ai>aj。要求在线询问一个子树内逆序对个数并且有修改一个结点的值。

这个我们可以用树分块解决!

同“Gty的文艺妹子序列”那题,ans[i,j]表示第i块内取一个元素与第j块取一个元素能形成的逆序对个数,其中i是块树中j的祖先。

sum[i,j]表示块树中以第i块为根的子树中j元素的出现次数。

num[i]表示第i块内部形成的逆序对个数。

对于询问操作,先广度搜索处理出剩余部分形成的逆序对个数,就是一层层的做再添进线段树内。然后扫描块树处理出每块内部形成的逆序对个数。接下来就只需要考虑跨块的情况。

显然对于一个块i,可以利用ans计算其影响。但第二维枚举很不值,由于可以和其造成影响的是一个子树,所以就可以dfs序变成序列上的区间,然后ans数组第二维就用线段树维护即可。

然后块与块之间就解决了。

接下来考虑剩余部分与块之间产生的影响。

因为有sum数组,只要第二维也用线段树维护即可。

然后考虑修改操作。

对ans的影响是块个数个的,对sum的影响也是块个数个的,重新计算num的复杂度是与块大小有关的。它们都不大,暴力乱搞。

树分块没卵用

像上面提到的题目甚至gty的妹子树那道题,都有比树分块优秀的做法。

考虑上面那道题。

如果只有询问操作,那我会做!可持久化线段树合并就行了,可以处理出答案数组。

有修改操作了?

对修改操作分块,每做完一块内所有询问,就暴力重构可持久化线段树。块内的询问,由于距离上一次重构所经过的修改操作次数很小,因此可以枚举并计算对答案的影响。

gty的妹子树也可以使用相同思路。

目前笔者还没有想到什么题目就树分块可搞。因为树分块只能解决子树问题,所以这种思路基本都能套上。



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: