CF375D Tree and Queries / Dsu on tree 模板

Tree and Queries

CF375D (Luogu)

题面翻译

  • 给定一棵 \(n\) 个节点的树,根节点为 \(1\)。每个节点上有一个颜色 \(c_i\)\(m\) 次操作。操作有一种:
    1. u k:询问在以 \(u\) 为根的子树中,出现次数 \(\ge k\) 的颜色有多少种。
  • \(2\le n\le 10^5\)\(1\le m\le 10^5\)\(1\le c_i,k\le 10^5\)

题目描述

You have a rooted tree consisting of \(n\) vertices. Each vertex of the tree has some color. We will assume that the tree vertices are numbered by integers from 1 to $ n $ . Then we represent the color of vertex \(v\) as \(c_{v}\) . The tree root is a vertex with number 1.

In this problem you need to answer to $ m $ queries. Each query is described by two integers \(v_{j},k_{j}\) . The answer to query \(v_{j},k_{j}\) is the number of such colors of vertices \(x\) , that the subtree of vertex \(v_{j}\) contains at least \(k_{j}\) vertices of color \(x\) .

You can find the definition of a rooted tree by the following link: http://en.wikipedia.org/wiki/Tree_(graph_theory).

输入格式

The first line contains two integers \(n\) and \(m\) \((2<=n<=10^{5}; 1<=m<=10^{5})\) . The next line contains a sequence of integers \(c_{1},c_{2},...,c_{n}\) \((1<=c_{i}<=10^{5})\) . The next \(n-1\) lines contain the edges of the tree. The \(i\) -th line contains the numbers \(a_{i},b_{i}\) \((1<=a_{i},b_{i}<=n; a_{i}≠b_{i})\) — the vertices connected by an edge of the tree.

Next \(m\) lines contain the queries. The \(j\) -th line contains two integers \(v_{j},k_{j}\) \((1<=v_{j}<=n; 1<=k_{j}<=10^{5})\) .

输出格式

Print \(m\) integers — the answers to the queries in the order the queries appear in the input.

样例 #1

样例输入 #1

8 5
1 2 2 3 3 2 3 3
1 2
1 5
2 3
2 4
5 6
5 7
5 8
1 2
1 3
1 4
2 3
5 3

样例输出 #1

2
2
1
0
1

样例 #2

样例输入 #2

4 1
1 2 3 4
1 2
2 3
3 4
1 1

样例输出 #2

4

提示

A subtree of vertex \(v\) in a rooted tree with root \(r\) is a set of vertices \({u :dist(r,v)+dist(v,u)=dist(r,u)}\) . Where \(dist(x,y)\) is the length (in edges) of the shortest path between vertices \(x\) and \(y\) .

Solution

对于刚学 \(\texttt{Dsu on tree}\),这道题可以作为模板题来进行入门以及基础思想的熟悉和练习。

\(\texttt{Dsu on tree}\) / 树上启发式合并,是一种根据经验来对树上的合并操作进行优化的办法,一般适用于需要统计子树的颜色种类,并且是离线的。这种做法既不像树套树一样难写,也不像树上莫队一样有 \(\mathcal O(n\sqrt n)\) 的时间复杂度。\(\texttt{Dsu on tree}\) 的代码不难写,并且时间复杂度是 \(\mathcal O(n\log n)\) 的。因此在一些题中,用 \(\texttt{Dsu on tree}\) 可以糊一些部分分,甚至于暴打 \(\texttt{std}\)

将每个点的颜色存储在 \(col\) 数组中,用 \(cnt\) 作为桶来统计颜色,用 \(sum_i\) 来表示颜色数量至少为 \(i\) 的颜色数。首先先像重轻链剖分一样用一个 \(\texttt{DFS}\) 跑出每个点的重儿子、子树大小、\(\texttt{DFS}\) 序。然后对于每一个子树进行以下操作:

  • 计算所有轻儿子的答案并清除对 \(cnt\)\(sum\) 数组的贡献。
  • 计算重儿子答案并且保留对 \(cnt\)\(sum\) 的贡献。
  • 将轻儿子贡献加入 \(cnt\)\(sum\) 数组并更新答案

用到这种启发式合并思想的还有按秩合并并查集(将小的集合并入大的集合,从而达到 \(\mathcal O(\log n)\) 的时间复杂度)。\(\texttt{Dsu on tree}\) 的时间复杂度证明比较麻烦,不过可以感性认识:因为树上的重儿子个数最多只有 \(\log n\) 个,所以时间复杂度就是 \(\mathcal O(n\log n)\) 的。

可能参考代码更好理解:

#include<bits/stdc++.h>
using namespace std;
template<typename T> void read(T &k)
{
	k=0;T flag=1;char b=getchar();
	while (!isdigit(b)) {flag=(b=='-')?-1:1;b=getchar();}
	while (isdigit(b)) {k=k*10+b-48;b=getchar();}
	k*=flag;
}
template<typename T> void write(T k) {if (k<0) putchar('-'),write(-k);if (k>9) write(k/10);putchar(k%10+48);}
template<typename T> void writewith(T k,char c) {write(k);putchar(c);}
const int _SIZE=1e5;
struct EDGE{
	int nxt,to;
}edge[(_SIZE<<1)+5];
int tot,head[_SIZE+5];
void AddEdge(int x,int y)
{
	edge[++tot]=(EDGE){head[x],y};
	head[x]=tot;
}
int n,m,col[_SIZE+5];
int id[_SIZE+5],node[_SIZE+5],son[_SIZE+5],siz[_SIZE+5],dfn;
int ans[_SIZE+5];
int cnt[_SIZE+5],sum[_SIZE+5];
vector<pair<int,int> > vec[_SIZE+5];//用于存储对于每个节点的询问
void dfs1(int x,int fa)//第一遍dfs,跑dfs序,重儿子和size
{
	id[x]=++dfn;//dfs序
	node[dfn]=x;//dfs序对应的节点
	siz[x]=1;
	int maxson=-1;
	for (int i=head[x];i;i=edge[i].nxt)
	{
		int twd=edge[i].to;
		if (twd==fa) continue;
		dfs1(twd,x);
		siz[x]+=siz[twd];
		if (siz[twd]>maxson) maxson=siz[twd],son[x]=twd;
	}
}
void add(int x) {cnt[col[x]]++;sum[cnt[col[x]]]++;}//加入该点的影响
void del(int x) {sum[cnt[col[x]]]--;cnt[col[x]]--;}//清除该点的影响
void dfs2(int x,int fa,bool keep)//keep用于告知当前点对cnt和sum的贡献是否应该清除
{
	for (int i=head[x];i;i=edge[i].nxt)//计算轻儿子答案
	{
		int twd=edge[i].to;
		if (twd==fa || twd==son[x]) continue;
		dfs2(twd,x,false);//不保留贡献
	}
	if (son[x]) dfs2(son[x],x,true);add(x);//计算重儿子答案,保留贡献,加入当前点的贡献
	for (int i=head[x];i;i=edge[i].nxt)//加入轻儿子的贡献
	{
		int twd=edge[i].to;
		if (twd==fa || twd==son[x]) continue;
		for (int j=id[twd];j<=id[twd]+siz[twd]-1;j++) add(node[j]);//同一子树dfs序连续,直接for即可
	}
	for (auto i:vec[x]) ans[i.first]=sum[i.second];//遍历当前节点x的询问并更新答案
	if (!keep) for (int i=id[x];i<=id[x]+siz[x]-1;i++) del(node[i]);//如果需要不保留贡献则将当前节点的子树全部清除贡献
}
int main()
{
	read(n);read(m);
	for (int i=1;i<=n;i++) read(col[i]);
	for (int i=1;i<n;i++) 
	{
		int u,v;read(u),read(v);
		AddEdge(u,v);AddEdge(v,u);
	}
	for (int i=1;i<=m;i++)
	{
		int u,k;read(u),read(k);
		vec[u].push_back(make_pair(i,k));//存储id和k到对应的u节点上
	}
	dfs1(1,0);
	dfs2(1,0,1);
	for (int i=1;i<=m;i++) writewith(ans[i],'\n');puts("");
	return 0;
}

posted @ 2022-08-17 10:30  Hanx16Msgr  阅读(36)  评论(0)    收藏  举报