题目出处:Codeforces-375D
【 题面 】:
有一个大小为n且以1为根的树,树上每个点都有对应的颜色ci。现给出m次询问v, k,问以v为根的子树中有多少种颜色至少出现了k次。
【输入格式】
第一行两个数n,m表示树的大小以及询问的次数。
第二行n个数表示树上每个结点的颜色。
接下来的n-1行,每行两个数a, b表示树上的边。
接下来m行,每行两个数v, k表示询问。
【输出格式】
m行,每行一个数表示第i次询问的答案。
样例输入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
4 1
1 2 3 4
1 2
2 3
3 4
1 1
样例输出2
4
数据范围
2≤n≤100000
1≤m≤100000
1≤ci≤100000
1≤a, b≤n, a≠b
1≤v≤n, 1≤k≤100000
对于其中30%的数据保证n,m≤100且ci≤n
对于其中60%的数据保证n≤5000
【考试历程】:立马想到暴力算法,设f[i][j]表示以i为根结点的子树中颜色为j的节点数量,然后树形DP由儿子传给爸爸就可以得到每一个点子树颜色信息,最后对于每一个询问暴力查询即可.
时间复杂度O(n2m*n)=O(n3m),数组只能开到5000 * 5000,然后就只能得到60分。
随后想到子树查询便想到了dfs序转换为区间问题,然后我想到了莫队貌似可做,然后就敲了一下。
emmmmm,ZZ的我不懂得变通,原来的莫队题目k是定制,这次是变了的,然后我还在用老方法,写到一半感觉又写不下去了,然后直接重敲,瞎搞一通,只拿了30分…
【正解】:相信自己,正解就是莫队(或者分块,至于什么用启发式合并做的大佬emmm),其实对于每一个询问k,我们只需维护一个树状数组,下标表示颜色数量,值代表有多少颜色达到了k个,动态去维护一下即可。
code
#include<bits/stdc++.h>
using namespace std;
struct fuk
{
int x,next;
}a[200011];
struct fuc
{
int id,l,r,k;
}q[100011];
int top=0,cnt=0,first[100011],n,m,blo;
int dfn[100011],pos[100011],size[100011],to[100011];
int c[100011],b[100011],v[100011],t[100011],s,ans[100011];
void add(int x,int to)
{
top++;
a[top].x=to;
a[top].next=first[x];
first[x]=top;
}
void dfs(int x,int fa)
{
dfn[x]=++cnt;
pos[cnt]=x;
size[x]=1;
for(int i=first[x];i;i=a[i].next)
{
int y=a[i].x;
if(y==fa) continue;
dfs(y,x);
size[x]+=size[y];
}
}
bool mycmp(fuc a,fuc b)
{
if(a.l/blo!=b.l/blo)
return a.l<b.l;
else if((a.l/blo)&1)
return a.r<b.r;
else return a.r>b.r;
}
void change(int x,int val)
{
for(;x<=100001;x+=x&-x)
c[x]+=val;
}
int ask(int x)
{
int ans=0;
for(;x;x-=x&-x)
ans+=c[x];
return ans;
}
void insert(int x)
{
if(x==0) return;
to[b[x]]++;
change(to[b[x]],-1); //由于树状数组不能有下标为0,我们整体都+1
change(to[b[x]]+1,1);
}
void remove(int x)
{
if(x==0) return;
to[b[x]]--;
change(to[b[x]]+2,-1);
change(to[b[x]]+1,1);
}
int main()
{
freopen("count.in","r",stdin);
freopen("count.out","w",stdout);
scanf("%d%d",&n,&m);
blo=sqrt(n);
for(int i=1;i<=n;i++)
scanf("%d",&v[i]),t[i]=v[i];
sort(t+1,t+n+1);
s=unique(t+1,t+n+1)-(t+1);
for(int i=1;i<=n;i++)
v[i]=lower_bound(t+1,t+s+1,v[i])-t;
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y); add(y,x);
}
dfs(1,0);
for(int i=1;i<=m;i++)
{
int l,r,k;
scanf("%d%d",&l,&k);
q[i].id=i; q[i].l=dfn[l]; q[i].k=k; q[i].r=dfn[l]+size[l]-1;
}
sort(q+1,q+m+1,mycmp);
int l=0,r=0;
for(int i=1;i<=n;i++)
b[i]=v[pos[i]];
for(int i=1;i<=m;i++)
{
int ql=q[i].l,qr=q[i].r,k=q[i].k;
while(l>ql) insert(--l);
while(r<qr) insert(++r);
while(l<ql) remove(l++);
while(r>qr) remove(r--);
ans[q[i].id]=ask(100001)-ask(k);
}
for(int i=1;i<=m;i++)
printf("%d\n",ans[i]);
}
这里提醒一下大家如果要用树状数组,一定要注意下标和0。然后其实如果你用以上的代码将莫队while操作的第一项和第二项互换,你会发现TLE了。
为什么?假设我们将询问排序后第一个问题区间为[3,3],你的l为1,然后先执行remove操作导致原本就为0的to[b[i]]又-1,其实本来我们应该先把这一个节点加入考虑区间中先+1即r的增加导致目前区间变为[1,3]后再由l的增加来减除[1,2]区间带来的影响;不然直接这样下标就又变成-1,树状数组直接GG了,一定要注意
呵呵我不会说有更快的方法的【谁叫我一开始想到了树状数组】
code
#include<bits/stdc++.h>
using namespace std;
struct fuk
{
int x,next;
}a[200011];
struct fuc
{
int id,l,r,k;
}q[100011];
int top=0,cnt=0,first[100011],n,m,blo;
int dfn[100011],pos[100011],size[100011],to[100011];
int c[100011],b[100011],v[100011],t[100011],s,ans[100011],sum[100011];
void add(int x,int to)
{
top++;
a[top].x=to;
a[top].next=first[x];
first[x]=top;
}
void dfs(int x,int fa)
{
dfn[x]=++cnt;
pos[cnt]=x;
size[x]=1;
for(int i=first[x];i;i=a[i].next)
{
int y=a[i].x;
if(y==fa) continue;
dfs(y,x);
size[x]+=size[y];
}
}
bool mycmp(fuc a,fuc b)
{
if(a.l/blo!=b.l/blo)
return a.l<b.l;
else if((a.l/blo)&1)
return a.r<b.r;
else return a.r>b.r;
}
void insert(int x)
{
if(x==0) return;
to[b[x]]++;
sum[to[b[x]]]++;
}
void remove(int x)
{
if(x==0) return;
to[b[x]]--;
sum[to[b[x]]+1]--;
}
int main()
{
freopen("count.in","r",stdin);
freopen("count.out","w",stdout);
scanf("%d%d",&n,&m);
blo=sqrt(n);
for(int i=1;i<=n;i++)
scanf("%d",&v[i]),t[i]=v[i];
sort(t+1,t+n+1);
s=unique(t+1,t+n+1)-(t+1);
for(int i=1;i<=n;i++)
v[i]=lower_bound(t+1,t+s+1,v[i])-t;
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y); add(y,x);
}
dfs(1,0);
for(int i=1;i<=m;i++)
{
int l,r,k;
scanf("%d%d",&l,&k);
q[i].id=i; q[i].l=dfn[l]; q[i].k=k; q[i].r=dfn[l]+size[l]-1;
}
sort(q+1,q+m+1,mycmp);
int l=0,r=0;
for(int i=1;i<=n;i++)
b[i]=v[pos[i]];
for(int i=1;i<=m;i++)
{
int ql=q[i].l,qr=q[i].r,k=q[i].k;
while(l>ql) insert(--l);
while(r<qr) insert(++r);
while(l<ql) remove(l++);
while(r>qr) remove(r--);
ans[q[i].id]=sum[k];
}
for(int i=1;i<=m;i++)
printf("%d\n",ans[i]);
}
sum数组下标和上面的树状数组含义相同,表示颜色数为k的颜色种类。
这里的sum数组运用的就很巧妙,因为一个颜色的数量假设由k1增加到k2,那么很明显数量为k1—k2的所有颜色数量都会+1,而一个颜色的减少只会让原先的颜色数表示的sum[]-1,那么对于每一个询问区间我们考虑完了所有的颜色,那么sum[k]就是我们的答案【因为一个颜色在该区间的数量>=k,必定都会使sum[k]这个数组+1】,这就是巧妙之处。
总结:要提升思想的深度,不局限于做过的题目,懂得举一反三是很重要的,要加深对算法的理解,灵活运用,要多刷题多思考。