题意:
给定一棵树,给一些询问询问一个节点 (x) 子树里,与 (x) 距离为 (k)((k) 不同)节点的值有多少种。
(n) 个点,(m) 次查询。
数据范围:(1 ≤ n ≤ 10^5,1 ≤ m ≤ 10^5)
分析:
树上启发式合并。
一个离线算法,总复杂度一般是 (O(nlogn)) 的,要么就乘上一个统计的复杂度。
此题中用一个 (multiset) 维护每个深度的所有节点值,并用数组维护每个深度不同的值的个数。
一开始总是超时,后来发现,因为题目中是以 (0) 为根,而实际上不用考虑这个点,而我在启发式合并的时候去算了这个点,导致超时。
复杂度:(O(nlog^2n))
代码:
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
typedef pair<int,int>P;
const int N=1e5+10;
int ans[N],son[N],sz[N],name[N],depth[N],num[N];
int nt;
multiset<int>st[N];
vector<P>nd[N];
map<string,int>mp;
struct node
{
int to,nxt;
}edge[N<<1];
int head[N];
int cnt;
void init()
{
memset(head,-1,sizeof(head));
cnt=1;
}
void addedge(int from,int to)
{
edge[cnt].to=to;
edge[cnt].nxt=head[from];
head[from]=cnt++;
}
void read(int &x)
{
x=0;
int f=1;
char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-')
f=-1;
ch=getchar();
}
while(isdigit(ch))
{
x=(x<<3)+(x<<1)+ch-'0';
ch=getchar();
}
x*=f;
}
void dfs1(int v,int d)
{
sz[v]=1;
son[v]=-1;
depth[v]=d;
for(int i=head[v];i!=-1;i=edge[i].nxt)
{
int u=edge[i].to;
dfs1(u,d+1);
sz[v]+=sz[u];
if(son[v]==-1||sz[u]>sz[son[v]])
son[v]=u;
}
}
void add(int v)
{
if(st[depth[v]].count(name[v])==0)
num[depth[v]]++;
st[depth[v]].insert(name[v]);
for(int i=head[v];i!=-1;i=edge[i].nxt)
{
int u=edge[i].to;
add(u);
}
}
void del(int v)
{
st[depth[v]].erase(st[depth[v]].find(name[v]));
if(st[depth[v]].count(name[v])==0)
num[depth[v]]--;
for(int i=head[v];i!=-1;i=edge[i].nxt)
{
int u=edge[i].to;
del(u);
}
}
void solve(int v,bool f)
{
for(int i=head[v];i!=-1;i=edge[i].nxt)
{
int u=edge[i].to;
if(u==son[v])
continue;
solve(u,false);
}
if(son[v]>-1)
solve(son[v],true);
if(v==0)
return;
for(int i=head[v];i!=-1;i=edge[i].nxt)
{
int u=edge[i].to;
if(u==son[v])
continue;
add(u);
}
if(st[depth[v]].count(name[v])==0)
num[depth[v]]++;
st[depth[v]].insert(name[v]);
for(int i=0;i<nd[v].size();i++)
{
P t=nd[v][i];
if(t.first+depth[v]<N)
ans[t.second]=num[t.first+depth[v]];
}
if(!f)
del(v);
}
int main()
{
int n,m,r;
scanf("%d",&n);
init();
for(int i=1;i<=n;i++)
{
char s[25]={};
scanf("%s",s);
read(r);
if(mp.count(s)==0)
{
mp[s]=++nt;
name[i]=nt;
}
else
name[i]=mp[s];
addedge(r,i);
}
int v,k;
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
read(v),read(k);
nd[v].pb(make_pair(k,i));
}
dfs1(0,0);
solve(0,false);
for(int i=1;i<=m;i++)
printf("%d
",ans[i]);
return 0;
}
下面是网上看到的其它的做法:
1.(dfs序+二分)
利用 (dfs) 序的特点,把点按深度归类,同一棵子树中的点一定排列在一起。利用二分即可,注意序号。如果不存答案的话,会超时。
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
typedef pair<int,int>P;
const int N=1e5+5;
vector<int>dn[N],pic[N];
int sz[N],dfn[N],name[N],depth[N],rnk[N];
map<P,int>ms;
map<string,int>mp;
void dfs(int v,int d,int &cnt)
{
sz[v]=1;
rnk[cnt]=v;
dfn[v]=cnt++;
dn[d].pb(dfn[v]);
depth[v]=d;
for(int i=0;i<pic[v].size();i++)
{
int u=pic[v][i];
dfs(u,d+1,cnt);
sz[v]+=sz[u];
}
}
int solve(int v,int k,int n)
{
if(ms[make_pair(v,k)]>0)
return ms[make_pair(v,k)];
if(depth[v]+k>n||dn[depth[v]+k].empty())
return ms[make_pair(v,k)]=0;
set<int>st;
int a=dfn[v],b=dfn[v]+sz[v]-1;
int l=lower_bound(dn[depth[v]+k].begin(),dn[depth[v]+k].end(),a)-dn[depth[v]+k].begin();
int r=upper_bound(dn[depth[v]+k].begin(),dn[depth[v]+k].end(),b)-dn[depth[v]+k].begin()-1;
for(int i=l;i<=r;i++)
st.insert(name[rnk[dn[depth[v]+k][i]]]);
return ms[make_pair(v,k)]=st.size();
}
int main()
{
int n,m,r,cot=0,cnt=0;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
char s[25]={};
scanf("%s",s);
scanf("%d",&r);
if(mp.count(s)==0)
{
mp[s]=++cot;
name[i]=mp[s];
}
else
name[i]=mp[s];
pic[r].pb(i);
}
dfs(0,0,cnt);
int x,y;
scanf("%d",&m);
while(m--)
{
scanf("%d%d",&x,&y);
printf("%d
",solve(x,y,n));
}
return 0;
}
2.主席树+二分+倍增
传送门
利用树的程序遍历,把相同深度的点放在一起,同时按深度把点分别存储,即转化为主席树求区间内不同数的个数问题,关键在于求区间左右端点。
利用二分,对要求深度的点就行二分,判断条件为其父亲节点是否为 (v),用倍增查找。