数据结构与算法:树上启发式合并

前言

原本以为这个也就在重链剖分里用一下,没想到这么厉害!

一、原理

具体原理详见重链剖分的复杂度分析()

当没有修改操作,且可以通过遍历子树建立的信息得到答案时,就可以考虑树上启发式合并。

首先,在处理树上启发式合并问题中,通常考虑设置全局的信息,然后在递归过程中实时修改。之后,考虑定义递归函数 dfs(u,keep) 为当前来到 u 节点,keep 表示是否保留子树 u 对信息的贡献。之后先遍历所有轻儿子的子树,结束时消除对信息的贡献,即 dfs(v,0)。之后再遍历重儿子的子树,结束时保留对信息的贡献,即 dfs(v,1)。

之后考察单个节点 u,对信息进行贡献,再遍历所有轻儿子的子树,重新对信息进行贡献。这里是因为在遍历每个轻儿子时,需要让各个儿子之间互不干扰。在最后遍历完重儿子后,把 u 的信息合并进去,再贡献轻儿子得到 u 的答案。

复杂度分析方面,前面的递归计算贡献没啥好说的,就是 O(n),重点是后面重新贡献和清除部分。由于只有当一个子树是轻儿子时,才会重新遍历消除这棵子树的贡献。那么此时必然就有一个重儿子,即大小大于等于当前子树的子树。也就是说,只有当存在一个比当前集合更大的集合时,才会重新遍历贡献一遍。所以每次重新贡献时,当前集合的大小都会至少翻一倍,所以对于每个节点,其重新贡献被遍历的次数就是 logn 的。取消操作类似。

太抽象了,还是去题目里看吧()

二、题目

1.树上数颜色

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<typename T>
struct Heavy_Light_Decomposition{
    
    int n;
    vector<int>fat;
    vector<int>dep;
    vector<int>siz;
    vector<int>son;
    vector<int>top;
    vector<int>dfn;
    vector<int>seg;
    int cnt;

    Heavy_Light_Decomposition(){}

    //_n: number of node, not size!!
    Heavy_Light_Decomposition(int _n,const vector<vector<int>>&g,int root=1){
        n=_n;
        fat.assign(n+1,0);
        dep.assign(n+1,0);
        siz.assign(n+1,0);
        son.assign(n+1,0);
        top.assign(n+1,0);
        dfn.assign(n+1,0);
        seg.assign(n+1,0);
        cnt=0;

        dfs1(root,0,g);
    }

    void dfs1(int u,int fa,const vector<vector<int>>&g){
        fat[u]=fa;
        dep[u]=dep[fa]+1;
        siz[u]=1;
        
        for(auto v:g[u]){
            if(v!=fa){
                dfs1(v,u,g);

                siz[u]+=siz[v];
                if(siz[son[u]]<siz[v]){
                    son[u]=v;
                }
            }
        }
    }
};

void solve()
{
    int n;
    cin>>n;
    vector<vector<int>>g(n+1);
    for(int i=1,u,v;i<=n-1;i++)
    {
        cin>>u>>v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    vector<int>a(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    
    Heavy_Light_Decomposition<int>hld(n,g);

    vector<int>cnts(n+1);
    int kind=0;
    vector<int>ans(n+1);

    auto add=[&](auto &&self,int u)->void
    {
        if(++cnts[a[u]]==1)
        {
            kind++;
        }

        for(auto v:g[u])
        {
            if(v!=hld.fat[u])
            {
                self(self,v);
            }
        }
    };

    auto erase=[&](auto &&self,int u)->void
    {
        if(--cnts[a[u]]==0)
        {
            kind--;
        }

        for(auto v:g[u])
        {
            if(v!=hld.fat[u])
            {
                self(self,v);
            }
        }
    };

    auto dfs=[&](auto &&self,int u,int keep)->void
    {
        for(auto v:g[u])
        {
            if(v!=hld.fat[u]&&v!=hld.son[u])
            {
                self(self,v,0);
            }
        }

        if(hld.son[u]!=0)
        {
            self(self,hld.son[u],1);
        }

        if(++cnts[a[u]]==1)
        {
            kind++;
        }

        for(auto v:g[u])
        {
            if(v!=hld.fat[u]&&v!=hld.son[u])
            {
                add(add,v);
            }
        }
        ans[u]=kind;
        if(keep==0)
        {
            erase(erase,u);
        }
    };
    dfs(dfs,1,0);

    int q;
    cin>>q;
    int x;
    while(q--)
    {
        cin>>x;

        cout<<ans[x]<<endl;
    }
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

在这个题里,考虑维护一个全局词频数组 cnts 和种类数 kind,这两个信息在来到每个节点时是可以 O(1) 更新好的。之后,当来到节点 u 时,先去所有轻儿子收集信息。由于每次遍历后都取消贡献,所以在遍历完后,此时的两个信息就都是空的状态。此时再去重儿子收集信息,回来的时候直接把当前节点 u 的贡献计算上,然后再遍历所有轻儿子重新计算贡献,记录此时 u 的答案。在返回时,若此时 u 是轻儿子,那么就再回去遍历一遍 u 整棵子树,把信息清空即可。

若将重新贡献和清空信息看作集合的合并,由于这个操作只会对轻儿子做,所以每做一次就相当于将当前集合合并到更大的集合中,所以合并的次数就是 logn 的。

2.颜色平衡树

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<typename T>
struct Heavy_Light_Decomposition{
    
    int n;
    vector<int>fat;
    vector<int>dep;
    vector<int>siz;
    vector<int>son;
    vector<int>top;
    vector<int>dfn;
    vector<int>seg;
    int cnt;

    Heavy_Light_Decomposition(){}

    //_n: number of node, not size!!
    Heavy_Light_Decomposition(int _n,const vector<vector<int>>&g,int root=1){
        n=_n;
        fat.assign(n+1,0);
        dep.assign(n+1,0);
        siz.assign(n+1,0);
        son.assign(n+1,0);
        top.assign(n+1,0);
        dfn.assign(n+1,0);
        seg.assign(n+1,0);
        cnt=0;

        dfs1(root,0,g);
    }

    void dfs1(int u,int fa,const vector<vector<int>>&g){
        fat[u]=fa;
        dep[u]=dep[fa]+1;
        siz[u]=1;
        
        for(auto v:g[u]){
            if(v!=fa){
                dfs1(v,u,g);

                siz[u]+=siz[v];
                if(siz[son[u]]<siz[v]){
                    son[u]=v;
                }
            }
        }
    }
};

void solve()
{
    int n;
    cin>>n;
    vector<vector<int>>g(n+1);
    vector<int>a(n+1);
    for(int i=1,fa;i<=n;i++)
    {
        cin>>a[i]>>fa;
        g[i].push_back(fa);
        g[fa].push_back(i);
    }
    
    Heavy_Light_Decomposition<int>hld(n,g);

    vector<int>cnts(n+1);
    vector<int>kinds(n+1);
    vector<int>ans(n+1);

    auto add=[&](auto &&self,int u)->void
    {
        kinds[cnts[a[u]]]--;
        cnts[a[u]]++;
        kinds[cnts[a[u]]]++;

        for(auto v:g[u])
        {
            if(v!=hld.fat[u])
            {
                self(self,v);
            }
        }
    };

    auto erase=[&](auto &&self,int u)->void
    {
        kinds[cnts[a[u]]]--;
        cnts[a[u]]--;
        kinds[cnts[a[u]]]++;

        for(auto v:g[u])
        {
            if(v!=hld.fat[u])
            {
                self(self,v);
            }
        }
    };

    auto dfs=[&](auto &&self,int u,int keep)->void
    {
        for(auto v:g[u])
        {
            if(v!=hld.fat[u]&&v!=hld.son[u])
            {
                self(self,v,0);
            }
        }

        if(hld.son[u]!=0)
        {
            self(self,hld.son[u],1);
        }

        kinds[cnts[a[u]]]--;
        cnts[a[u]]++;
        kinds[cnts[a[u]]]++;

        for(auto v:g[u])
        {
            if(v!=hld.fat[u]&&v!=hld.son[u])
            {
                add(add,v);
            }
        }
        ans[u]=(hld.siz[u]==kinds[cnts[a[u]]]*cnts[a[u]]);
        if(keep==0)
        {
            erase(erase,u);
        }
    };
    dfs(dfs,1,0);

    int sum=0;
    for(int i=1;i<=n;i++)
    {
        sum+=ans[i];
    }
    cout<<sum<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

考虑在上一个题的基础上,多维护一个 kinds 数组表示出现次数为 i 的颜色有几种,这个同样是可以在 O(1) 时间内完成增加和删除的。之后还是去整合答案,对于当前子树的根节点 u,先把 u 的颜色的词频取出 x,再去查词频为 x 的颜色种类 y。若 x 乘以 y 恰好等于当前子树的大小,就说明当前子树内出现的颜色的种类数都是相等的。

3.E. Lomsat gelral

怎么感觉这个信息的维护方式和莫队很像(?

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<typename T>
struct Heavy_Light_Decomposition{
    
    int n;
    vector<int>fat;
    vector<int>dep;
    vector<int>siz;
    vector<int>son;
    vector<int>top;
    vector<int>dfn;
    vector<int>seg;
    int cnt;

    Heavy_Light_Decomposition(){}

    //_n: number of node, not size!!
    Heavy_Light_Decomposition(int _n,const vector<vector<int>>&g,int root=1){
        n=_n;
        fat.assign(n+1,0);
        dep.assign(n+1,0);
        siz.assign(n+1,0);
        son.assign(n+1,0);
        top.assign(n+1,0);
        dfn.assign(n+1,0);
        seg.assign(n+1,0);
        cnt=0;

        dfs1(root,0,g);
    }

    void dfs1(int u,int fa,const vector<vector<int>>&g){
        fat[u]=fa;
        dep[u]=dep[fa]+1;
        siz[u]=1;
        
        for(auto v:g[u]){
            if(v!=fa){
                dfs1(v,u,g);

                siz[u]+=siz[v];
                if(siz[son[u]]<siz[v]){
                    son[u]=v;
                }
            }
        }
    }
};

void solve()
{
    int n;
    cin>>n;
    vector<int>a(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    vector<vector<int>>g(n+1);
    for(int i=1,u,v;i<=n-1;i++)
    {
        cin>>u>>v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    Heavy_Light_Decomposition<int>hld(n,g);

    vector<int>cnts(n+1);
    int maxcnt=0;
    ll sum=0;

    auto add=[&](auto &&self,int u)->void
    {
        cnts[a[u]]++;
        if(cnts[a[u]]==maxcnt)
        {
            sum+=a[u];
        }
        else if(cnts[a[u]]>maxcnt)
        {
            maxcnt=cnts[a[u]];
            sum=a[u];
        }

        for(auto v:g[u])
        {
            if(v!=hld.fat[u])
            {
                self(self,v);
            }
        }
    };

    auto erase=[&](auto &&self,int u)->void
    {
        cnts[a[u]]=0;
        maxcnt=0;
        for(auto v:g[u])
        {
            if(v!=hld.fat[u])
            {
                self(self,v);
            }
        }
    };

    vector<ll>ans(n+1);

    auto dfs=[&](auto &&self,int u,int keep)->void
    {
        for(auto v:g[u])
        {
            if(v!=hld.fat[u]&&v!=hld.son[u])
            {
                self(self,v,0);
            }
        }

        if(hld.son[u]!=0)
        {
            self(self,hld.son[u],1);
        }

        cnts[a[u]]++;
        if(cnts[a[u]]==maxcnt)
        {
            sum+=a[u];
        }
        else if(cnts[a[u]]>maxcnt)
        {
            maxcnt=cnts[a[u]];
            sum=a[u];
        }

        for(auto v:g[u])
        {
            if(v!=hld.fat[u]&&v!=hld.son[u])
            {
                add(add,v);
            }
        }

        ans[u]=sum;

        if(keep==0)
        {
            erase(erase,u);
        }
    };

    dfs(dfs,1,0);

    for(int i=1;i<=n;i++)
    {
        cout<<ans[i]<<" ";
    }
    cout<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

对于出现次数最多的颜色累加和,考虑在如何 O(1) 更新这个信息。那么就是在词频数组 cnts 的基础上,再维护一个当前出现的最大词频 maxcnt。那么在加入当前节点 u 的颜色时,若增加词频后小于最大词频,那么累加和信息肯定不变。若恰好等于最大词频,那么就可以直接往累加和里增加了。而若超过了最大词频,就直接重置两个信息即可。

重点是删除方法。由于 cnts 和 maxcnt 维护的是当前子树的信息,那么在遍历这个子树清除后,这两个信息必然会全被清成 0。那么若此时发现每次删除的信息不好维护,就可以直接将碰到节点的颜色的词频重设成 0,反正最后都会被清成 0。

4.E. Blood Cousins Return

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<typename T>
struct Heavy_Light_Decomposition{
    
    int n;
    vector<int>fat;
    vector<int>dep;
    vector<int>siz;
    vector<int>son;
    vector<int>top;
    vector<int>dfn;
    vector<int>seg;
    int cnt;

    Heavy_Light_Decomposition(){}

    //_n: number of node, not size!!
    Heavy_Light_Decomposition(int _n,const vector<vector<int>>&g,int root=1){
        n=_n;
        fat.assign(n+1,0);
        dep.assign(n+1,0);
        siz.assign(n+1,0);
        son.assign(n+1,0);
        top.assign(n+1,0);
        dfn.assign(n+1,0);
        seg.assign(n+1,0);
        cnt=0;

        dfs1(root,0,g);
    }

    void dfs1(int u,int fa,const vector<vector<int>>&g){
        fat[u]=fa;
        dep[u]=dep[fa]+1;
        siz[u]=1;
        
        for(auto v:g[u]){
            if(v!=fa){
                dfs1(v,u,g);

                siz[u]+=siz[v];
                if(siz[son[u]]<siz[v]){
                    son[u]=v;
                }
            }
        }
    }
};

void solve()
{
    int n;
    cin>>n;
    vector<string>s(n+1);
    vector<vector<int>>g(n+1);
    vector<int>head(n+1);
    for(int i=1,fa;i<=n;i++)
    {
        cin>>s[i]>>fa;
        if(fa==0)
        {
            head[i]=1;
        }
        else
        {
            g[i].push_back(fa);
            g[fa].push_back(i);
        }
    }

    map<string,int>mp;
    for(int i=1,cnt=0;i<=n;i++)
    {
        if(mp.find(s[i])==mp.end())
        {
            mp[s[i]]=++cnt;
        }
    }
    vector<int>a(n+1);
    for(int i=1;i<=n;i++)
    {
        a[i]=mp[s[i]];
    }

    vector<int>fat(n+1);
    vector<int>dep(n+1);
    vector<int>siz(n+1);
    vector<int>son(n+1);

    auto dfs1=[&](auto &&self,int u,int fa)->void
    {
        fat[u]=fa;
        dep[u]=dep[fa]+1;
        siz[u]=1;
        
        for(auto v:g[u]){
            if(v!=fa){
                self(self,v,u);

                siz[u]+=siz[v];
                if(siz[son[u]]<siz[v]){
                    son[u]=v;
                }
            }
        }
    };
    
    for(int i=1;i<=n;i++)
    {
        if(head[i])
        {
            dfs1(dfs1,i,0);
        }
    }

    int m;
    cin>>m;
    vector<vector<pii>>q(n+1);
    for(int i=1,u,k;i<=m;i++)
    {
        cin>>u>>k;
        q[u].push_back({k,i});
    }

    vector<set<int>>lev(n+1);

    auto add=[&](auto &&self,int u)->void
    {
        lev[dep[u]].insert(a[u]);
        for(auto v:g[u])
        {
            if(v!=fat[u])
            {
                self(self,v);
            }
        }
    };

    auto erase=[&](auto &&self,int u)->void
    {
        lev[dep[u]].erase(a[u]);
        for(auto v:g[u])
        {
            if(v!=fat[u])
            {
                self(self,v);
            }
        }
    };

    vector<int>ans(m+1);

    auto dfs2=[&](auto &&self,int u,int keep)->void
    {
        for(auto v:g[u])
        {
            if(v!=fat[u]&&v!=son[u])
            {
                self(self,v,0);
            }
        }

        if(son[u]!=0)
        {
            self(self,son[u],1);
        }

        lev[dep[u]].insert(a[u]);

        for(auto v:g[u])
        {
            if(v!=fat[u]&&v!=son[u])
            {
                add(add,v);
            }
        }

        for(auto [k,id]:q[u])
        {
            if(dep[u]+k<=n)
            {
                ans[id]=lev[dep[u]+k].size();
            }
        }

        if(keep==0)
        {
            erase(erase,u);
        }
    };

    for(int i=1;i<=n;i++)
    {
        if(head[i])
        {
            dfs2(dfs2,i,0);
        }
    }

    for(int i=1;i<=m;i++)
    {
        cout<<ans[i]<<endl;
    }
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

首先,考虑对每个字符串离散化转成数字,那么问题就转化为和前几个题一样了。

之后,由于对于每个节点,要查子树内距离不同高度的信息,所以考虑先把查询离线下来,然后对每个节点设置查询列表。那么在到达当前节点时,就可以根据信息直接回答这些查询了。

在信息的设置上,考虑维护每个深度有的颜色个数,这个可以通过对每个深度都开一个 set 实现去重。又因为在删除时可以直接清空,所以这个也是好操作的。

5.E. Blood Cousins

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

void solve()
{
    int n;
    cin>>n;
    vector<vector<int>>g(n+1);
    vector<int>head(n+1);
    for(int i=1,fa;i<=n;i++)
    {
        cin>>fa;
        if(fa==0)
        {
            head[i]=1;
        }
        else
        {
            g[fa].push_back(i);
            g[i].push_back(fa);
        }
    }

    auto log2=[&](int n)->int
    {
        int ans=0;
        while((1<<ans)<=(n>>1)){
            ans++;
        }
        return ans;
    };

    vector<int>fat(n+1);
    vector<int>dep(n+1);
    vector<int>siz(n+1);
    vector<int>son(n+1);

    int MAXP=log2(n);
    vector<vector<int>>stjump(n+1,vector<int>(MAXP+1));

    auto dfs1=[&](auto &&self,int u,int fa)->void
    {
        fat[u]=fa;
        dep[u]=dep[fa]+1;
        siz[u]=1;

        stjump[u][0]=fa;
        for(int p=1;(1<<p)<=dep[u];p++){
            stjump[u][p]=stjump[stjump[u][p-1]][p-1];
        }
        
        for(auto v:g[u]){
            if(v!=fa){
                self(self,v,u);

                siz[u]+=siz[v];
                if(siz[son[u]]<siz[v]){
                    son[u]=v;
                }
            }
        }
    };
    
    for(int i=1;i<=n;i++)
    {
        if(head[i])
        {
            dfs1(dfs1,i,0);
        }
    }

    auto query=[&](int x,int k)->int
    {
        int res=x;
        for(int p=MAXP;p>=0;p--)
        {
            if(k>>p&1)
            {
                res=stjump[res][p];
            }
        }
        return res;
    };

    int m;
    cin>>m;
    vector<vector<pii>>q(n+1);
    for(int i=1,x,k;i<=m;i++)
    {
        cin>>x>>k;
        
        int res=query(x,k);
        if(res)
        {
            q[res].push_back({k,i});
        }
    }

    vector<int>cnts(n+1);

    auto add=[&](auto &&self,int u)->void
    {
        cnts[dep[u]]++;
        for(auto v:g[u])
        {
            if(v!=fat[u])
            {
                self(self,v);
            }
        }
    };

    auto erase=[&](auto &&self,int u)->void
    {
        cnts[dep[u]]=0;
        for(auto v:g[u])
        {
            if(v!=fat[u])
            {
                self(self,v);
            }
        }
    };

    vector<int>ans(m+1);

    auto dfs2=[&](auto &&self,int u,int keep)->void
    {
        for(auto v:g[u])
        {
            if(v!=fat[u]&&v!=son[u])
            {
                self(self,v,0);
            }
        }

        if(son[u])
        {
            self(self,son[u],1);
        }

        cnts[dep[u]]++;

        for(auto v:g[u])
        {
            if(v!=fat[u]&&v!=son[u])
            {
                add(add,v);
            }
        }

        for(auto [k,id]:q[u])
        {
            if(dep[u]+k<=n)
            {
                ans[id]=cnts[dep[u]+k]-1;
            }
        }

        if(keep==0)
        {
            erase(erase,u);
        }
    };

    for(int i=1;i<=n;i++)
    {
        if(head[i])
        {
            dfs2(dfs2,i,0);
        }
    }

    for(int i=1;i<=m;i++)
    {
        cout<<ans[i]<<" ";
    }
    cout<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

乍一看上去可能会觉得过程很复杂,但其实转化一下就行。对于当前节点 x 的查询,利用 st 表往上跳 k 步来到其要查的祖先节点,然后将这条查询任务发给这个祖先节点即可。那么这个问题就转化为之前的几个问题了,直接套模板即可。注意要特判没有祖先的情况,不加入查询即可。

6.D. Arpa's letter-marked tree and Mehrdad's Dokhtar-kosh paths

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<typename T>
struct Heavy_Light_Decomposition{
    
    int n;
    vector<int>fat;
    vector<int>dep;
    vector<int>siz;
    vector<int>son;
    vector<int>top;
    vector<int>dfn;
    vector<int>seg;
    int cnt;

    Heavy_Light_Decomposition(){}

    //_n: number of node, not size!!
    Heavy_Light_Decomposition(int _n,const vector<vector<int>>&g,int root=1){
        n=_n;
        fat.assign(n+1,0);
        dep.assign(n+1,0);
        siz.assign(n+1,0);
        son.assign(n+1,0);
        top.assign(n+1,0);
        dfn.assign(n+1,0);
        seg.assign(n+1,0);
        cnt=0;

        dfs1(root,0,g);
        dfs2(root,root,g);
    }

    void dfs1(int u,int fa,const vector<vector<int>>&g){
        fat[u]=fa;
        dep[u]=dep[fa]+1;
        siz[u]=1;
        
        for(auto v:g[u]){
            if(v!=fa){
                dfs1(v,u,g);

                siz[u]+=siz[v];
                if(siz[son[u]]<siz[v]){
                    son[u]=v;
                }
            }
        }
    }

    void dfs2(int u,int up,const vector<vector<int>>&g){
        top[u]=up;
        dfn[u]=++cnt;
        seg[cnt]=u;
        
        if(son[u]==0){
            return ;
        }

        dfs2(son[u],up,g);

        for(auto v:g[u]){
            if(v!=fat[u]&&v!=son[u]){
                dfs2(v,v,g);
            }
        }
    }
};

void solve()
{
    int n;
    cin>>n;
    vector<vector<pii>>g(n+1);
    int fa;char ch;
    for(int i=2;i<=n;i++)
    {
        cin>>fa>>ch;
        g[i].push_back({fa,ch-'a'});
        g[fa].push_back({i,ch-'a'});
    }

    vector<int>fat(n+1);
    vector<int>dep(n+1);
    vector<int>siz(n+1);
    vector<int>son(n+1);
    vector<int>eor(n+1);

    auto dfs1=[&](auto &&self,int u,int fa)->void
    {
        fat[u]=fa;
        dep[u]=dep[fa]+1;
        siz[u]=1;
        
        for(auto [v,w]:g[u]){
            if(v!=fa){

                eor[v]=eor[u]^(1<<w);
                self(self,v,u);

                siz[u]+=siz[v];
                if(siz[son[u]]<siz[v]){
                    son[u]=v;
                }
            }
        }
    };
    dfs1(dfs1,1,0);

    vector<int>maxdep(1<<22);
    
    auto add=[&](auto &&self,int u)->void
    {
        maxdep[eor[u]]=max(maxdep[eor[u]],dep[u]);

        for(auto [v,w]:g[u])
        {
            if(v!=fat[u])
            {
                self(self,v);
            }
        }
    };

    auto erase=[&](auto &&self,int u)->void
    {
        maxdep[eor[u]]=0;

        for(auto [v,w]:g[u])
        {
            if(v!=fat[u])
            {
                self(self,v);
            }
        }
    };

    vector<int>ans(n+1);

    auto calc=[&](auto &&self,int u,int lca)->void
    {
        if(maxdep[eor[u]])
        {
            ans[lca]=max(ans[lca],maxdep[eor[u]]+dep[u]-2*dep[lca]);
        }
        for(int p=0;p<22;p++)
        {
            int s=eor[u]^(1<<p);
            if(maxdep[s])
            {
                ans[lca]=max(ans[lca],maxdep[s]+dep[u]-2*dep[lca]);
            }
        }

        for(auto [v,w]:g[u])
        {
            if(v!=fat[u])
            {
                self(self,v,lca);
            }
        }
    };

    auto dfs2=[&](auto &&self,int u,int keep)->void
    {
        for(auto [v,w]:g[u])
        {
            if(v!=fat[u]&&v!=son[u])
            {
                self(self,v,0);
                ans[u]=max(ans[u],ans[v]);
            }
        }
        if(son[u])
        {
            self(self,son[u],1);
            ans[u]=max(ans[u],ans[son[u]]);
        }

        if(maxdep[eor[u]])
        {
            ans[u]=max(ans[u],maxdep[eor[u]]-dep[u]);
        }
        for(int p=0;p<22;p++)
        {
            int s=eor[u]^(1<<p);
            if(maxdep[s])
            {
                ans[u]=max(ans[u],maxdep[s]-dep[u]);
            }
        }

        maxdep[eor[u]]=max(maxdep[eor[u]],dep[u]);

        for(auto [v,w]:g[u])
        {
            if(v!=fat[u]&&v!=son[u])
            {
                calc(calc,v,u);
                add(add,v);
            }
        }
        if(keep==0)
        {
            erase(erase,u);
        }
    };
    dfs2(dfs2,1,0);

    for(int i=1;i<=n;i++)
    {
        cout<<ans[i]<<" ";
    }
    cout<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

首先,对于一条合法的回文路径,要么其中字母的词频全是偶数,要么就只出现一个词频为奇数的字母。而对于奇偶词频,考虑将其转化成异或问题,即对题目里这 22 个字母的词频状态压缩。之后,对于路径问题,还是考虑转化成前缀问题。所以在构建出从根节点到每个节点 u 这条路径上字母的词频异或后,此时两点 x 和 y 这条路径上的词频异或就是两点状态异或起来的结果。

在来到当前节点 u 时,首先答案可以由所有孩子子树的答案构成,之后就需要考虑跨子树的答案了。那么首先,讨论从 u 出发,和自己重儿子的子树构成的答案。那么若从根节点来到 u 的状态为 s,其就可以和重儿子子树内,状态为 s 以及和状态为 s 只有一位不同的状态异或,得到合法的状态。又因为要让路径最长,所以考虑定义 为状态为 s 的点最深出现在第几层。

那么在将 u 合并到重儿子后,此时去所有轻儿子的子树内考虑。那么对于当前到的节点 v,还是去之前合并过的集合内找状态。然后和最深出现的位置合并,通过深度计算路径长度,然后将当前子树合并到大集合里即可。

7.森林

最能体现 dsu on tree 的一道题!

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
 
/*   /\_/\
*   (= ._.)
*   / >  \>
*/
 
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
 
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
 
template<typename T>
struct Persistent_SegTree{
    vector<int>root;
    vector<int>left;
    vector<int>right;
    vector<T>data;
    int cnt;
 
    //自定义
 
    Persistent_SegTree(){}
 
    Persistent_SegTree(int n){
        root.assign(n,0);
        left.assign(n*300,0);
        right.assign(n*300,0);
        data.assign(n*300,0);
        cnt=0;
 
        //自定义
    }
 
    //自定义
    int build(int l,int r){
        int cur=++cnt;
        data[cur]=0;
 
        if(l==r){
        }
        else{
            int m=l+r>>1;
            left[cur]=build(l,m);
            right[cur]=build(m+1,r);
        }
        return cur;
    }
 
    //单点修改
    int change(int jobi,T jobv,int l,int r,int i){
        int cur=copy(i);
        data[cur]+=jobv;
 
        if(l==r){
        }
        else{
            int m=l+r>>1;
            if(jobi<=m){
                left[cur]=change(jobi,jobv,l,m,left[cur]);
            }
            else{
                right[cur]=change(jobi,jobv,m+1,r,right[cur]);
            }
        }
        return cur;
    }
 
    //区间 [u,v] 查第 k 小
    int kth(int jobk,int l,int r,int u,int v,int lca,int fa){
        if(l==r){
            return l;
        }
        
        int cnt=data[left[u]]-data[left[lca]]+data[left[v]]-data[left[fa]];
        int m=l+r>>1;
        if(cnt>=jobk){
            return kth(jobk,l,m,left[u],left[v],left[lca],left[fa]);
        }
        else{
            return kth(jobk-cnt,m+1,r,right[u],right[v],right[lca],right[fa]);
        }
    }
 
    //自定义
    int copy(int i){
        int cur=++cnt;
        left[cur]=left[i];
        right[cur]=right[i];
        data[cur]=data[i];
 
        //自定义
 
        return cur;
    }
};
 
void solve()
{
    int n,m,q;
    cin>>n>>m>>q;
    vector<int>a(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    vector<vector<int>>g(n+1);
    for(int i=1,u,v;i<=m;i++)
    {
        cin>>u>>v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
 
    vector<int>sorted=a;
    sort(sorted.begin()+1,sorted.end());
    int len=1;
    for(int i=2;i<=n;i++)
    {
        if(sorted[len]!=sorted[i])
        {
            sorted[++len]=sorted[i];
        }
    }
    auto rnk=[&](int v)->int
    {
        int l=1;
        int r=len;
        int m;
        int ans;
        while(l<=r)
        {
            m=l+r>>1;
            if(sorted[m]>=v)
            {
                ans=m;
                r=m-1;
            }
            else
            {
                l=m+1;
            }
        }
        return ans;
    };
    for(int i=1;i<=n;i++)
    {
        a[i]=rnk(a[i]);
    }

    const int MAXP=20;
    vector<int>deep(n+1);
    vector<vector<int>>stjump(n+1,vector<int>(MAXP+1));
    Persistent_SegTree<ll>st(n+1);
    vector<int>head(n+1);
    vector<int>size(n+1);

    auto dfs=[&](auto &&self,int u,int fa,int top)->void
    {
        deep[u]=deep[fa]+1;
        stjump[u][0]=fa;
        st.root[u]=st.change(a[u],1,1,len,st.root[fa]);
        head[u]=top;
        size[top]++;

        for(int p=1;p<=MAXP;p++){
            stjump[u][p]=stjump[stjump[u][p-1]][p-1];
        }

        for(auto v:g[u]){
            if(v!=fa){
                self(self,v,u,top);
            }
        }
    };
 
    auto query=[&](int a,int b)->int
    {
        if(deep[a]<deep[b]){
            swap(a,b);
        }

        for(int p=MAXP;p>=0;p--){
            if(deep[stjump[a][p]]>=deep[b]){
                a=stjump[a][p];
            }
        }

        if(a==b){
            return a;
        }

        for(int p=MAXP;p>=0;p--){
            if(stjump[a][p]!=stjump[b][p]){
                a=stjump[a][p];
                b=stjump[b][p];
            }
        }

        return stjump[a][0];
    };

    for(int i=1;i<=n;i++)
    {
        if(head[i]==0)
        {
            dfs(dfs,i,0,i);
        }
    }

    int pre=0;
    string op;
    int u,v,k;
    while(q--)
    {
        cin>>op;
        if(op[0]=='L')
        {
            cin>>u>>v;
            u^=pre,v^=pre;

            g[u].push_back(v);
            g[v].push_back(u);

            int fu=head[u];
            int fv=head[v];

            if(size[fu]>=size[fv])
            {
                dfs(dfs,v,u,fu);
            }
            else
            {
                dfs(dfs,u,v,fv);
            }
        }
        else if(op[0]=='Q')
        {
            cin>>u>>v>>k;
            u^=pre,v^=pre,k^=pre;
    
            int lca=query(u,v);
            int fa=stjump[lca][0];
            
            int rnk=st.kth(k,1,len,st.root[u],st.root[v],st.root[lca],st.root[fa]);
            pre=sorted[rnk];
            cout<<pre<<endl;
        }
    }
}
 
void init()
{
}
 
signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    cin>>t;
    init();
    // while(t--)
    // {
        solve();    
    // }
    return 0;
}

对于这个路径第 k 大的问题,如果不涉及连边的操作,那么其实就是主席树的板子题。而不管怎么连接,都不会影响两点之间的路径。那么根据启发式合并的原理,在两树合并时,考虑让小树挂到大树上,然后暴力修改小树所有节点的版本即可。那么在合并时,就需要每次查询当前节点所在树的头节点以及整棵树的大小,这个就可以通过一个类似并查集的结构实现。

注意 testcase 是编号不是数据组数,这个太他妈坑了......

总结

感觉这玩意儿维护信息的方式和莫队很像啊!!前几题完全可以把树用 dfn 序拍成数组跑莫队!!

END

相关推荐
学无止境_永不停歇1 小时前
从零手写高性能C++ TCP 服务器框架(十一) --- Connection实现
linux·服务器·网络·c++
努力的章鱼bro1 小时前
CUDA编程入门
c++·人工智能·cuda
不是光头 强1 小时前
feign-list-param-crash-cpp
java·数据结构·list
x_xbx1 小时前
LeetCode:20. 有效的括号
算法·leetcode·职场和发展
计算机安禾1 小时前
【算法设计与分析】第40篇:空间数据结构:KD树与四叉树的查询分析
数据结构·算法
努力努力再努力wz2 小时前
【C++高阶数据结构系列】:跳表 SkipList 详解:多层索引、随机晋升与C++ 完整实现(附跳表实现的源码)
开发语言·数据结构·数据库·c++·redis·缓存·skiplist
江屿风2 小时前
C++图的两种构建算法流食般投喂-竞赛编
开发语言·c++·笔记·算法·图论
m沐沐2 小时前
【机器学习】信用卡欺诈检测实战:逻辑回归 + 过采样
人工智能·算法·机器学习·pycharm·逻辑回归
充值内卷2 小时前
TauriCPP 基于 WebView2 的轻量级 C++ 桌面应用框架
开发语言·c++