2024/8/19~22总结(树上合并、点分治)
树上合并
-
总的来说,树上合并类问题主要用于解决树上统计种类数、最大值一类的问题。
-
最朴素的树上合并思路为分别统计每个子树的答案合并再加上父亲节点本身的答案。一般采用启发式合并,将小子树合并进大子树中
如
树上数颜色
- 题意:
给定一颗有根树,每个节点有颜色,求每棵子树的颜色种类数 - 简要题解:
非常经典,颜色数统计用类似莫队的桶,每到加到临界点便改变颜色数,运用类似树剖的思想,将dfn序与重儿子预处理出来启发式合并优化复杂度
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
vector <int> q[N];
int n,tot[N],c[N],dfn[N],colordfn,siz[N],big[N],L[N],R[N],cnt[N],ans[N],ques[N];
void add(int i)
{
if(cnt[c[i]]==0) colordfn++;
cnt[c[i]]++;
}
void del(int i)
{
cnt[c[i]]--;
if(cnt[c[i]]==0) colordfn--;
}
void dfs0(int u,int fa)//初始化dfn序和树的相关信息
{
L[u]=++colordfn;dfn[colordfn]=u;siz[u]=1;//L->u在dfn序中的位置 dfn->dfn序
for(int i=0;i<tot[u];i++)
{
int v=q[u][i];
if(v==fa) continue;
dfs0(v,u);
siz[u]+=siz[v];
if(!big[u]||siz[v]>siz[big[u]]) big[u]=v;//处理重儿子
}
R[u]=colordfn;
}
void dfs1(int u,int fa,bool keep)
{
for(int i=0;i<tot[u];i++)
{
int v=q[u][i];
if(v==big[u]||v==fa) continue;
dfs1(v,u,0);
}
if(big[u])
dfs1(big[u],u,1);
for(int i=0;i<tot[u];i++)
{
int v=q[u][i];
if(v==fa||v==big[u]) continue;
for(int i=L[v];i<=R[v];i++)
add(dfn[i]);
}
add(u);
ans[u]=colordfn;
if(keep==0)
for(int i=L[u];i<=R[u];i++)
del(dfn[i]);
}
int main()
{
cin>>n;
for(int i=1,u,v;i<n;i++)
{
cin>>u>>v;
q[u].push_back(v),tot[u]++;
q[v].push_back(u),tot[v]++;
}
for(int i=1;i<=n;i++)
{
cin>>c[i];
}
dfs0(1,0);
colordfn=0;
dfs1(1,0,0);
int m;
cin>>m;
for(int i=1;i<=m;i++)
{
cin>>ques[i];
}
for(int i=1;i<=m;i++) cout<<ans[ques[i]]<<endl;
return 0;
}
线段树合并
- 题意:给定一棵树,给定一段从u到v的简单路径,给这条简单路径上的所有点发放一个权值,求每个点最后拥有最多的是哪种权值
- 题解:经典的树上区间加求种类数问题。区间操作考虑树上差分,操作为u+1,v+1,lca(u,v)-1,fa[lca(u,v)]-1。差分后求种类数要将子树的答案和这个节点自己的答案合并。直接用桶按位合并是\(O({n^2)}\) 的,于是考虑对每个结点建一颗线段树,统计答案时一层一层向上合并。
#include<bits/stdc++.h>
using namespace std;
const int N=5e6+10;
const int M=1e5+10;
const int LOG=20;
const int rmax=100000;
int f[M][LOG+10],dep[M],rt[M],sum[N],ls[N],rs[N],res[N],n,m,cnt,tot[M],ans[M];
vector <int> q[M];
void initlca(int u,int fa) //初始化倍增函数f
{
dep[u]=dep[fa]+1;
f[u][0]=fa;
for(int i=1;i<LOG;i++) f[u][i]=f[f[u][i-1]][i-1];
for(int i=0;i<tot[u];i++)
{
int v=q[u][i];
if(v==fa) continue;
initlca(v,u);
}
}
int lca(int a,int b) //求lca
{
if(dep[a]<dep[b]) swap(a,b);
for(int i=LOG-1;i>=0;i--)
if(dep[f[a][i]]>=dep[b]) a=f[a][i];
if(a==b) return a;
for(int i=LOG;i>=0;i--) if(f[a][i]!=f[b][i]) a=f[a][i],b=f[b][i];
return f[a][0];
}
void push_up(int id)
{
if(sum[ls[id]]<sum[rs[id]])
{
sum[id]=sum[rs[id]];
res[id]=res[rs[id]];
}
else
{
sum[id]=sum[ls[id]];
res[id]=res[ls[id]];
}
}
int build(int id,int l,int r,int color,int val) //新建节点
{
if(!id) id=++cnt;
if(l==r)
{
sum[id]+=val,res[id]=color;
return id;
}
int mid=(l+r)>>1;
if(color<=mid) ls[id]=build(ls[id],l,mid,color,val);
else rs[id]=build(rs[id],mid+1,r,color,val);
push_up(id);
return id;
}
int merge(int a,int b,int l,int r) //核心代码,合并
{
if(!a||!b) return a+b;
if(l==r)
{
sum[a]+=sum[b];
return a;
}
int mid=(l+r)>>1;
ls[a]=merge(ls[a],ls[b],l,mid);
rs[a]=merge(rs[a],rs[b],mid+1,r);
push_up(a);
return a;
}
void cacl(int u) //统计答案
{
for(int i=0;i<tot[u];i++)
{
int v=q[u][i];
if(v==f[u][0]) continue;
cacl(v);
rt[u]=merge(rt[u],rt[v],1,rmax);
}
ans[u]=res[rt[u]];
if(sum[rt[u]]==0) ans[u]=0;
}
int main()
{
cin>>n>>m;
for(int i=1,u,v;i<n;i++)
{
cin>>u>>v;
q[u].push_back(v),tot[u]++;
q[v].push_back(u),tot[v]++;
}
initlca(1,0);
for(int i=1,x,y,z;i<=m;i++)
{
cin>>x>>y>>z;
rt[x]=build(rt[x],1,rmax,z,1);
rt[y]=build(rt[y],1,rmax,z,1);
int l=lca(x,y);
rt[l]=build(rt[l],1,rmax,z,-1); //差分
rt[f[l][0]]=build(rt[f[l][0]],1,rmax,z,-1);
}
cacl(1);
for(int i=1;i<=n;i++) cout<<ans[i]<<endl;
return 0;
}
点分治
- 对于解决树上k距离、与路径和最值等路径问题的有力工具。虽然dsu on tree也可解决大部分点分治题目,但点分治思路相对固定,思维难度较低。缺点是在与其他数据结构结合时代码量极大,同时并不好调,需要注意细节并且写熟练。
- 点分治作为一种分治算法,与序列分治有异曲同工之妙。序列分治是取l与r之间的mid来判断与统计答案,而树上的“mid”被称为“重心”,是树中最大子树最小的节点。不过,与序列二分不同的是,重心是用来统计其统计范围内经过这个重心的树上路径的答案,而如何统计也成为了点分治中的重点。
【模板】点分治
- 以此题为例来介绍点分治中的几个重点板块。
- 题意:给定一棵有 n 个点的树,询问树上距离为 k 的点对是否存在
- 题解:树上路径问题,考虑点分治。将某重心子树中的路径分为两类:经过当前重心的,不经过的。经过的现在处理,不经过的继续分重心递归求解。
- 求重心
- 事实上求中心就是遍历一遍子树,统计每个节点最大子树最小的节点
void getroot(int u,int fa,int sumsiz)
//sumsiz->当前统计区域的总节点数。因为除u节点的每个子树外,其父亲及以上的节点也算做统计重心时的“子树”
{
maxsiz[u]=0,siz[u]=1;
for(int i=0;i<tot[u];i++)
{
int v=q[u][i].v;
if(v==fa||vis[v]) continue;
getroot(v,u,sumsiz);
maxsiz[u]=max(siz[v],maxsiz[u]);
siz[u]+=siz[v];
}
maxsiz[u]=max(sumsiz-siz[u],maxsiz[u]);
if(maxsiz[u]<maxsiz[zx]) zx=u;
}
- 分治
- dfs整棵树,求重心后分而治之,统计经过重心的答案
void solve(int u)
{
vis[u]=1,tong[0]=1;cacl(u);
for(int i=0;i<tot[u];i++)
{
int v=q[u][i].v;
if(vis[v]) continue;
maxsiz[0]=n,zx=0;
getroot(v,0,siz[v]);solve(zx);
}
}
- 统计重心的答案
- 点分治考察的重点,其他的部分都比较固定。同时,需要极其注重其中的细节。
- 对于这道题,考虑开数组rev存重心当前处理的子树中所有节点与重心存在的距离。再存个桶,存已经处理了的子树中的所有距离,便可\(O(1)\)处理所有经过重心的可能距离。同时注意先处理后再将rev存入桶中,细节详见代码。
void cacl(int u)
{
int c=0;
for(int i=0;i<tot[u];i++)
{
int v=q[u][i].v;
if(vis[v]) continue;
cnt=0,dis[v]=q[u][i].w;getdis(v,u); //求v及其子树所有可能的距离,存在rev中,详见后文
for(int j=1;j<=cnt;j++)
for(int k=1;k<=m;k++)
if(query[k]>=rev[j])
ans[k] |= tong[query[k]-rev[j]];
//统计答案,对于每个询问,若桶中存有与rev匹配(和为query)的值,则说明此询问有解。
//或等于不影响之前的答案。
for(int j=1;j<=cnt;j++)
{
qq[++c]=rev[j];
if(rev[j]<=10000000)
//注意越界,超过后由于询问都没这么大,一定不会有匹配的值了,直接舍弃
tong[rev[j]]=1;
//统计完答案后将rev存入桶中,若先存入桶中,会出现自己与自己匹配的情况。
}
}
for(int i=1;i<=c;i++)
tong[qq[i]]=0; //注意记得将桶清空
}
- 求重心子树中每个节点与中心的距离
- 将新加入的节点求距离后加入rev中递归继续求解
void getdis(int u,int fa)
{
rev[++cnt]=dis[u];
for(int i=0;i<tot[u];i++)
{
int v=q[u][i].v;
if(v==fa||vis[v]) continue;
dis[v]=dis[u]+q[u][i].w;
getdis(v,u);
}
}
- 主函数套路非常固定,在此不多赘述
- 完整代码
#include<bits/stdc++.h>
using namespace std;
const int N=10000005;
const int M=4e4+10;
int siz[N],tot[N],maxsiz[N],zx,n,cnt=0,rev[N],tong[N],m,ans[N],qq[N],query[N],dis[N];
bool vis[N];
struct node
{
int v,w;
};
vector <node> q[N];
void getroot(int u,int fa,int sumsiz)
{
maxsiz[u]=0,siz[u]=1;
for(int i=0;i<tot[u];i++)
{
int v=q[u][i].v;
if(v==fa||vis[v]) continue;
getroot(v,u,sumsiz);
maxsiz[u]=max(siz[v],maxsiz[u]);
siz[u]+=siz[v];
}
maxsiz[u]=max(sumsiz-siz[u],maxsiz[u]);
if(maxsiz[u]<maxsiz[zx]) zx=u;
}
void getdis(int u,int fa)
{
rev[++cnt]=dis[u];
for(int i=0;i<tot[u];i++)
{
int v=q[u][i].v;
if(v==fa||vis[v]) continue;
dis[v]=dis[u]+q[u][i].w;
getdis(v,u);
}
}
void cacl(int u)
{
int c=0;
for(int i=0;i<tot[u];i++)
{
int v=q[u][i].v;
if(vis[v]) continue;
cnt=0,dis[v]=q[u][i].w;getdis(v,u);
for(int j=1;j<=cnt;j++)
for(int k=1;k<=m;k++)
if(query[k]>=rev[j])
ans[k] |= tong[query[k]-rev[j]];
for(int j=1;j<=cnt;j++)
{
qq[++c]=rev[j];
if(rev[j]<=10000000)
tong[rev[j]]=1;
}
}
for(int i=1;i<=c;i++)
tong[qq[i]]=0;
}
void solve(int u)
{
vis[u]=1,tong[0]=1;cacl(u);
for(int i=0;i<tot[u];i++)
{
int v=q[u][i].v;
if(vis[v]) continue;
maxsiz[0]=n,zx=0;
getroot(v,0,siz[v]);solve(zx);
}
}
int main()
{
cin>>n>>m;
for(int i=1,u,v,w;i<n;i++)
{
cin>>u>>v>>w;
q[u].push_back({v,w}),tot[u]++;
q[v].push_back({u,w}),tot[v]++;
}
for(int i=1;i<=m;i++) cin>>query[i];
maxsiz[0]=n;
getroot(1,0,n);solve(zx);
for(int i=1;i<=m;i++)
if(ans[i]) cout<<"AYE"<<endl;
else cout<<"NAY"<<endl;
}
注意细节!注意细节!注意细节!重要的事情说三遍!!!
点分治例题
- 由于点分治代码很相似,在此只叙述主要思路以及统计答案的过程
Tree
- 题意:求树上两点间边权和小于等于k的路径数量
- 统计答案:对于每个重心,由于是求小于等于k的路径数,因此直接抛弃掉桶,将每种路径长存在rev中,将rev排序,有些取巧地用双指针l,r,表示rev[l+1]到rev[r]之间所有边(不包括l本身)都可以与rev[l]匹配满足小于等于k
int cacl(int u,int w)
{
cnt=0;dis[u]=w;getdis(u,0);
int l=1,r=cnt,res=0;
sort(rev+1,rev+cnt+1);
while(l<=r)
{
if(rev[l]+rev[r]<=k) res+=r-l,l++;
//rev[l]能与rev[l+1]到rev[r]之间的所有数匹配,方案数+r-l.
else r--;
}
return res;
}
树的难题
- 题意:给一棵树,每个点有颜色,每种颜色有权值,连续相同颜色权值和为其颜色权值本身。求边数在L到R之间的路径的最大权值和。
- 统计答案:同样的分治求重心,然而不同颜色的最大权值和并不好统计,同时还要考虑重心的不同与相同颜色分界的问题。考虑对重心的所有v按颜色编号排序,使所有颜色一样的边在一起连续统计。而对已经处理过的边维护最大权值,考虑线段树。
- 建两棵线段树,一棵维护当前相同颜色但已经处理过的边的最大权值,一棵维护其他已经处理且颜色与当前边不同的最大权值。
- 同时建一个栈,将相同颜色集中处理完后,再将这些节点全部扔进维护颜色不同的线段树中并将维护相同颜色的线段树清空。
- 线段树
struct tree
{
#define ls ((u<<1)) //左儿子
#define rs ((u<<1)+1)//右儿子
struct edge
{
int maxnum,tag; //最大权值、是否清空的标记
}tr[N<<3];
void clear() //清空
{
tr[1].tag=1;tr[1].maxnum=-inf;
}
void push_down(int u)//下放清空标记
{
if(tr[u].tag)
{
tr[ls].tag=tr[rs].tag=1;
tr[ls].maxnum=tr[rs].maxnum=-inf;
tr[u].tag=0;
}
}
void push_up(int u)
{
tr[u].maxnum=max(tr[ls].maxnum,tr[rs].maxnum);
}
int query(int u,int l,int r,int nl,int nr)//区间询问最大值
{
if(l>nr||r<nl) return -inf;
if(l>=nl&&r<=nr) return tr[u].maxnum;
if(tr[u].tag) return -inf;
push_down(u);int mid=(l+r)>>1;
return max(query(ls,l,mid,nl,nr),query(rs,mid+1,r,nl,nr));
}
void change(int u,int l,int r,int p,int x)//单点修改
{
if(l>p||r<p) return;
if(l==r)
{
tr[u].tag=0;
tr[u].maxnum=max(tr[u].maxnum,x);
return;
}
push_down(u);
int mid=(l+r)>>1;
if(p<=mid) change(ls,l,mid,p,x);
else change(rs,mid+1,r,p,x);
push_up(u);
}
}diff,same;
- 统计答案
void getdis(int u,int fa,int val,int lastcol)
//这里的getdis是用来统计答案的,同时处理dep满足L与R的限制
{
if(u==fa) return;
dep[u]=dep[fa]+1;
// cout<<"getdis "<<u<<" fa is "<<fa<<" dep is "<<dep[u]<<" val is "<<val<<endl;
if(dep[u]>R) return;
if(dep[u]>=L&&dep[u]<=R) ans=max(ans,val);
// cout<<"change ans "<<ans;
ans=max({ans,val+same.query(1,0,n,max(0,L-dep[u]),R-dep[u])-c[nowcolor],val+diff.query(1,0,n,max(L-dep[u],0),R-dep[u])});
// cout<<" to "<<ans<<endl;
for(int i=0;i<tot[u];i++)
{
int v=q[u][i].v;
if(v==fa||vis[v]) continue;
if(q[u][i].color==lastcol) getdis(v,u,val,lastcol);
else getdis(v,u,val+c[q[u][i].color],q[u][i].color);
}
}
void addsame(int u,int fa,int val,int lastcolor) //same树中插入权值
{
dep[u]=dep[fa]+1;
if(dep[u]>R) return;
same.change(1,0,n,dep[u],val);
for(int i=0;i<tot[u];i++)
{
int v=q[u][i].v;
if(v==fa||vis[v]) continue;
if(q[u][i].color==lastcolor) addsame(v,u,val,lastcolor);//区分同色与异色
else addsame(v,u,val+c[q[u][i].color],q[u][i].color);
}
}
void adddiff(int u,int fa,int val,int lastcolor)//diff树中插入权值
{
dep[u]=dep[fa]+1;
if(dep[u]>R) return;
diff.change(1,0,n,dep[u],val);
for(int i=0;i<tot[u];i++)
{
int v=q[u][i].v;
if(v==fa||vis[v]) continue;
if(q[u][i].color==lastcolor) adddiff(v,u,val,lastcolor);//同same
else adddiff(v,u,val+c[q[u][i].color],q[u][i].color);
}
}
//这里cacl 用来处理重心所有边
void cacl(int u)//记得在主函数中排序
{
// cout<<"cacl "<<u<<endl;
diff.clear(),same.clear();//注意清空
dep[u]=0; //一定别忘了初始化
int top=0;
for(int i=0;i<tot[u];i++)
{
dep[u]=0;
int v=q[u][i].v;
if(vis[v]||v==u) continue;
// cout<<v<<' '<<q[u][i].color<<endl;
if(i==0||q[u][i].color==q[u][i-1].color)
{
sta[++top]=v; //同色节点入栈
// cout<<top<<endl;
continue;
}
nowcolor=q[u][i-1].color;//记录颜色
for(int j=1;j<=top;j++)
{
//注意,与模板题相同,先统计答案再插入,避免自己匹配自己
getdis(sta[j],u,c[q[u][i-1].color],q[u][i-1].color);
addsame(sta[j],u,c[q[u][i-1].color],q[u][i-1].color);
}
same.clear();//记得清空
for(int j=1;j<=top;j++)
adddiff(sta[j],u,c[q[u][i-1].color],q[u][i-1].color);//扔进diff树中
top=0;
sta[++top]=q[u][i].v;
}
same.clear();
nowcolor=q[u][tot[u]-1].color;//注意,最后一种颜色统计不到,要单独处理
for(int j=1;j<=top;j++)
{
getdis(sta[j],u,c[q[u][tot[u]-1].color],q[u][tot[u]-1].color);
addsame(sta[j],u,c[q[u][tot[u]-1].color],q[u][tot[u]-1].color);
}
top=0;
diff.clear(),same.clear();//保险
}
- 看看用来调试的注释数量,就知道点分治不是什么好写好调的东西,细节真的多,注释全是坑啊