• 树分块基础


    树上分块

    大部分时候,树上问题是由树剖,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;
    }
    
    
  • 相关阅读:
    学习spring cloud记录4Eureka
    学习spring cloud记录6初识nacos
    学习spring cloud记录11Feign初体验
    kafka+springboot入门
    学习spring cloud记录8nacos环境隔离nasespace
    windows下安装oracle
    mysql修改存储路径后无法启动的问题
    图神经网络的攻击防御 Learner
    论文解读(BGRL)《LargeScale Representation Learning on Graphs via Bootstrapping》 Learner
    论文解读(GROC)《Towards Robust Graph Contrastive Learning》 Learner
  • 原文地址:https://www.cnblogs.com/IzayoiMiku/p/14691521.html
Copyright © 2020-2023  润新知