zoukankan      html  css  js  c++  java
  • 树分块基础

    树上分块

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

    求颜色个数直接调用答案 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
    ",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;
    }
    
    
  • 相关阅读:
    springcloud 项目源码 微服务 分布式 Activiti6 工作流 vue.js html 跨域 前后分离
    springcloud 项目源码 微服务 分布式 Activiti6 工作流 vue.js html 跨域 前后分离
    OA办公系统 Springboot Activiti6 工作流 集成代码生成器 vue.js 前后分离 跨域
    java企业官网源码 自适应响应式 freemarker 静态引擎 SSM 框架
    java OA办公系统源码 Springboot Activiti工作流 vue.js 前后分离 集成代码生成器
    springcloud 项目源码 微服务 分布式 Activiti6 工作流 vue.js html 跨域 前后分离
    java 视频播放 弹幕技术 视频弹幕 视频截图 springmvc mybatis SSM
    最后阶段总结
    第二阶段学习总结
    第一阶段学习总结
  • 原文地址:https://www.cnblogs.com/IzayoiMiku/p/14691521.html
Copyright © 2011-2022 走看看