bzoj4539 [Hnoi2016]树(缩点+主席树+LCA【待完善)

15 篇文章 0 订阅
7 篇文章 0 订阅

题目链接

分析:
从学姐blog中扒出来的一道神题

小范围数据的暴力非常的显然,然而MLE和TLE不是我们的重点

一开始就想到把需要插入的子树直接视为一个点
加上原树,我们得到的就是一个有m+1个结点的重构树

最初的模板树缩成一点,作为大树的根结点
我们考虑插入一个子树
子树加入时会重新标号,但是大小顺序是不变的
所以一个结点代表的子树的结点编号一定是一段连续的区间
我们可以通过二分的方式查找到这个点所属的缩点后的结点,然后连接
这个时候需要注意,此时连接的边是有边权的
边权为加入的子树的根到父结点根的距离
(父结点实际上也是一个子树缩成的一个点)

在上述的插入过程中
我们需要记录连接的准确节点
但是暴力的时间复杂度是不科学的
然而子树中的结点编号大小有序,所以我们只要知道了这棵子树最小的编号,就能确定连接结点在子树中是第几大

对模板树求出dfs序列,这样子树都是连续的区间
求解区间第k大,可以用静态主席树完成

我们这样就得到了一棵m+1个结点的重构树
下面的问题就是路径长度的查询

首先想到一定是先确定两个结点(x,y)分别位于重构树中的哪个结点中
以及这两个结点在重构树中相距的距离
之后我们再在该节点的内部做LCA


口hu的题解
代码几乎不可写,于是只能%学姐的代码
网上都是dalao的题解(dalao眼里这题不是很难,所以题解对于代码实现不是很详细)
只能自己看代码,慢慢理解:

首先

我们肯定要建出原树
我们dfs一遍原树,处理出我们需要的信息:
d f n dfn dfn:dfs序
c l o clo clo:时间戳
p r e pre pre_1:原树中的倍增父结点
d i s dis dis_1:原树中的倍增路径长度
d e e p deep deep_1:原树中的结点深度
s i z e size size:原树中的子树大小
i n in in:子树在dfs序中的起始位置
o u t out out:子树在dfs序中的终止位置

void dfs_1(int now,int faa)
{
	for (int i=1;i<=20;i++)
	{
		if (deep_1[now]-(1<<i)<0) break;
		pre_1[now][i]=pre_1[pre_1[now][i-1]][i-1];
		dis_1[now][i]=dis_1[now][i-1]+dis_1[pre_1[now][i-1]][i-1];	
	}
	
    fa[now]=faa; dfn[++clo]=now; in[now]=clo; size[now]=1;
	for (int i=st_1[now];i;i=way_1[i].nxt)
	    if (way_1[i].y!=faa)
		{
			deep_1[way_1[i].y]=deep_1[now]+1;
			pre_1[way_1[i].y][0]=now; dis_1[way_1[i].y][0]=1;
			dfs_1(way_1[i].y,now);
			size[now]+=size[way_1[i].y];
		}	
	out[now]=clo;        
}

这一部分还是比较简单,容易理解的

之后我们就要按照dfs序建立主席树:

void insert(int &now,int l,int r,int x)
{
	top++;
	t[top]=t[now];
	now=top; t[now].sum++;
	if (l==r) return;
	int mid=(l+r)>>1;
	if (x<=mid) insert(t[now].l,l,mid,x);
	else insert(t[now].r,mid+1,r,x);
}

int ask(int x,int y,int l,int r,int k)
{
	if (l==r) return r;
	int tmp=t[t[y].l].sum-t[t[x].l].sum;
	int mid=(l+r)>>1;
	if (tmp>=k) return ask(t[x].l,t[y].l,l,mid,k);
	else return ask(t[x].r,t[y].r,mid+1,r,k-tmp);
}

建立主席树和第k大查询都是比较基础的
这里我们需要进一步说明一下为什么要用主席树查询第k大:
我们的建树过程实际上是对原树的一个局部复制,所以所有操作都可以放到原树上完成
考虑插入了一棵子树
而子树的结点编号和原树中的结点编号的大小关系是一样的
如果我们知道一点在新的子树中排第x
就可以在原树中查找这棵子树中的第x大,找到这个点在原树上的位置
相应的操作就可以在原树上完成了

缩点

我们开始建树
f f f:结点i的子树中结点编号的最大值
L L L:子树在dfs序中的起始位置
R R R:子树在dfs序中的结束位置
b e l o n g belong belong:缩成的结点中根的编号
p r e pre pre:子树的父结点位于父子树中的第几大

//缩点并插入 
f[0]=0; f[1]=size[1]; L[1]=1; R[1]=size[1]; belong[1]=1;
o=1;    //新结点计数器 
for (int i=1;i<=m;i++)
{
	int a; ll b;
	scanf("%d%lld",&a,&b);
	int t=find(b);              //找到插入的准确结点
	ll now=b-f[t-1];           //在小树DFS序中的排名    
	int rak=ask(root[L[t]-1],root[R[t]],1,n,now);
	o++;
	f[o]=f[o-1]+size[a];       //这个节点中的编号范围 
	L[o]=in[a]; R[o]=out[a]; belong[o]=a; pre[o]=rak;
	build(o,t,deep_1[rak]-deep_1[belong[t]]+1);
}

可以注意到,在这里有一个find函数
这个函数的作用就是定位结点在哪一个缩点内的
注意到f数组实际上是一个类似前缀和的定义
所以我们可以用二分确定:

int find(ll x)    //定位在哪一个块内 
{
	int l=1,r=o;
	while (l<=r)
	{
		int mid=(l+r)>>1;
		if (x<=f[mid-1]) r=mid-1;
		else if (x>f[mid]) l=mid+1;
		else return mid;
	}
}

我们通过build函数建立了重构树,下面就需要遍历一下重构树,维护相应信息:
d e e p deep deep_2:重构树中缩点的深度
p r e pre pre_2:重构树中缩点的倍增父结点
d i s dis dis_2:重构树中缩点的倍增路径长度

void dfs_2(int now,int faa)
{
	for (int i=1;i<=20;i++)
	{
		if (deep_2[now]-(1<<i)<0) break;
		pre_2[now][i]=pre_2[pre_2[now][i-1]][i-1];
		dis_2[now][i]=dis_2[now][i-1]+dis_2[pre_2[now][i-1]][i-1];
	}
	
	for (int i=st[now];i;i=way[i].nxt)
	    if (way[i].y!=faa)
	    {
	    	deep_2[way[i].y]=deep_2[now]+1;
	    	pre_2[way[i].y][0]=now; 
	    	dis_2[way[i].y][0]=way[i].v;
	    	dfs_2(way[i].y,now);
		}
}

目前为止,我们得到了原树和重构树的所有信息
就可以倍增的计算lca了
在代码中
l c a lca lca函数:原树的lca
L C A LCA LCA函数:重构树的lca

有点不一样的就是LCA函数:

void cl(int k,int x)
{
	for (int i=0;i<k;i++)
	    x=pre_2[x][i];
	nowy=x;
}

ll LCA(int x,int y)
{
	if (deep_2[x]<deep_2[y]) swap(x,y);
	ll d=deep_2[x]-deep_2[y];
	ll ans=0;
	for (int i=0;i<=20;i++)
	    if (d==(1<<i))
		{
			cl(i,x);
			break;
		}    
	int t=d;
	for (int i=20;i>=0;i--) if ((d>>i)&1)
	{
		t-=(1<<i);
		ans+=dis_2[x][i];
		x=pre_2[x][i];
		for (int j=0;j<=20;j++)
		    if (t==(1<<j))
		    {
		    	cl(j,x);
		    	break;
			}
	}
	if (x==y) 
	{
		flag=0; nowx=x;
		return ans;
	}
	for (int i=20;i>=0;i--)
	    if (pre_2[x][i]!=pre_2[y][i])
	    {
	    	ans+=dis_2[x][i]; 
	    	ans+=dis_2[y][i];
	    	x=pre_2[x][i]; y=pre_2[y][i];
	    }
	nowx=x; nowy=y;
	return ans+2;
}

这里就有比较多的细节了
如果 x , y x,y x,y L C A LCA LCA不是 x x x y y y,那么我们在 L C A LCA LCA的最后一步不让他们向上跳就可以得到与LCA相连的节点
所以我们不跳最后一步也不计入答案,但是 a n s ans ans要+2
因为我们这两个缩点连接到LCA上需要一个短边
找到这两个结点连接的位置,回原树做lca即可

对于其中一个点是 L C A LCA LCA的情况
我们需要在蹦最后一个 2 k 2^k 2k步的时候,单独处理一下(即在外过程跳 2 k − 1 2^k-1 2k1步,$2k=20+21+…+2(k-1) $)

同时我们需要维护几个值:
n o w x nowx nowx:x当前在的缩点
n o w y nowy nowy:y当前所在的缩点

最后

就是利用以上所有函数回答问题了
如果x和y就在一个缩点内,我们直接做原树lca即可

稍微复杂一点的就是两个点不在同一缩点内
而这又需要分成两种情况:
lca(x,y)=x或y
lca(x,y)=新结点

tip

开ll
因为到后期新结点的编号回高达 n 2 n^2 n2

#include<cstdio>
#include<cstring>
#include<iostream>
#define ll long long

using namespace std;

const ll N=100005;
const ll lg=20;
struct node{
	int y,nxt;
	ll v;
};
node way_1[N<<1],way[N<<1];
int n,m,Q,st_1[N],st[N],tot=0,tt=0,top=0;
int dfn[N],clo=0,in[N],out[N],pre[N];
int root[N],belong[N],o,lcanow,nowy,nowx;

ll deep_1[N],size[N],pre_1[N][21],dis_1[N][21],fa[N];
ll L[N],R[N],f[N];
ll deep_2[N],pre_2[N][21],dis_2[N][21];
bool flag;

struct Tree{
	int sum,l,r;
};
Tree t[N*20];

void add_t(int u,int w)    //建立原树 
{
	tt++; way_1[tt].y=w;way_1[tt].nxt=st_1[u];st_1[u]=tt;
	tt++; way_1[tt].y=u;way_1[tt].nxt=st_1[w];st_1[w]=tt;
}

void build(int u,int w,ll z)
{
	tot++; way[tot].y=w;way[tot].v=z;way[tot].nxt=st[u];st[u]=tot;
	tot++; way[tot].y=u;way[tot].v=z;way[tot].nxt=st[w];st[w]=tot;
}

void dfs_1(int now,int faa)
{
	for (int i=1;i<=20;i++)
	{
		if (deep_1[now]-(1<<i)<0) break;
		pre_1[now][i]=pre_1[pre_1[now][i-1]][i-1];
		dis_1[now][i]=dis_1[now][i-1]+dis_1[pre_1[now][i-1]][i-1];	
	}
	
    fa[now]=faa; dfn[++clo]=now; in[now]=clo; size[now]=1;
	for (int i=st_1[now];i;i=way_1[i].nxt)
	    if (way_1[i].y!=faa)
		{
			deep_1[way_1[i].y]=deep_1[now]+1;
			pre_1[way_1[i].y][0]=now; dis_1[way_1[i].y][0]=1;
			dfs_1(way_1[i].y,now);
			size[now]+=size[way_1[i].y];
		}	
	out[now]=clo;        
}

void insert(int &now,int l,int r,int x)
{
	top++;
	t[top]=t[now];
	now=top; t[now].sum++;
	if (l==r) return;
	int mid=(l+r)>>1;
	if (x<=mid) insert(t[now].l,l,mid,x);
	else insert(t[now].r,mid+1,r,x);
}

int ask(int x,int y,int l,int r,int k)
{
	if (l==r) return r;
	int tmp=t[t[y].l].sum-t[t[x].l].sum;
	int mid=(l+r)>>1;
	if (tmp>=k) return ask(t[x].l,t[y].l,l,mid,k);
	else return ask(t[x].r,t[y].r,mid+1,r,k-tmp);
}

int find(ll x)    //定位在哪一个块内 
{
	int l=1,r=o;
	while (l<=r)
	{
		int mid=(l+r)>>1;
		if (x<=f[mid-1]) r=mid-1;
		else if (x>f[mid]) l=mid+1;
		else return mid;
	}
}

void dfs_2(int now,int faa)
{
	for (int i=1;i<=20;i++)
	{
		if (deep_2[now]-(1<<i)<0) break;
		pre_2[now][i]=pre_2[pre_2[now][i-1]][i-1];
		dis_2[now][i]=dis_2[now][i-1]+dis_2[pre_2[now][i-1]][i-1];
	}
	
	for (int i=st[now];i;i=way[i].nxt)
	    if (way[i].y!=faa)
	    {
	    	deep_2[way[i].y]=deep_2[now]+1;
	    	pre_2[way[i].y][0]=now; 
	    	dis_2[way[i].y][0]=way[i].v;
	    	dfs_2(way[i].y,now);
		}
}

ll lca(int x,int y)
{
	if (deep_1[x]<deep_1[y]) swap(x,y);
	ll d=deep_1[x]-deep_1[y];
	ll ans=0;
	if (d)
	    for (int i=0;i<=20&&d;i++,d>>=1)
	        if (d&1)
	           ans+=dis_1[x][i],x=pre_1[x][i];
	lcanow=y;
	if (x==y) return ans;
	for (int i=20;i>=0;i--)
	    if (pre_1[x][i]!=pre_1[y][i])
	    {
	    	ans+=dis_1[x][i];
	    	ans+=dis_1[y][i];
	    	x=pre_1[x][i]; y=pre_1[y][i];
		}
	lcanow=pre_1[x][0];
	return ans+2;
}

void cl(int k,int x)
{
	for (int i=0;i<k;i++)
	    x=pre_2[x][i];
	nowy=x;
}

ll LCA(int x,int y)
{
	if (deep_2[x]<deep_2[y]) swap(x,y);
	ll d=deep_2[x]-deep_2[y];
	ll ans=0;
	for (int i=0;i<=20;i++)
	    if (d==(1<<i))
		{
			cl(i,x);
			break;
		}    
	int t=d;
	for (int i=20;i>=0;i--) if ((d>>i)&1)
	{
		t-=(1<<i);
		ans+=dis_2[x][i];
		x=pre_2[x][i];
		for (int j=0;j<=20;j++)
		    if (t==(1<<j))
		    {
		    	cl(j,x);
		    	break;
			}
	}
	if (x==y) 
	{
		flag=0; nowx=x;
		return ans;
	}
	for (int i=20;i>=0;i--)
	    if (pre_2[x][i]!=pre_2[y][i])
	    {
	    	ans+=dis_2[x][i]; 
	    	ans+=dis_2[y][i];
	    	x=pre_2[x][i]; y=pre_2[y][i];
	    }
	nowx=x; nowy=y;
	return ans+2;
}

int main()
{
	scanf("%d%d%d",&n,&m,&Q);
	for (int i=1;i<n;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		add_t(x,y);
	}
	deep_1[1]=1;
	dfs_1(1,0);
	
	root[0]=0;
	for (ll i=1;i<=n;i++)
	{
		root[i]=root[i-1];
		insert(root[i],1,n,dfn[i]);
	}
	
	//缩点并插入 
	f[0]=0; f[1]=size[1]; L[1]=1; R[1]=size[1]; belong[1]=1;
	o=1;    //新结点计数器 
	for (int i=1;i<=m;i++)
	{
		int a; ll b;
		scanf("%d%lld",&a,&b);
		int t=find(b);              //找到插入的准确结点
		ll now=b-f[t-1];           //在小树DFS序中的排名    
		int rak=ask(root[L[t]-1],root[R[t]],1,n,now);
		o++;
		f[o]=f[o-1]+size[a];       //这个节点中的编号范围 
		L[o]=in[a]; R[o]=out[a]; belong[o]=a; pre[o]=rak;
		build(o,t,deep_1[rak]-deep_1[belong[t]]+1);
	}
	
	deep_2[1]=1; 
	dfs_2(1,0);
	
	for (int i=1;i<=Q;i++)
	{
		ll x,y;
		scanf("%lld%lld",&x,&y);
		int xx=find(x);             //在重构树中定位 
		int yy=find(y); 
		if (xx==yy)
		{
			int t=ask(root[L[xx]-1],root[R[xx]],1,n,x-f[xx-1]);
			int tt=ask(root[L[yy]-1],root[R[yy]],1,n,y-f[yy-1]);
			printf("%lld\n",lca(t,tt));
			continue;
		} 
		
		flag=1;
		ll ans=LCA(xx,yy);
		if (!flag)
		{
			if (deep_2[xx]<deep_2[yy]) swap(xx,yy),swap(x,y);
			int t=ask(root[L[xx]-1],root[R[xx]],1,n,x-f[xx-1]);
			ans+=deep_1[t]-deep_1[belong[xx]];
			int rak=ask(root[L[nowx]-1],root[R[nowx]],1,n,y-f[nowx-1]);
			ll k=lca(rak,pre[nowy]);
			if (lcanow==rak) printf("%lld\n",ans-(deep_1[rak]-deep_1[belong[nowx]]));
			else printf("%lld\n",ans-(deep_1[pre[nowy]]-deep_1[belong[nowx]])+k);
		}
		else
		{
			int t=ask(root[L[xx]-1],root[R[xx]],1,n,x-f[xx-1]);
			int tt=ask(root[L[yy]-1],root[R[yy]],1,n,y-f[yy-1]);
			ans+=deep_1[t]-deep_1[belong[xx]];
			ans+=deep_1[tt]-deep_1[belong[yy]];
			printf("%lld\n",ans+lca(pre[nowx],pre[nowy]));
		}
	}
	
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值