前言
本题之所以被用于"入门树形结构详解",是因为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 N≤103, M ≤ 2 × 1 0 3 M≤2×10^3 M≤2×103。
对于100%的数据满足, 1 ≤ N , M ≤ 1 0 5 1≤N, M≤10^5 1≤N,M≤105。
样例解释
在这里,从农场 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)的用连通块解决的算法……但是本文是几个树形结构的详解,而不是连通块的详解,所以更好的做法这里不再阐述。