线段树合并 (学习笔记)(26.1.19)

线段树合并 (学习笔记)

概述

可以将两棵线段树合并为一颗的算法,一般用在动态开点线段树和权值线段树中

对于一些需要维护子树权值的题目中,父亲节点需要去合并自己的两个子树节点,对于普通的线段树,我们只需要去权值合并就行,但是对于动态开点线段树,如果我们没有一些优化,就会导致时间空间发生一些问题了,所以相应的线段树合并就诞生了

思路

线段树合并面临的共有三种情况

  1. 儿子都为空,我们直接不管这个节点即可
  2. 儿子都不为空,我们就需要递归去加两个儿子
  3. 有一个儿子为空,我们可以直接将这个儿子的信息加到当前线段树,另一个不管

实现

P4556 【模板】线段树合并

这道题还用到了动态开点和权值线段树和树上差分

对于动态开点,就是去正常的进行线段树操作,在进入的时候判断一下某个下标是否存在,如果不存在,就去新建一个下标,所以要专门储存lson和rson,不能直接去k<<1这样类似的操作

对于权值线段树,这是一个把权值作为下标进行操作的数据结构,它维护了在值域 \([1,V]\) 区间内的所有权值计数,在dfs2操作,对于某个节点,遍历其子树将所有节点加至它自己,root做的是以x为节点的权值线段树的根

对于树上差分,没有太多好讲的,主要就是可以用链表方式存储其对应的所有修改操作,其他无太多

#include <bits/stdc++.h>
using namespace std;
int n, m;
const int N = 100010;
int par[N][17], dep[N], mx[N*80], ls[N*80], rs[N*80];
int rew[N<<2];
int ans[N];
struct node{int to, nxt;}s[N<<1];int h[N], ecnt=0;
int idcnt;

//正常dfs,为了寻找deep和祖先 
void dfs(int x, int fa){
	par[x][0]=fa, dep[x]=dep[fa]+1;
	for(int i=1; i<=16; i++) par[x][i]=par[par[x][i-1]][i-1];
	for(int i=h[x];i;i=s[i].nxt){int u=s[i].to;if(u==fa) continue;dfs(u,x);}
	return;
}

//这个很像平衡树的合并 
int merge(int x, int y, int l, int r){
	if(!x||!y) return x|y;//有一个是空就返回
	//在找到某个权值后再修改 
	if(l==r) {mx[x]+=mx[y];return x;}
	int mid=(l+r)>>1;
	ls[x] = merge(ls[x],ls[y],l,mid),
	rs[x] = merge(rs[x],rs[y], mid+1,r),
	mx[x] = max(mx[ls[x]],mx[rs[x]]);
	return x;
}

//去做修改操作,把这里某个对应的权值修改 
void update(int &x, int l, int r, int pos, int data){
	if(!x) x=++idcnt;
	//这是新建一个节点,表明需要使用,动态开点操作,不需要考虑一些子节点的下标
	if(l==r) {mx[x]+=data;return;}
	int mid=(l+r)>>1;
	//权值线段树,利用的是去寻找权值,然后对权值修改
	//因为是权值线段树,所以存的左右儿子是大小权值
	if(pos<=mid) update(ls[x],l,mid,pos,data);
	else update(rs[x],mid+1,r,pos,data);
	mx[x]=max(mx[ls[x]],mx[rs[x]]);
	return;
}

//查询,第i座房的最大mx
int qry(int x, int l, int r){
	if(l==r) return l;
	int mid=(l+r)>>1;
	if(mx[x]==mx[ls[x]]) return qry(ls[x],l,mid);//证明子树中是左儿子大
	//否则寻找右儿子,为的就是去寻找最大的一个权值
	return qry(rs[x],mid+1,r);
}

int root[N], Log[N];
struct node_{
	int val, nxt;
}res[N<<2];
int rcnt=0, rh[N];
//意义就是,对于以u为根节点,遍历所有的能到的子节点并且合并到自己的权值线段树上
//代码会自己处理好权值的分配问题,因为有动态开点
void dfs2(int x, int fa){
	// 遍历所有邻接边,递归处理子树,子树合并到父节点
	//先处理图意义上的所有边,挨着合并
	for(int i=h[x];i;i=s[i].nxt){
		int u=s[i].to;
		if(u==fa) continue;
		dfs2(u,x), root[x]=merge(root[x],root[u],1,100000);
		// 把儿子u的线段树合并到当前节点x的线段树
		// root存的就是子树节点内的 
	}
	//再处理一下当前节点的所有差分权值,更新一下
	//也就是说这是树上差分的内容,同时也就更新了对一个节点所对应的权值
	//root是所有子树的权值节点合并完的根节点,只需要记录就行,没什么实际含义
    //若自己为叶子节点,那么他会把自己的所有操作权值做好,并且存储完毕
	for(int i=rh[x];i;i=res[i].nxt) update(root[x],1,100000,abs(res[i].val),(res[i].val>=0?1:-1));
	if(mx[root[x]]) ans[x]=qry(root[x],1,100000);
}

//LCA部分 
int LCA(int u, int v){
	if(dep[u]<dep[v]) swap(u,v);
	int mid=dep[u]-dep[v];
	while(~Log[mid]) u=par[u][Log[mid]], mid-=(1<<Log[mid]);
	if(u==v) return u;
	for(int i=16; i>=0; i--)
		if(par[u][i]!=par[v][i]) u=par[u][i], v=par[v][i];
	return par[u][0];
}

void addnode(int u, int v){s[++ecnt] = {v,h[u]};h[u] = ecnt;}
void add(int u, int val) {res[++rcnt] = {val,rh[u]};rh[u]=rcnt;}

int main(){
	ios::sync_with_stdio(0), cin.tie(0); 
	cin>>n>>m;
	Log[0]=-1;
	for(int i=1; i<N; i++)Log[i]=Log[i>>1]+1;
	for(int i=1; i<n; i++){
		int a, b;
		cin>>a>>b;
		addnode(a,b), addnode(b,a);
	}
	dfs(1,0);
	for(int i=1; i<=m; i++){
		int x, y, z, lca;
		cin>>x>>y>>z;
		//进行树上差分
		lca=LCA(x,y), add(x,z), add(y,z), add(lca,-z), add(par[lca][0],-z);
	}
	dfs2(1,0);
	for(int i=1; i<=n; i++) cout<<ans[i]<<'\n';
	return 0;
}
posted @ 2026-01-19 15:54  Yuriha  阅读(4)  评论(0)    收藏  举报