关闭

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

1138人阅读 评论(6) 收藏 举报
分类:

前言

树分块这种算法,本蒟蒻没有查询到太多资料。
而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的妹子树也可以使用相同思路。
目前笔者还没有想到什么题目就树分块可搞。因为树分块只能解决子树问题,所以这种思路基本都能套上。
这里写图片描述这里写图片描述

5
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:209371次
    • 积分:9123
    • 等级:
    • 排名:第2025名
    • 原创:689篇
    • 转载:4篇
    • 译文:0篇
    • 评论:180条
    最新评论
    文章分类