入门树形结构详解&&洛谷P5836题解

前言

本题之所以被用于"入门树形结构详解",是因为AC本题需要下面几个前置芝士:

①倍增优化LCA;

②树上前缀和;

③树上差分。

本篇文章适合树形结构初学者阅读。讲得详细的程度远远超过您的想象!

题面

题目描述

Farmer John 计划建造 N个农场,用 N-1条道路连接,构成一棵树(也就是说,所有农场之间都互相可以到达,并且没有环)。每个农场有一头奶牛,品种为更赛牛或荷斯坦牛之一。

Farmer John 的 MM 个朋友经常前来拜访他。在朋友 ii 拜访之时,Farmer John 会与他的朋友沿着从农场Ai到农场Bi的唯一路径行走(有可能出现Ai=Bi的情况)。除此之外,他的朋友们还可以品尝他们经过的路径上任意一头奶牛的牛奶。由于 Farmer John 的朋友们大多数也是农场主,所以他们对牛奶有着极强的偏好。有些朋友只喝更赛牛的牛奶,其余的只喝荷斯坦牛的牛奶。任何 Farmer John 的朋友只有在他们访问时能喝到他们偏好的牛奶才会高兴。

请求出每个朋友在拜访过后是否会高兴。如果高兴输出0,否则输出1,详见输出格式。

输入输出格式

输入格式

输入的第一行包含两个整数N和M。

第二行包含一个长为 N的字符串。如果第 i个农场中的奶牛是更赛牛,则字符串中第i个字符为G,如果第 i个农场中的奶牛是荷斯坦牛则为H。

以下 N-1行,每行包含两个不同的整数X和Y,表示农场 X与Y之间有一条道路。

以下M行,每行包含整数Ai, Bi和一个字符Ci,表示朋友i摆放时从Ai走到Bi(沿着唯一路径行走),且他喜欢喝的牛奶的种类。

输出格式
输出一个长为M的二进制字符串。如果第i个朋友会感到高兴,则字符串的第i个字符为1;否则为0。

样例数据

Input:
5 5
HHGHG
1 2
2 3
2 4
1 5
1 4 H
1 4 G
1 3 G
1 3 H
5 5 H

Output:
10110

数据范围

共有12个测试点,各测试点均分。

第一个测试点: 与样例相同;

第2~5个测试点: N 1 0 3 N≤10^3 , M 2 × 1 0 3 M≤2×10^3

对于100%的数据满足, 1 N , M 1 0 5 1≤N, M≤10^5

样例解释

在这里,从农场 1 到农场 4 的路径包括农场 1、2 和 4。所有这些农场里都是荷斯坦牛,所以第一个朋友会感到满意,而第二个朋友不会。

解法

首先,显而易见地发现,节点u到节点v的唯一路径为u→LCA(u,v)→v,其中LCA(u,v)表示u与v的最近公共祖先。

所以,我们只需要找到LCA(u,v),并判断这条路径是否存在H或G即可。但是我们不能把这条路走一遍,而要使用树上前缀和与树上差分来优化这个过程。

树上前缀和

为了表述方便,设1号节点为根节点(设任何一个节点为根节点都行,不会影响答案的正确性)

树上前缀和与一维前缀和类似,但是本题中有两个参数,即从该节点到1号根节点的H的数量与G的数量。所以,这里的前缀和表示为pre[i].h与pre[i].g,其中pre[i].h表示从i号节点到1号节点的H的数量,pre[i].g表示从i号节点到1号节点的G的数量。

递推十分显然,dfs即可,这里不再详细论述。

然后,也容易发现,从i号节点到j号节点(i的深度比j的深度小)的H的数量为dp[j].h-dp[father[i]].h,G的数量同理;这里father[i]表示i的父节点。如果不懂可以画图或拿一维前缀和来类比一下,就能瞬间明白。

这下思路就很清晰啦。剩下的就是对码力的考验,但我对自己的码力是很有自信的。

萌新福利

相信看这篇博客的都是与我一样的普及组的小萌新——如果阁下不是,请接收我的orz并跳过此"详细讲解"。

①定义

int n,q,u,v,cnt=0;//n为节点数,q为询问数 
int head[100005],father[100005][20],depth[100005],lg[100005],ans[100005];//LCA必备
//ans[i]表示第i次询问的答案,最后一起输出(强迫症)
char t,s[100005];//s存储每个农场是啥种奶牛

struct edge
{
	int next;
	int to;
}e[200005];//前向星存图

struct node
{
	int H;
	int G;
}a[100005];//树上前缀和

②加边函数

inline void add_edge(int u,int v)
{
	cnt++;	
	e[cnt].to=v;
	e[cnt].next=head[u];
	head[u]=cnt;
}

③跑一遍dfs,求出一些重要的东西

inline void dfs(int now,int fath)
{
	father[now][0]=fath;//它的父亲
	depth[now]=depth[fath]+1;//它的深度是它父亲的深度加一
	
	if (s[now]=='H')//树上前缀和递推
	{
		a[now].H=a[fath].H+1;
		a[now].G=a[fath].G;
	}
	else//树上前缀和递推
	{
		a[now].H=a[fath].H;
		a[now].G=a[fath].G+1;
	}
	for (int i=1;i<=lg[depth[now]];i++)  father[now][i]=father[father[now][i-1]][i-1];//核心代码,跑出与自己距离为2^i的祖宗们
	for (int i=head[now];i;i=e[i].next)
	{
		if (e[i].to!=fath)  dfs(e[i].to,now);
	}//接着dfs,枚举它的儿子
}

④LCA的模板(这不用说了吧,如果不会建议您先去看看LCA怎么搞再回来)

int LCA(int u,int v)
{
	if (depth[u]<depth[v])  swap(u,v);
	while (depth[u]>depth[v])  u=father[u][lg[depth[u]-depth[v]]-1];
	if (u==v)  return u;
	
	for (int k=lg[depth[u]];k>=0;k--)
	{
		if (father[u][k]!=father[v][k])
		{
			u=father[u][k];
			v=father[v][k];
		}
	}
	return father[u][0];
}

⑤主函数:

signed main()
{
	cin>>n>>q;
	for (int i=1;i<=n;i++)  cin>>s[i];
	for (int i=1;i<n;i++)
	{
		cin>>u>>v;
		add_edge(u,v);
		add_edge(v,u);
	}
	for (int i=1;i<=n;i++)  lg[i]=lg[i-1]+((1<<lg[i-1])==i);
	
	dfs(1,0);
	
	for (int i=1;i<=q;i++)
	{
		cin>>u>>v>>t;
		
		if (judge(u,v,t))  ans[i]=1;
		else ans[i]=0;
	}
	for (int i=1;i<=q;i++)  cout<<ans[i];
	cout<<endl;
	
	return 0;
}

主函数大家肯定都能看懂。

但是,有人就要问了: judge是啥?

告诉您,这是本篇代码的核心,要放在最后。注意这里judge是一个bool函数,即判断如果FJ的一位朋友从u号节点到v号节点且喜欢分类为t(t=‘H’或’G’)的奶牛,他是否会开心。

inline bool judge(int u,int v,char t)
{
	int now=LCA(u,v),k=father[now][0],cntH=0,cntG=0;
	cntH=(a[u].H-a[k].H)+(a[v].H-a[k].H);//累加H的数量
	cntG=(a[u].G-a[k].G)+(a[v].G-a[k].G);//累加G的数量
	
	if (s[now]=='H')  cntH--;
	else cntG--;//把重复算的简一下
	//类似树上差分的操作
	
	if (t=='H')
	{
		if (cntH>0)  return true;
		else return false;
	}
	else if (t=='G')
	{
		if (cntG>0)  return true;
		else return false;
	}//看看朋友们是否会开心,如果开心就return true否则return false
}

放一张图,您将会瞬间明白:

在这里插入图片描述
然后发现,这里LCA(u,v)被算了两遍。

所以,如果LCA(u,v)为’H’那么H就被多算了一次,需要减去1;LCA(u,v)为’G’时同理。

So easy! Isn’t it?

评价

①难度: 普及+/提高(洛谷难度明显评低了)

②做题时间: 32min

③分数: 100

事实上还有一个O(n+q)的用连通块解决的算法……但是本文是几个树形结构的详解,而不是连通块的详解,所以更好的做法这里不再阐述。

原创文章 19 获赞 27 访问量 1948

猜你喜欢

转载自blog.csdn.net/Cherrt/article/details/105490408
今日推荐