树分治、动态树分治学习笔记
点分治
点分治适合处理大规模的树上路径信息问题,选取重心,将当前的树拆分为几颗子树,然后递归子树求解问题,但是今天的重点不在这里
边分治
与点分治类似,选取一条边,均匀地将树分成两个部分,但是对于一个点有多个儿子时,时间复杂度就会非常大,于是我们可以将其转化,这里有两种方法
对于边分治,我们有这些性质:递归深度为
例题:「BZOJ2870」最长道路
题意:给定一棵
我们可以用边分治将树分为两个部分,
那么答案就是
那么我们考虑把路径分为
示例代码:
#include<bits/stdc++.h>
using namespace std;
vector <int> G[50001];
int n,cnt=1,mx=1e9,rt,tot[2],ver[200001],vis[100001],head[100001],val[100001],edge[200001],nex[200001],siz[100001];
long long ans;
struct node{
int dist,minn;
bool operator<(node &a){
return minn<a.minn;
}
}a[2][100001];
void add(int x,int y,int z){
ver[++cnt]=y;
edge[cnt]=z;
nex[cnt]=head[x];
head[x]=cnt;
}
void build(int u,int f){
for(int i=0,last=0;i<G[u].size();i++){
int v=G[u][i];
if(v!=f){
if(!last){
add(u,v,1);
add(v,u,1);
last=u;
}
else if(i==G[u].size()-1){
add(last,v,1);
add(v,last,1);
}
else{
val[++n]=val[u];
add(last,n,0);
add(n,last,0);
add(n,v,1);
add(v,n,1);
last=n;
}
}
}
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
if(v!=f) build(v,u);
}
}
void get(int u,int f,int all){
siz[u]=1;
for(int i=head[u];i!=0;i=nex[i]){
int v=ver[i];
if(!vis[i>>1]&&v!=f){
get(v,u,all);
siz[u]+=siz[v];
if(max(siz[v],all-siz[v])<mx){
mx=max(siz[v],all-siz[v]);
rt=i;
}
}
}
}
void dfs(int u,int f,int l,int m,int p){
a[p][++tot[p]]={l,m};
for(int i=head[u];i!=0;i=nex[i]){
int v=ver[i];
if(!vis[i>>1]&&v!=f) dfs(v,u,l+edge[i],min(m,val[v]),p);
}
}
void solve(int u,int all){
vis[u>>1]=1;
tot[0]=tot[1]=0;
dfs(ver[u],0,0,val[ver[u]],0);
dfs(ver[u^1],0,0,val[ver[u^1]],1);
sort(a[0]+1,a[0]+tot[0]+1);
sort(a[1]+1,a[1]+tot[1]+1);
for(int i=tot[0],p=tot[1]+1,lj=0;i>=1;i--){
while(p>1&&a[1][p-1].minn>=a[0][i].minn){
p--;
lj=max(lj,a[1][p].dist);
}
if(p<=tot[1]) ans=max(ans,1ll*a[0][i].minn*(lj+a[0][i].dist+edge[rt]+1));
}
for(int i=tot[1],p=tot[0]+1,lj=0;i>=1;i--){
while(p>1&&a[0][p-1].minn>=a[1][i].minn){
p--;
lj=max(lj,a[0][p].dist);
}
if(p<=tot[0]) ans=max(ans,1ll*a[1][i].minn*(lj+a[1][i].dist+edge[rt]+1));
}
int aus=siz[ver[u]];
if(aus>1){
mx=1e9;
get(ver[u],0,aus);
solve(rt,aus);
}
aus=all-aus;
if(aus>1){
mx=1e9;
get(ver[u^1],0,aus);
solve(rt,aus);
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&val[i]);
ans=max(ans,1ll*val[i]);
}
for(int i=1,u=0,v=0;i<n;i++){
scanf("%d%d",&u,&v);
G[u].push_back(v);
G[v].push_back(u);
}
build(1,0);
get(1,0,n);
solve(rt,n);
printf("%lld",ans);
return 0;
}
点分树
就是将点分治递归的点提前递归求解出来,然后和它的上级中心连边建树即可,下面画图解释(图片来源 但是我好歹自己画了一遍)
当然这里必须介绍一些关于重心的性质,对做题有帮助:
证明:利用反证法证明,假设
证明:利用调试法,暴力跳动即可,设当前节点为
当然这里只给出了第 因为我懒,自己看这个)
当然:点分树的子树是原树的一个联通快,这也是在点分树上进行换根操作的基础
对于无修改的点分治的题目,点分树的意义其实不大,主要用来减少常数,就比如 「WC2010」重建计划
动态点分治
动态点分治用来解决带点权或边权修改的树上路径信息统计问题,对于修改其实就可以在点分树上暴力跳父亲节点修改,统计也差不多,但是具体由题目而定,一般来说对于一个节点需要记录它到点分树自己的子树的信息和它在点分树上的父亲节点到自己子树的信息,由于树高
例题 :「模板」点分树|震波
题意:查询操作:查询与
对于每个节点维护线段树(注意动态开点)
以查询操作为例,我们要先将答案加上线段树
核心代码:
if(!op){
last=dist.query(dist.root[x],0,n,0,y);
for(int i=x;fa[i]!=0;i=fa[i]){
last+=dist.query(dist.root[fa[i]],0,n,0,y-lca.dist(x,fa[i]));
last-=ch.query(ch.root[i],0,n,0,y-lca.dist(x,fa[i]));
}
printf("%d\n",last);
}
else{
dist.updata(dist.root[x],0,n,0,y-a[x]);
for(int i=x;fa[i]!=0;i=fa[i]){
dist.updata(dist.root[fa[i]],0,n,lca.dist(x,fa[i]),y-a[x]);
ch.updata(ch.root[i],0,n,lca.dist(x,fa[i]),y-a[x]);
}
a[x]=y;
}
例题 :「ZJOI2007」捉迷藏
题意:修改操作:改变第
操作类似,相对于上一题来说就是把线段树改为可删堆,再多维护一个
核心代码:
if(s[0]=='G'){
if(all!=n) printf("%d\n",ans.top1());
else printf("-1\n");
}
else{
scanf("%d",&x);
if(!col[x]){
if(ch[x].size()>=2) ans.erase(ch[x].top1()+ch[x].top2());
ch[x].erase(0);
if(ch[x].size()>=2) ans.insert(ch[x].top1()+ch[x].top2());
for(int i=x;fa[i]!=0;i=fa[i]){
if(ch[fa[i]].size()>=2) ans.erase(ch[fa[i]].top1()+ch[fa[i]].top2());
ch[fa[i]].erase(dist[i].top1());
dist[i].erase(lca.dist(x,fa[i]));
if(dist[i].size()) ch[fa[i]].insert(dist[i].top1());
if(ch[fa[i]].size()>=2) ans.insert(ch[fa[i]].top1()+ch[fa[i]].top2());
}
}
else{
if(ch[x].size()>=2) ans.erase(ch[x].top1()+ch[x].top2());
ch[x].insert(0);
if(ch[x].size()>=2) ans.insert(ch[x].top1()+ch[x].top2());
for(int i=x;fa[i]!=0;i=fa[i]){
if(ch[fa[i]].size()>=2) ans.erase(ch[fa[i]].top1()+ch[fa[i]].top2());
if(dist[i].size()) ch[fa[i]].erase(dist[i].top1());
dist[i].insert(lca.dist(x,fa[i]));
ch[fa[i]].insert(dist[i].top1());
if(ch[fa[i]].size()>=2) ans.insert(ch[fa[i]].top1()+ch[fa[i]].top2());
}
}
col[x]^=1;
all+=(col[x]==1)-(col[x]==0);
}
例题 :「ZJOI2015」幻想乡战略游戏
题意:给定一个带边权的树,初始点权为
方法
令
所以最终
求出
示例代码:
#include<bits/stdc++.h>
using namespace std;
vector <pair<int,int> > G[100001];
long long all,full,dep[100001],size[100001];
int n,q,tot,fa[100001],son[100001],top[100001],dfn[100001],rnk[100001];
struct point{
long long olds,news,lazy;
int maxn;
}node[400001];
void dfs1(int rt,int from){
size[rt]=1;
son[rt]=-1;
for(int i=0;i<G[rt].size();i++){
int to=G[rt][i].second;
if(to!=from){
dep[to]=dep[rt]+G[rt][i].first;
fa[to]=rt;
dfs1(to,rt);
size[rt]+=size[to];
if(son[rt]==-1||size[to]>size[son[rt]]) son[rt]=to;
}
}
}
void dfs2(int rt,int t){
top[rt]=t;
dfn[rt]=++tot;
rnk[tot]=rt;
if(son[rt]==-1) return;
dfs2(son[rt],t);
for(int i=0;i<G[rt].size();i++){
int to=G[rt][i].second;
if(to!=fa[rt]&&to!=son[rt]) dfs2(to,to);
}
}
void build(int rt,int l,int r){
if(l==r){
node[rt].olds=dep[rnk[l]]-dep[fa[rnk[l]]];
return;
}
int mid=(l+r)/2;
build(rt*2,l,mid);
build(rt*2+1,mid+1,r);
node[rt].olds=node[rt*2].olds+node[rt*2+1].olds;
}
void push_down(int rt){
node[rt*2].news+=node[rt*2].olds*node[rt].lazy;
node[rt*2+1].news+=node[rt*2+1].olds*node[rt].lazy;
node[rt*2].maxn+=node[rt].lazy;
node[rt*2+1].maxn+=node[rt].lazy;
node[rt*2].lazy+=node[rt].lazy;
node[rt*2+1].lazy+=node[rt].lazy;
node[rt].lazy=0;
}
void updata(int rt,int l,int r,int L,int R,long long w){
if(L<=l&&r<=R){
node[rt].news+=node[rt].olds*w;
node[rt].maxn+=w;
node[rt].lazy+=w;
return;
}
push_down(rt);
int mid=(l+r)/2;
if(L<=mid) updata(rt*2,l,mid,L,R,w);
if(R>=mid+1) updata(rt*2+1,mid+1,r,L,R,w);
node[rt].news=node[rt*2].news+node[rt*2+1].news;
node[rt].maxn=max(node[rt*2].maxn,node[rt*2+1].maxn);
}
long long query(int rt,int l,int r,int L,int R){
if(L<=l&&r<=R) return node[rt].news;
push_down(rt);
int mid=(l+r)/2;
long long ret=0;
if(L<=mid) ret+=query(rt*2,l,mid,L,R);
if(R>=mid+1) ret+=query(rt*2+1,mid+1,r,L,R);
return ret;
}
int find(int rt,int l,int r){
if(l==r) return rnk[l];
push_down(rt);
int mid=(l+r)/2;
if(2*node[rt*2+1].maxn>all) return find(rt*2+1,mid+1,r);
else return find(rt*2,l,mid);
}
long long lookup(int x){
long long ret=0;
while(x){
ret+=query(1,1,n,dfn[top[x]],dfn[x]);
x=fa[top[x]];
}
return ret;
}
void modily(int x,long long e){
while(x){
updata(1,1,n,dfn[top[x]],dfn[x],e);
x=fa[top[x]];
}
}
int main(){
scanf("%d%d",&n,&q);
for(int i=1;i<n;i++){
int l,r,w;
scanf("%d%d%d",&l,&r,&w);
G[l].push_back({w,r});
G[r].push_back({w,l});
}
dep[1]=0;
dfs1(1,0);
dfs2(1,1);
build(1,1,n);
while(q--){
int u,e;
scanf("%d%d",&u,&e);
modily(u,e);
all+=e;
full+=dep[u]*e;
printf("%lld\n",full+all*dep[ans]-2*lookup(find(1,1,n)));
}
return 0;
}
方法
这个就比线段树分治好理解多了,记录
对于如何求出答案,有一个暴力的思想,我们向重心移动一次相对不移动的代价是更优秀的,于是我们就可以在原树上暴力移动比较,由于最终只有一个节点满足最优,所以是肯定能找到答案的,但是就会
示例代码:
#include<bits/stdc++.h>
using namespace std;
vector <pair<int,int> > G[100001],T[100001];
struct dist{
int cnt,fir[200001],lg[200001],dis[100001],st[400001][21];
int lca(int u,int v){
if(fir[u]>fir[v]) swap(u,v);
int k=lg[fir[v]-fir[u]+1];
return dis[u]+dis[v]-2*min(st[fir[u]][k],st[fir[v]-(1<<k)+1][k]);
}
void dfs(int u,int f){
st[++cnt][0]=dis[u];
fir[u]=cnt;
for(int i=0;i<G[u].size();i++){
int v=G[u][i].first;
if(v!=f){
dis[v]=dis[u]+G[u][i].second;
dfs(v,u);
st[++cnt][0]=dis[u];
}
}
}
void init(){
lg[1]=dis[1]=cnt=0;
memset(fir,0,sizeof(fir));
for(int i=2;i<=200000;i++) lg[i]=lg[i/2]+1;
dfs(1,0);
for(int k=1;k<=lg[cnt];k++){
for(int i=1;i+(1<<k)-1<=cnt;i++) st[i][k]=min(st[i][k-1],st[i+(1<<(k-1))][k-1]);
}
}
}d;
int n,q,x,y,w,rt=-1,root,dp[100001],siz[100001],fa[100001],vis[100001];
long long suma[100001],sumb[100001],sumv[100001];
void find(int u,int fa,int all){
siz[u]=1;
dp[u]=0;
for(int i=0;i<G[u].size();i++){
int v=G[u][i].first;
if(v!=fa&&!vis[v]){
find(v,u,all);
siz[u]+=siz[v];
dp[u]=max(dp[u],siz[v]);
}
}
dp[u]=max(dp[u],all-siz[u]);
if(rt==-1||dp[u]<dp[rt]) rt=u;
}
void build(int u){
vis[u]=1;
for(int i=0;i<G[u].size();i++){
int v=G[u][i].first;
if(!vis[v]){
rt=-1;
find(v,u,siz[v]);
T[u].push_back({rt,v});
fa[rt]=u;
build(rt);
}
}
}
void updata(int x,int y){
sumv[x]+=y;
for(int i=x;fa[i]!=0;i=fa[i]){
int dis=d.lca(fa[i],x);
sumv[fa[i]]+=y;
suma[fa[i]]+=1ll*dis*y;
sumb[i]+=1ll*dis*y;
}
}
long long solve(int u){
long long ans=suma[u];
for(int i=u;fa[i]!=0;i=fa[i]){
int dis=d.lca(fa[i],u);
ans+=suma[fa[i]]-sumb[i]+dis*(sumv[fa[i]]-sumv[i]);
}
return ans;
}
long long query(int u){
long long ans=solve(u);
for(int i=0;i<T[u].size();i++){
if(solve(T[u][i].second)<ans) return query(T[u][i].first);
}
return ans;
}
int main(){
scanf("%d%d",&n,&q);
for(int i=1;i<n;i++){
scanf("%d%d%d",&x,&y,&w);
G[x].push_back({y,w});
G[y].push_back({x,w});
}
d.init();
find(1,0,n);
root=rt;
build(root);
while(q--){
scanf("%d%d",&x,&y);
updata(x,y);
printf("%lld\n",query(root));
}
return 0;
}
本文作者:AKIOI
本文链接:https://www.cnblogs.com/zyxawa/p/18328039
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步