树上的颜色
【问题描述】
给出一棵有N个点的有根树,结点编号为1,2,3,……,N,根结点编号为1,编号为i的结点图上颜色Ci。现在有M个询问,每个询问要求求出以结点u为根的子树上涂有此种颜色的结点个数不小于k的颜色个数有多少。
【输入格式】
输入文件名为color.in。
第一行包含两个正整数N和M。
第二行包含N个正整数,C1,C2,…,CN。
接下来的N-1行每行有两个正整数x和y,表示结点x和y有边相连。
再接下来的M行每行有两个正整数u和k,表示一个询问。
【输出格式】
输出文件名为color.out。
输出M行,每行一个非负整数,对应每个询问的答案。
【输入输出样例1】
color.in
4 1
1 2 3 4
1 2
2 3
3 4
1 1
color.out
4
【输入输出样例2】
color.in
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
color.out
2
2
1
0
1
【数据规模与约定】
对于10%的数据,N≤100,M≤100,Ci≤100;
对于30%的数据,N≤500,M≤100;
对于60%的数据,N≤2000,M≤100000;
对于100%的数据,N≤100000,M≤100000,Ci≤100000。
【题解】
10分做法:
直接暴力,O(N*N*M)
30分做法:
对颜色离散化后直接暴力。
60分做法:
离线处理询问,将结点相同的询问放在一起处理。对一个结点,暴力处理子树上所有点,用color[i]统计第i钟颜色的数量,用sum[i]统计颜色数量为i,i+1,i+2,……的颜色个数。设要加入一个点,其颜色为x,则sum[color[x]+1]增加1,sum的其他值不变,同时color[x]++即可。同理,删去一个点,sum[color[x]–]–即可。因此处理一棵子树只需O(n)的时间统计出color和sum数组,并且做完后清空color和sum数组也是O(n)的,而sum数组就是询问的答案。时间复杂度O(N²)
100分做法:
在60分做法的基础上,考虑处理点u的时候,假设其孩子结点为v1,v2,…,vk,按顺序先处理其孩子结点,每做完一个需要清空,发现vk做完后不需要清空,因此不妨将孩子结点的子树结点个数最多的放在最后处理,这样可以证明时间复杂度变为O(NlogN)。(假设时间为2NlogN,然后用数学归纳法)
这已经是我第n次树上题爆0了,我还是应该总结一下经验教训,克服对树上题的恐惧感,加强对该类题目的训练,先试着打对暴力,再逐渐像正解靠拢。同时记住,树上题有两个很套路的部分:链式前向星的存储和从父亲节点搜儿子的DFS。
存储:
struct edge
{
int to,next;
}a[MAX<<1];
void Add(int u,int v)
{
a[++cnt]=(edge){v,head[u]};
head[u]=cnt;
}
DFS:
void dfs(ll u,ll fa)
{
for(ll i=head[u];i;i=a[i].next)
{
ll v=a[i].to;
if(v==fa) continue;
dfs(v,u);
//f[u]=max(f[u],f[v]+a[i].w);
}
//for(ll i=head[u];i;i=a[i].next)
// if(fa!=a[i].to)
// ans+=(f[u]-f[a[i].to]-a[i].w);
}
据学长的说法,这道题有两种做法:平衡树(Splay)和树上莫队。我自己只学会了莫队。
莫队:按我的理解,莫队就是一种分块(优雅的暴力)。他把一个区间分作sqrt(n)+1个块(当n为平方数时是sqrt(n))。再把分成的块以,左端点所在块的标号为第一优先级,右端点为第二优先级排序。然后询问的答案都由上一个询问更新而来。
树上莫队:讲究的是把子树变为链,在先序遍历前提下,左端点为子树根节点,右端点为搜到的最后一个儿子。把他们存下来,再按普通莫队搞即可。
#include<iostream>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#define fp(i,a,b) for(int i=a;i<=b;i++)
#define fq(i,a,b) for(int i=a;i>=b;i--)
#define il inline
#define ll long long
using namespace std;
const int MAX=100005;
struct edge
{
int to,next;
}a[MAX<<1];
int head[MAX],cnt;
struct query
{
int l,r,k,id;
}q[MAX];
int line[MAX],Begin[MAX],End[MAX],tot;//line是最终处理出来的数列 Begin End表示某一个节点对应的某个区间
int n,m,gen,re[MAX];
int col[MAX],num[MAX],sum[MAX];
int ans[MAX];
void Add(int u,int v)
{
a[++cnt]=(edge){v,head[u]};
head[u]=cnt;
}
void dfs(int u,int fa)
{
Begin[u]=++tot;
line[tot]=col[u];
for (int e=head[u];e;e=a[e].next)
{
int v=a[e].to;
if (v==fa) continue;
dfs(v,u);
}
End[u]=tot;
}
bool cmp(query a,query b)
{
return (re[a.l]<re[b.l])||(re[a.l]==re[b.l]&&a.r<b.r);
}
il int gi()
{
int x=0;
short int t=1;
char ch=getchar();
while((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
if(ch=='-') t=-1,ch=getchar();
while(ch>='0'&&ch<='9') x=x*10+ch-48,ch=getchar();
return x*t;
}
int main()
{
n=gi();m=gi();gen=sqrt(n);
fp(i,1,n)
col[i]=gi();
fp(i,1,n)
re[i]=(i-1)/gen+1;
fp(i,1,n-1)
{
int x=gi(),y=gi();
Add(x,y);Add(y,x);
}
dfs(1,0);
fp(i,1,m)
{
int u=gi();
q[i]=(query){Begin[u],End[u],gi(),i};
}
sort(q+1,q+1+m,cmp);
int L=1,R=0;
fp(i,1,m)//60分做法的暴力搞
{
while(q[i].l<L) L--,num[line[L]]++,sum[num[line[L]]]++;
while(q[i].l>L) sum[num[line[L]]]--,num[line[L]]--,L++;
while(q[i].r>R) R++,num[line[R]]++,sum[num[line[R]]]++;
while(q[i].r<R) sum[num[line[R]]]--,num[line[R]]--,R--;
ans[q[i].id]=sum[q[i].k];
}
fp(i,1,m)
printf("%d\n",ans[i]);
return 0;
}