【学习笔记】部分树上算法(概念篇)
本文包括:
轻重链剖分(done)
线段树合并(done)
to be upd:
长链剖分
DSU on tree(树上启发式合并)
点分治
边分治
LCT
有待更新
本文非例题代码大多未经过编译,谨慎使用
本文本来只有重剖长剖dsu,但是发现不会写,另外几个甚至更简单就带歪了.jpg
part1 轻重链剖分
树剖是一类算法的总称,主要分为三个家伙:
轻重链剖分、长链剖分、实链剖分
实链剖分一般只在LCT会用到,略去(我不会)
假如我们现在有一棵树,我们想要对他完成链上和子树上的一些操作(如子树加、子树和查询、链加链查询)
首先我们需要了解一些子树问题怎么做
dfs序
我们从根出发,向下进行dfs
设置一个时间,从1开始
第一次经过一个点时,把这个点的dfs序设置为当前时间,并且把时间+1
可能会有点疑惑这个玩意干啥用的
最大的用处是,同一个子树内的点的dfs序是连续的
于是我们可以先设一个数组
求dfs序的代码:
int tim;//时间戳
int a[N];//原点权(下标为编号)
int w[N];//新点权(下标为dfs序)
void dfs(int x,int fa){//当前节点及父亲
dfn[x]=++tim;
w[dfn[x]]=a[x];
for(i=0;i<e[x].size();i++){
if(e[x][i]==fa) continue;
dfs(e[x][i],x);
}
}
剖分
此时加入了链操作,事情就略微复杂了
我们朴素的dfs序无法解决链上问题(so bad)
我们需要一点点高档货——重链剖分
我们定义:对于一个节点。他的所有儿子中子树大小最大的儿子称为重儿子(有大小相等的随便选一个就行),其余为轻儿子
我们又可以定义:由一个轻儿子开始,后续全部由重儿子组成的链称为一条重链(根是轻儿子)
我们有一个结论:从一个节点到根的路径上,重链数量不超过
这个性质很好的保证了我们的复杂度
我们不妨先用一个dfs去找重儿子并且处理深度之类的信息
void dfs1(int x,int ft){
siz[x]=1;
dep[x]=dep[ft]+1;
fa[x]=ft;
for(int i=0;i<e[x].size();i++){
if(e[x][i]==ft) continue;
dfs1(e[x][i],x);
siz[x]+=siz[e[x][i]];
if(!son[x] or siz[e[x][i]]>siz[son[x]]) son[x]=e[x][i];
}
}
然后我们在标dfs序的时候先标重儿子的,这样重链上的dfs序将是连续的。顺便记录一下每条重链的顶端
void dfs2(int x,int top){
dfn[x]=++tim;
w[tim]=a[x];
tp[x]=top;
if(son[x]) dfs2(son[x],top);
for(int i=0;i<e[x].size();i++){
if(e[x][i]==son[x] or e[x][i]==fa[x]) continue;
dfs2(e[x][i],e[x][i]);
}
}
考虑怎么处理链上操作询问:直接往上跳
找出
跳一次就把跳过去的部分修改、查询搞定
直到两个在同一条重链,就把两点间修改了
void mli(int x,int y,int z){//修改
while(tp[x]!=tp[y]){
if(dep[tp[x]]<dep[tp[y]]) swap(x,y);
add(1,1,n,dfn[tp[x]],dfn[x],z);
x=fa[tp[x]];
}
if(dep[x]>dep[y]) swap(x,y);
add(1,1,n,dfn[x],dfn[y],z);
}
//查询
int qli(int x,int y){
int ans=0;
while(tp[x]!=tp[y]){
if(dep[tp[x]]<dep[tp[y]]) swap(x,y);
ans+=query(1,1,n,dfn[tp[x]],dfn[x]);
x=fa[tp[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return ans+query(1,1,n,dfn[x],dfn[y]);
}
借助跳,我们也可以求 LCA
int LCA(int x,int y){
while(tp[x]!=tp[y]){
if(dep[tp[x]]<dep[tp[y]]) swap(x,y);
x=fa[tp[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return x;
}
线段树合并
看起来这个玩意不是很像那种处理树上问题的东西,确实,但是这个玩意最大的应用就是树上问题
这玩意和DSU on tree可以说互相平替,这玩意空间大一些但是好搞,但是dsu大多数时候多一只log
合并的是动态开点权值线段树
首先直接放出合并代码因为真的没啥好说的,暴力递归一个一个搞
int merge(int a,int b,int l,int r){
if(!a or !b) return a+b;
if(l==r){
tr[a].max+=tr[b].max;
tr[a].ans=l;
return a;
}
int mid=(l+r)>>1;
tr[a].l=merge(tr[a].l,tr[b].l,l,mid);
tr[a].r=merge(tr[a].r,tr[b].r,mid+1,r);
pup(a);
return a;
}
dsu可做,但是不要
我们给每个节点搞一个动态开点权值线段树
然后给每个点记录一下区间最大值和每个结点的答案,pushup更新
然后dfs从叶子向上dfs回溯算完子节点再合并给父结点
#include<bits/stdc++.h>
#define int long long
#define ls tr[now].l
#define rs tr[now].r
using namespace std;
struct SEG{
int l,r,max,ans;
}tr[5000050];
int rt[100010],cl[100010];
int cnt,n,ass[100010];
vector<int> e[100010];
void pup(int now){
if(tr[ls].max>tr[rs].max){
tr[now].max=tr[ls].max;
tr[now].ans=tr[ls].ans;
}
else if(tr[ls].max<tr[rs].max){
tr[now].max=tr[rs].max;
tr[now].ans=tr[rs].ans;
}
else if(tr[ls].max==tr[rs].max){
tr[now].max=tr[ls].max;
tr[now].ans=tr[ls].ans+tr[rs].ans;
}
}
void upd(int &now,int l,int r,int pos){
if(!now) now=++cnt;
if(l==r){
tr[now].max++;
tr[now].ans=l;
return;
}
int mid=(l+r)>>1;
if(pos<=mid) upd(ls,l,mid,pos);
else upd(rs,mid+1,r,pos);
pup(now);
}
int merge(int a,int b,int l,int r){
if(!a or !b) return a+b;
if(l==r){
tr[a].max+=tr[b].max;
tr[a].ans=l;
return a;
}
int mid=(l+r)>>1;
tr[a].l=merge(tr[a].l,tr[b].l,l,mid);
tr[a].r=merge(tr[a].r,tr[b].r,mid+1,r);
pup(a);
return a;
}
void dfs(int now,int fa){
for(int i=0;i<e[now].size();i++){
int to=e[now][i];
if(to==fa) continue;
dfs(to,now);
merge(rt[now],rt[to],1,100000);
}
upd(rt[now],1,100000,cl[now]);
ass[now]=tr[rt[now]].ans;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>cl[i];
rt[i]=i;
cnt++;
}
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1,0);
for(int i=1;i<=n;i++) cout<<ass[i]<<" ";
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!