CF375D Tree and Queries / Dsu on tree 模板
Tree and Queries
题面翻译
- 给定一棵 \(n\) 个节点的树,根节点为 \(1\)。每个节点上有一个颜色 \(c_i\)。\(m\) 次操作。操作有一种:
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;
}