Blood Cousins Return

题意:

给定一棵树,给一些询问询问一个节点 (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),用倍增查找。

原文地址:https://www.cnblogs.com/1024-xzx/p/12773278.html