树链剖分|树上启发式合并
树链剖分
分为重链剖分和长链剖分以及其他奇怪的剖分。以重剖为主。
重链剖分
将树上问题重链剖分为序列问题(经常是 DFS 序)然后用数据结构(经常是线段树)维护。
剖分部分
定义:
- 重儿子:对于一个点,其儿子中,子树最大的那个;
- 重边:父亲到重儿子的连边;
- 轻儿子:除了重儿子以外的儿子;
- 轻边:父亲到轻儿子的连边。
证明一个东西:一条链上(不是路径,虽然差不多,路径就是两条链拼起来)至多有
很显然,每次若是走轻边,子树大小都至少会减少一半,所以至多走
于是我们即使暴力维护轻边的复杂度都是
而由于重边在儿子中的唯一性,所以所有的重边形成了若干条链,不妨称之重链。
一条链上包含多条重链。由于轻重边的交替排布,重链的数量级也是
综上对于一条链的操作,复杂度就为
例题:
给你一棵带边权树,支持下列操作:
- 查询子树和
- 查询
路径和 - 修改子树边权
- 修改路径边权
先 DFS 出 dfn,然后操作 1、3 对应了一段连续的区间,直接线段树区间操作即可;操作 2、4 首先将路径拆成两条链
实现
第一遍 DFS 求出以下信息:
: 的深度; : 子树大小; : 的重儿子; : 的父亲。
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 求出以下信息:
:包含 的重链链头; : 的 dfn( 为回溯时 的 dfn); : 的 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;
}
特殊的,如果信息在点上,则可以把信息转移到边
长链剖分
忘了,但是很迪奥。
树上启发式合并(DSU on tree)
思想
借用了重剖的思想。分三步:
- 递归走轻儿子统计子树答案(不对答案影响)
- 走重儿子统计答案并把答案留在每个重儿子上
- 再走轻儿子合并答案
考虑复杂度,由重剖中的证明,轻儿子至多走
容易知道,其限制是不能带修,只能处理子树问题。
实现
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
给一棵树,每个节点有一种颜色
,设一个节点的主导颜色集合 为其子树内最多的颜色,对于每个节点求 。
开一个桶 add
和 del
。时间复杂度
这里有一个小 trick,就是如果统计子树答案与子树结构无关,则可以根据子树内 dfn 连续而直接遍历
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
给一棵树,每个点有一个字母
,定义深度为到 1 的距离, 次询问 子树内深度为 的点上的字母是否能重组变成回文串。
注意到回文串等价于至多存在一个字母的数量为奇数。我们可以直接用异或来代替,并用 __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
给一个森林,每个点有一个颜色
, 次询问 子树内距离 为 的点的颜色种类数。
同理,开一个桶 cnt[k][c]
记录当前子树内深度为 map
,所以时间复杂度
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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!