树链剖分|树上启发式合并

树链剖分

分为重链剖分和长链剖分以及其他奇怪的剖分。以重剖为主。

重链剖分

将树上问题重链剖分为序列问题(经常是 DFS 序)然后用数据结构(经常是线段树)维护。

剖分部分

定义:

  • 重儿子:对于一个点,其儿子中,子树最大的那个;
  • 重边:父亲到重儿子的连边;
  • 轻儿子:除了重儿子以外的儿子;
  • 轻边:父亲到轻儿子的连边。

证明一个东西:一条链上(不是路径,虽然差不多,路径就是两条链拼起来)至多有 \(\log n\) 条轻边。

很显然,每次若是走轻边,子树大小都至少会减少一半,所以至多走 \(\log n\) 次就到底了。

于是我们即使暴力维护轻边的复杂度都是 \(O(F(n)\log n)\) 的,\(F(n)\) 是数据结构单点操作复杂度。

而由于重边在儿子中的唯一性,所以所有的重边形成了若干条,不妨称之重链

一条链上包含多条重链。由于轻重边的交替排布,重链的数量级也是 \(\log n\) 的。考虑到大部分数据结构维护的是下标连续的区间,于是这里有一个小 trick,DFS 求 dfn 时,优先走重边,这样重链上的下标就连续了,所以我们只要做一次数据结构的区间操作即可维护重边上的信息,记 \(top(u)\) 为包含 \(u\) 的重链的链头,则每次跳 \(u\to fa(top(u))\) 即可一次跳过一条重链加一条轻边,时间复杂度 \(O(F'(n)\log n)\)\(F'(n)\) 为数据结构区间操作复杂度,对于线段树这种的,\(F'(n)\doteq F(n)\)

综上对于一条链的操作,复杂度就为 \(O(F(n)\log n)\)

例题:

给你一棵带边权树,支持下列操作:

  • 查询子树和
  • 查询 \(u,v\) 路径和
  • 修改子树边权
  • 修改路径边权

先 DFS 出 dfn,然后操作 1、3 对应了一段连续的区间,直接线段树区间操作即可;操作 2、4 首先将路径拆成两条链 \((u,\text{lca}),(\text{lca},v)\),然后分别剖即可。时间复杂度分别为 \(O(\log n),O(\log^2 n)\)

实现

第一遍 DFS 求出以下信息:

  • \(dep(u)\)\(u\) 的深度;
  • \(siz(u)\)\(u\) 子树大小;
  • \(son(u)\)\(u\) 的重儿子;
  • \(fa(u)\)\(u\) 的父亲。
void dfs1(int u,int Fa){
    dep[u]=dep[Fa]+1;
    fa[u]=Fa;
    int mxson,mxsiz=0;
    for(edge v:e[u]){
        if(v.v!=fa){
            dfs1(v.v,u);
            siz[u]+=siz[v.v];
            if(siz[v.v]>mxsiz)
                mxsiz=siz[v.v],mxson=v.v;
        }
    }
    son[u]=mxson;
}

第二遍 DFS 求出以下信息:

  • \(top(u)\):包含 \(u\) 的重链链头;
  • \(dfn(u)\)\(u\) 的 dfn(\(rdfn(u)\) 为回溯时 \(u\) 的 dfn);
  • \(idx(u)\)\(u\) 的 dfn 逆向索引。
void dfs2(int u,int t){
    top[u]=t;
    dfn[u]=++dfncnt;
    idx[dfncnt]=u;
    if(top[son[u]]!=t) dfs2(son[u],son[u]);
    else dfs2(son[u],t);
    for(edge v:e[u]){
        if(v.v!=fa[u]&&v.v!=son[u]){
            dfs2(v.v,v.v); // 轻儿子肯定不在 u 的重链上
        }
    }
    // rdfn[u]=dfncnt;
}

修改路径:

void modify_chain(int u,int v,int x){
    int fu=u,fv=v;
    while(top[fu]!=top[fv]){
        if(dep[top[fu]]<dep[top[fv]]) swap(fu,fv); 
        modify(1,1,n,dfn[top[fu]],dfn[fu],x);
        fu=fa[top[fu]]; // 每次选深的跳直到到同一重链上
    }
    if(dep[fu]>dep[fv]) swap(fu,fv);
    modify(1,1,dfn[fu],dfn[fv],x);
}

查询路径:

int query_chain(int u,int v,int x){
    int fu=u,fv=v,res=0;
    while(top[fu]!=top[fv]){
        if(dep[top[fu]]<dep[top[fv]]) swap(fu,fv); 
        res+=query(1,1,n,dfn[top[fu]],dfn[fu],x);
        fu=fa[top[fu]]; // 每次选深的跳直到到同一重链上
    }
    if(dep[fu]>dep[fv]) swap(fu,fv);
    res+=query(1,1,dfn[fu],dfn[fv],x);
    return res;
}

特殊的,如果信息在点上,则可以把信息转移到边 \((u,fa(u))\) 上。

长链剖分

忘了,但是很迪奥。

树上启发式合并(DSU on tree)

思想

借用了重剖的思想。分三步:

  • 递归走轻儿子统计子树答案(不对答案影响)
  • 走重儿子统计答案并把答案留在每个重儿子上
  • 再走轻儿子合并答案

考虑复杂度,由重剖中的证明,轻儿子至多走 \(\log n\) 个,重儿子只走一遍,总复杂度 \(O(n\log n)\)

容易知道,其限制是不能带修,只能处理子树问题。

实现

void dfs1(int u,int Fa);        // 重剖
void add(int u,int Fa);         // 统计子树答案
void del(int u,int Fa);         // 消除影响
int getans();
void dfs(int u,int Fa,int type){// type -> 边的类型
    for(auto v:e[u]){
        if(v.v!=Fa&&v.v!=son[u]){
            dfs(v,u,0);
        }
    }
    if(son[u]) dfs(son[u],u,1);
    for(auto v:e[u]){
        if(v.v!=Fa&&v.v!=son[u]){
            add(v.v,u);         // 统计轻儿子子树答案
        }
    }
    add(u);                     // 处理重链答案
    ans[u]=getans();            // 保存子树答案
    if(!type) del(u,Fa);        // 如果是轻儿子消去影响
}

例题 1:CF600E Lomsat gelral

给一棵树,每个节点有一种颜色 \(c_u\),设一个节点的主导颜色集合 \(P_u\) 为其子树内最多的颜色,对于每个节点求 \(\sum\limits_{x\in P_u}x\)
\(n\le 10^5\)

开一个桶 \(cnt_c\) 表示颜色 \(c\) 出现次数,再开一个桶 \(p_{x}\) 表示出现次数为 \(x\) 的颜色编号和,然后可写出 adddel。时间复杂度 \(O(n\log n)\)

这里有一个小 trick,就是如果统计子树答案与子树结构无关,则可以根据子树内 dfn 连续而直接遍历 \([dfn(u),rdfn(u)]\) 统计答案即可。(PS:但还是不建议这样用,有时候可能莫名 WA 掉)

int mxtcnt;
void add(int u,int Fa){
    pcnt[cnt[c[u]]]-=c[u];
    cnt[c[u]]++;
    pcnt[cnt[c[u]]]+=c[u];
    if(cnt[c[u]]>mxtcnt) mxtcnt=cnt[c[u]];
}
void del(int u){
    pcnt[cnt[c[u]]]-=c[u];
    cnt[c[u]]--;
    pcnt[cnt[c[u]]]+=c[u];
    while(!pcnt[mxtcnt]) mxtcnt--;
}
int getans(){
    return pcnt[mxtcnt];
}
void dfs(int u,int Fa,int type){
    for(int v:e[u]){
        if(v!=Fa&&v!=son[u]){
            dfs(v,u,0);
        }
    }
    if(son[u]) dfs(son[u],u,1);
    for(int v:e[u]){
        if(v!=Fa&&v!=son[u]){
            for(int i=dfn[v];i<=rdfn[v];i++){
                add(idx[i]);
            } 
        }
    }
    add(u);
    ans[u]=getans();
    if(!type){
        for(int i=dfn[u];i<=rdfn[u];i++){
            del(idx[i]);
        }
    }
}

例题 2:CF570D Tree Requests

给一棵树,每个点有一个字母 \(c_u\),定义深度为到 1 的距离,\(m\) 次询问 \(u\) 子树内深度为 \(x\) 的点上的字母是否能重组变成回文串。
\(n\le 5\times 10^5\)

注意到回文串等价于至多存在一个字母的数量为奇数。我们可以直接用异或来代替,并用 __builtin_popcount(cnt[dep[x]])<=1 来判断是否合法。其他同理。

code
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+3;
int n,c[maxn],dep[maxn],fa[maxn],son[maxn],siz[maxn],dfn[maxn],rdfn[maxn],idx[maxn],dfncnt;
vector<int>e[maxn];
struct pi{
    int first;
    int second;
};
vector<pi>Q[maxn];
int ans[maxn],cnt[maxn];
void dfs1(int u,int Fa){
    idx[dfncnt]=u;
    dep[u]=dep[Fa]+1;
    fa[u]=Fa;
    siz[u]=1;
    int mxson=0,mxsiz=0;
    for(int v:e[u]){
        if(v!=Fa){
            dfs1(v,u);
            siz[u]+=siz[v];
            if(siz[v]>mxsiz)
                mxsiz=siz[v],mxson=v;
        }
    }
    son[u]=mxson;
    rdfn[u]=dfncnt;
}
int json;
void add(int u){
    cnt[dep[u]]^=(1<<c[u]);
}
void del(int u){
    cnt[dep[u]]^=(1<<c[u]);
}
void dfs(int u,int Fa,int type){
    for(int v:e[u]){
        if(v!=Fa&&v!=son[u]){
            dfs(v,u,0);
        }
    }
    if(son[u]) dfs(son[u],u,1);
    for(int v:e[u]){
        if(v!=Fa&&v!=son[u]){
            for(int i=dfn[v];i<=rdfn[v];i++){
                add(idx[i]);
            } 
        }
    }
    add(u);
    for(auto i:Q[u]){
        if(__builtin_popcount(cnt[i.first])<=1) ans[i.second]=1;
        else ans[i.second]=0;
    }
    if(!type){
        for(int i=dfn[u];i<=rdfn[u];i++) del(idx[i]);
    }
}
signed main(){
    int m;
    ios::sync_with_stdio(0);
    cin>>n>>m;
    for(int i=2,u;i<=n;i++){
        cin>>u;
        e[i].emplace_back(u);
        e[u].emplace_back(i);
    }
    for(int i=1;i<=n;i++){
        char ch;
        cin>>ch;
        c[i]=ch-'a';
    }
    for(int i=1,a,b;i<=m;i++){
        cin>>a>>b;
        Q[a].push_back({b,i});
    }
    dfs1(1,0);
    dfs(1,0,0);
    for(int i=1;i<=m;i++){
        if(ans[i]) puts("Yes");
        else puts("No");
    }
    return 0;
}

例题 3:CF246E Blood Cousins Return

给一个森林,每个点有一个颜色 \(c_u\)\(m\) 次询问 \(u\) 子树内距离 \(u\)\(k\) 的点的颜色种类数。
\(n\le 10^5\)

同理,开一个桶 cnt[k][c] 记录当前子树内深度为 \(k\) 颜色 \(c\) 的数量,然后跑 DSU 即可,注意我们这个数组是开不下的,需要开 map,所以时间复杂度 \(O(n\log^2 n)\)

code
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+3;
int n,c[maxn],dep[maxn],fa[maxn],son[maxn],siz[maxn];
vector<int>e[maxn];
map<string,int> mp;
int ncnt;
struct pi{
    int first;
    int second;
};
vector<pi>Q[maxn];
int ans[maxn];
map<int,int>cnt[maxn];
void dfs1(int u,int Fa){
    dep[u]=dep[Fa]+1;
    fa[u]=Fa;
    siz[u]=1;
    int mxson=0,mxsiz=0;
    for(int v:e[u]){
        if(v!=Fa){
            dfs1(v,u);
            siz[u]+=siz[v];
            if(siz[v]>mxsiz)
                mxsiz=siz[v],mxson=v;
        }
    }
    son[u]=mxson;
}
int json;
int mxtcnt[maxn];
void add(int u){
    if(!cnt[dep[u]][c[u]]) mxtcnt[dep[u]]++;
    cnt[dep[u]][c[u]]++;
    for(int v:e[u]){
        if(v!=fa[u]&&v!=json) add(v);
    }
}
void del(int u){
    cnt[dep[u]][c[u]]--;
    if(!cnt[dep[u]][c[u]]) mxtcnt[dep[u]]--;
    for(int v:e[u]){
        if(v!=fa[u]) del(v);
    }
}
void dfs(int u,int Fa,int type){
    for(int v:e[u]){
        if(v!=Fa&&v!=son[u]){
            dfs(v,u,0);
        }
    }
    if(son[u]) dfs(son[u],u,1),json=son[u];
    add(u);json=0;
    for(auto i:Q[u]){
        ans[i.second]=mxtcnt[dep[u]+i.first];
    }
    if(!type){
        del(u);
    }
}
vector<int>root;
signed main(){
    int m;
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin>>n;
    for(int i=1,u;i<=n;i++){
        string ch;
        cin>>ch>>u;
        if(!mp[ch]) c[i]=mp[ch]=++ncnt;
        else c[i]=mp[ch];
        e[i].emplace_back(u);
        e[u].emplace_back(i);
        // if(!u) root.emplace_back(i);
    }
    cin>>m;
    for(int i=1,a,b;i<=m;i++){
        cin>>a>>b;
        Q[a].push_back({b,i});
    }
    dep[0]=-1;dfs1(0,0),dfs(0,0,0);
    
    for(int i=1;i<=m;i++){
        cout<<ans[i]<<" \n";
    }
    return 0;
}

posted @ 2024-10-11 20:55  view3937  阅读(5)  评论(0编辑  收藏  举报
Title