【2021牛客暑期多校训练营】7-F xay loves trees(dfs序,主席树+标记永久化)

链接:https://ac.nowcoder.com/acm/contest/11258/F
来源:牛客网


题目描述:

You have two trees rooted at 1 that both have n nodes. You need to find the largest subset of {1,2,⋯,n} such that:

  1. On the first tree, the set is connected, and for any two points u, v in set, one of u or v is an ancestor of the other.
  2. On the second tree, for any two points u, v in set, none of u or v is an ancestor of the other.

Output the biggest size.

大意:给出两棵有n个节点的树,编号为1~n且根为1,求一个满足:

  1. 任意两点在第一棵树中为祖先后代关系,所有节点在第一棵树中联通。
  2. 任意两点在第二棵树中不为祖先后代关系。

的最大点集的大小。


输入格式:
第一行一个整数t,即有t组数据。
接下来每一组数据:

  • 第一行一个整数n。
  • 接下来n-1行,每行两个整数,代表第一棵树的两个节点之间有边。
  • 接下来n-1行,每行两个整数,代表第二棵树的两个节点之间有边。

输出格式:
t行,每行一个整数,分别是每组数据的答案。


数据范围:
1 ≤ t ≤ 10 , n ≥ 1 , Σ n ≤ 3 × 1 0 5 . 1 \le t \le 10, \\n \ge 1 , \\\Sigma n \le 3×10^5. 1t10,n1,Σn3×105.



这道题比赛的时候老是想用lca类似方法做,我们三个都觉得应该建st表,不过想了想并不是这样,这样确实可以很快地判断两点是否有祖先后代关系,但每次加入对集合中所有的原有点都判断一遍显然会爆炸。
我们又想到找出已经选中的点封锁了哪些点,标记这些具有该性质的点。容易知道,将子树与其到根的链设为不可选。确实,这样加入点可以很快判断是否被标记,但搜索时将这个点加入取出时需要进行标记和释放,并不能很快完成,依然会爆炸。所以我们队并没有在考场上解决这个问题。


听了讲解后,我们得知了一条重要性质:两点具有祖先后代关系⇔两点的子树有交,而又知道一个点的子树的dfs序是一个连续的区间(下称“子树区间”),可以想到用线段树来维护选中状态。所以先在第二棵树上跑一个dfs序,求出每个节点的子树区间,再在第一棵树上搜索,选取一条直上直下的链,即不断选取接下来的节点并判断、标记。
可以想象,区间需要不断被修改、回退到加入点前的状态,实现方法有多种,具体我了解两种实现方法:

线段树+滑动窗口

其一是普通的线段树+滑动窗口的思想,线段树实现区间加【区间+1⇔标记被访问过】和区间最大值查询【若加入一点的子树(该点子树区间+1)后,整棵树的最大值仍为1,说明这个子树在之前没有被加入,说明该点可被加入集合】。
搜索时,先判断该点是否可以加入,如果可以就加入并更新答案(搜索过程中,出现的最长链的长度),否则需要运用滑动窗口的思想,丢弃这条链上最顶部的点(丢弃:子树区间-1,消除影响),以便让该点加入。注意在该点搜索完毕后,需要消除该点的影响(区间-1),如果丢弃了点,还应将丢弃的点重新加入。
该做法在其他选手的代码中出现了多次,也是一种较为节省空间的方法,注意消除影响的时候的细节,我没有亲自实现。

主席树

相比线段树需要在搜索后消除影响(目的就是回到之前版本的线段树),那么可以直接使用主席树维护加入各点时的状态,即最朴素的可持久化思想。
另外,相比还需要用滑动窗口维护链,此时可以用每个版本实现最大值修改最大值查询。(普通线段树无法消除最大值覆盖的影响,则不能采用这种思路)
注意,此处的最大值的意义与上一种思路完全不同,此处的最大值是第一棵树上的深度,即选中某点时,将此点的深度覆盖到子树区间,在查询时,若查询的点x的子树区间被覆盖(说明与之前某点矛盾),返回的就是覆盖该子树的最深点的深度(可能该子树区间有多个子区间,被不同的点覆盖过),即产生矛盾的点中最深点的深度d,说明x与“从x向上到深度为d+1的点集”没有冲突,但不能说明这个点集中任意两点没有冲突,所以想要获得没有冲突的点集,需要再与x向上的链中的点求这个最大深度,可以在递归过程中不断与父节点取最大深度实现。
得到了x向上的没有冲突的链(最高点深度为ans[x] [不是答案] 后,该链长度为dep[x]-ans[x]+1,求链长度的最大值 [这才是答案] 即可。
然而这个维护最大值修改和查询的线段树并不能像以前那样打lazy标记然后上推下推,以测试数据的第一组为例,跑完数据后的结果主席树是这样的:
某次数据跑完后产生的主席树
可见,这样在查询时下推确实获得了本次的数据,然而可能破坏了之前版本的线段树,再查询以前版本就乱套了。
同样,在树套树时也不能直接把标记推来推去,不然给你一个线段树的树套树,外层的线段树上推一下,就对应里面树的每个节点上推, O ( n 2 l o g 2 n ) O(n^2 log^2n) O(n2log2n)优秀数据结构诞生辣。树套树的标记永久化例题:[POI2006]TET-Tetris 3D
此时可以用到标记永久化的技巧。以此题的求最大值为例:
Tg为懒标记,M为最大值。

  • optmax(设置最大值v)时,途径的所有节点M都对这个v取max,到了完全包含节点(需要return时),节点M对v取完max之后,将该节点的标记Tg再对v取max。
  • querymax(查询最大值)时,记录沿途所有标记Tg的最大值,最后return时再对最后的节点M取最大值即可,不改变任何节点的值。

不难发现,这样一路推下去,有机结合是正确的。最后的节点上方未处理的最大值已经被标记Tg记录推了下来,而节点本身下方的修改早就被M处理过了。

上代码:

#include<cstdio>
#include<vector>
#include<algorithm>
#define mid ((l+r)>>1)
using namespace std;
const int maxn=300005;
vector<int> g[3][maxn];
//g[id][x][y]代表第id棵树上x~y连的有向边
void addedge(int gid,int x,int y){
	g[gid][x].push_back(y);
	g[gid][y].push_back(x);
	//无向边
}
int tt,n,x,y,M[maxn<<6],L[maxn<<6],R[maxn<<6],Tg[maxn<<6],T[maxn];
//M[]为最大值,Tg[]为永久化的标记,L[]、R[]为左右子树,T[]为主席树的根
//空间不同于一次固定添加一条链的主席树,区间修改的主席树可能开更多节点
int d[maxn],ll[maxn],rr[maxn],ans[maxn],ac,tot,tim;
//d[x]为x第一棵树上的深度,[ll[x],rr[x]]为x的子树区间,ans[x]为x最高爬到的点
//ac为一组数据的答案,tot为主席树节点数,tim为第二棵树dfs序的时间戳
int build(int l,int r){//每次建初始的空树
	int o=++tot;
	if(l<r){
		L[o]=build(l,mid);
		R[o]=build(mid+1,r);
	}
	return o;
}
int optmax(int p,int l,int r,int ql,int qr,int v){
	int o=++tot;
	L[o]=L[p]; R[o]=R[p]; M[o]=max(M[p],v);//访问到即更新
	if(ql<=l&&r<=qr){Tg[o]=max(Tg[o],v);return o;}//返回时再打标记
	if(ql<=mid)L[o]=optmax(L[p],l,mid,ql,qr,v);
	if(qr>mid)R[o]=optmax(R[p],mid+1,r,ql,qr,v);
	return o;
}
int querymax(int o,int l,int r,int ql,int qr){
	if(ql<=l&&r<=qr)return M[o];
	int ac=Tg[o];//记录标记的最大值
	if(ql<=mid)ac=max(ac,querymax(L[o],l,mid,ql,qr));
	if(qr>mid)ac=max(ac,querymax(R[o],mid+1,r,ql,qr));
	return ac;
}
void dfs2(int fa,int x){
	ll[x]=++tim;
	for(auto &y:g[2][x])
		if(y!=fa)dfs2(x,y);
	rr[x]=tim;
}
void dfs1(int fa,int x){
	ans[x]=querymax(T[fa],1,n,ll[x],rr[x])+1;
	//未加入该点的上一个覆盖状态,即刚加入父节点后
	//只能保证x点与到ans[x]的点满足约束
	ans[x]=max(ans[x],ans[fa]);//递归保证链上其他点也满足约束
	ac=max(ac,d[x]-ans[x]+1);//更新答案
	T[x]=optmax(T[fa],1,n,ll[x],rr[x],d[x]);//加入x点
	for(auto &y:g[1][x])//继续搜索
		if(y!=fa){
			d[y]=d[x]+1;
			dfs1(x,y);
		}
}
int main(){
	scanf("%d",&tt);
	while(tt--){
		ac=tim=0;
		//注意不清空tot,即主席树节点数,否则新建节点可能标记未清空
		//题目给出的数据范围是所有的节点加起来不超过某个数,也是一种提示
		//*当然,如果新建点时清空标记,此处可以重置tot,因为主席树的节点只有新建时更新,但其他数据结构的标记永久化就需要先清空整个树
		scanf("%d",&n);
		for(int i=1;i<=2;i++)for(int j=1;j<=n;j++)g[i][j].clear();
		for(int i=1;i<=2;i++)
			for(int j=1;j<=n-1;j++){
				scanf("%d%d",&x,&y);
				addedge(i,x,y);
			}
		dfs2(0,1);//第二棵树上的dfs序,计算每个节点的子树区间
		build(1,n);d[1]=1;//重新建树,根节点深度为1
		dfs1(0,1);//在第一棵树上搜索
		printf("%d\n",ac);
	}
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值