洛谷P4556,线段树合并
P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并
点击查看代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
void read(int &x){ //快读
x=0; char c=getchar();
while(!isdigit(c))c=getchar();
while(isdigit(c))x=x*10+c-'0',c=getchar();
}
const int N=100005;
#define mid ((l+r)>>1)
int n,m,ans[N];
vector<int> g[N];
int fa[N][20],dep[N];
int root[N],tot;
int ls[N*50],rs[N*50],sum[N*50],typ[N*50];
//sum:某种救济粮的数量
//typ:救济粮的类型
void dfs(int x,int f){ //树增
dep[x]=dep[f]+1; fa[x][0]=f;
for(int i=1; i<=18; i++)
fa[x][i]=fa[fa[x][i-1]][i-1];
for(int y:g[x])
if(y!=f) dfs(y,x);
}
int lca(int x,int y){ //求lca
if(dep[x]<dep[y]) swap(x,y);
for(int i=18; ~i; i--)
if(dep[fa[x][i]]>=dep[y])x=fa[x][i];
if(x==y) return y;
for(int i=18; ~i; i--)
if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];
return fa[x][0];
}
void pushup(int u){ //上传
if(sum[ls[u]]>=sum[rs[u]])
sum[u]=sum[ls[u]], typ[u]=typ[ls[u]];
else
sum[u]=sum[rs[u]], typ[u]=typ[rs[u]];
}
void change(int &u,int l,int r,int p,int k){ //点修
if(!u) u=++tot; //动态开点
if(l==r){sum[u]+=k; typ[u]=p; return;}
if(p<=mid) change(ls[u],l,mid,p,k);
else change(rs[u],mid+1,r,p,k);
pushup(u);
}
int merge(int x,int y,int l,int r){ //合并
if(!x||!y)return x+y;//一个空,返回另一个
if(l==r){sum[x]+=sum[y]; return x;}
ls[x]=merge(ls[x],ls[y],l,mid);
rs[x]=merge(rs[x],rs[y],mid+1,r);
pushup(x);
return x;
}
void dfs2(int x,int f){ //递归合并
for(int y:g[x]){
if(y==f) continue;
dfs2(y,x);
root[x]=merge(root[x],root[y],1,N);
}
ans[x]=sum[root[x]]?typ[root[x]]:0;
}
int main(){
read(n),read(m);
for(int i=1,x,y; i<n; i++){
read(x),read(y);
g[x].push_back(y); g[y].push_back(x);
}
dfs(1,0); //树增
for(int i=1,x,y,z; i<=m; i++){ //差分
read(x),read(y),read(z);
change(root[x],1,N,z,1);
change(root[y],1,N,z,1);
int t=lca(x,y);
change(root[t],1,N,z,-1);
change(root[fa[t][0]],1,N,z,-1);
}
dfs2(1,0); //递归合并
for(int i=1;i<=n;i++)printf("%d\n",ans[i]);
}
快读:其实快不快读都可以
- dfs:进行树的深度和父节点信息的预处理,为LCA计算做准备。
- g[x].push_back()来建树,注意是双向建树
- 其实这道题就是存在着两颗树,原树就是存储那n座房屋的,而每一个节点都有自己的一个线段树。这一棵线段树存储的就是救济粮的类型typ以及对应的数目sum。
change函数的功能其实管辖的就是这个节点的线段树,不仅可以动态构建:当节点 x 的线段树尚未存在时,动态创建它。而且还可以点修改:在线段树中修改特定救济粮类型(z)的数量。
- u 是当前线段树节点的编号(在 ls, rs, sum, typ 数组中的索引位置)。
- 叶子节点:每个叶子节点对应一个具体的救济粮类型,存储该类型在当前节点的差分数量。
- 非叶子节点:存储其管辖值域范围内数量最多的救济粮类型及其数量
在主函数中,change(root[x], 1, N, z, 1); 这行代码的作用是:在节点 x 的线段树中,为救济粮类型 z 的数量增加 1。往下的同理。
- merge函数就是将树合并,其返回值是合并后的线段树根节点的编号(索引)。
- dfs2才是本代码的重点,也是充分利用merge函数的地方。
root[x]=merge(root[x],root[y],1,N);
这一段代码就是把x和其子节点y的线段树合并在一起,更新后的root[x]虽然还是root[x],但是其映射的内容发生了改变,即sum值。
ans[x]=sum[root[x]]?typ[root[x]]:0;
相当于if(sum[root[x]] != 0) { ans[x] = typ[root[x]]; } else { ans[x] = 0; }
ans[x]的更新值其实就是其线段树根节点存储的救济粮类型 typ[root[x]]。为什么呢?
- 叶子节点:存储特定救济粮类型的数量
- 内部节点:存储其管辖值域范围内数量最多的救济粮类型及其数量
- 根节点:存储整个值域范围 [1, N] 内数量最多的救济粮类型及其数量
通过 pushup 操作,每个内部节点都会比较其左右子树的信息,并选择数量最多的类型
在 dfs2 过程中,节点 x 的线段树会合并所有子节点的线段树: - 合并后,节点 x 的线段树包含了 x 自身以及所有子节点的救济粮信息
- 根节点 root[x] 存储的是合并后整个值域范围内的最优解
- 因此,typ[root[x]] 就是节点 x 上数量最多的救济粮类型
除此之外,我们需要辨析一个重要概念:root[x]和x
root[x] 存储的是一个整数编号,这个编号是节点 x 对应的动态开点线段树的根节点在全局数组中的索引。可以把他看成它是访问节点 x 所有救济粮信息的唯一入口。而x则就是节点本身的编号。可以这么想,节点本来编号是5,但是它开的线段树的顶点的编号暂定为1,这个1就是root[x],所以二者还是有差别的。
- 所以,总体而言。这道题里面每个节点都有一个线段树,把线段树构建好之后,就要开始合并,从下面合并到上面,属于一种后序遍历,宏观上就是树的内容,微观上是线段树的内容。原树的合并之后,父节点就会拥有子节点的线段树数据,实际上还是利用了差分思想。
- 差分:标记后,当我们从叶子节点向上聚合数据(通过线段树合并),每个节点最终得到的救济粮数量就是所有经过该节点的路径的净效果。因为:x 和 y 的增加操作标记了路径的起点和终点。t 的减少操作避免了路径在 t 处被重复计算(因为 x 和 y 的路径在t交汇)f的减少操作确保路径只影响到 t 及以下的有效节点,而不影响 t 的父节点及以上(因为路径不包含 f)。
- 后两个change:在 t (LCA) 和 t 的父节点的线段树中为类型 z 减少1。这是因为:路径在 t 处相遇,但 t 是路径的最高点,所以需要抵消一次计数(因为 x 和 y 的增加操作会使 t 被计算两次?实际上,差分标记需要平衡,确保只有路径上的节点获得净增加)。在 t 的父节点处减少1,是为了确保路径不影响 t 的祖先节点。因为路径只从 x 到 y,不包含 t 的父节点。
2025·8·28

浙公网安备 33010602011771号