【学习笔记】线段树合并

线段树合并

前置知识:

  • 动态开点

  • 权值线段树

引入

洛谷P3224

永无乡包含 \(n\) 座岛,编号从 \(1\)\(n\) ,每座岛都有自己的独一无二的重要度,按照重要度可以将这 \(n\) 座岛排名,名次用 \(1\)\(n\) 来表示。某些岛之间由巨大的桥连接,通过桥可以从一个岛到达另一个岛。如果从岛 \(a\) 出发经过若干座(含 \(0\) 座)桥可以 到达岛 \(b\) ,则称岛 \(a\) 和岛 \(b\) 是连通的。

现在有两种操作:

B x y 表示在岛 \(x\) 与岛 \(y\) 之间修建一座新桥。

Q x k 表示询问当前与岛 \(x\) 连通的所有岛中第 \(k\) 重要的是哪座岛,即所有与岛 \(x\) 连通的岛中重要度排名第 \(k\) 小的岛是哪座,请你输出那个岛的编号。(\(n \le 10^5\)


求第 \(k\) 小我们想到可以开权值线段树,记录当前值的出现次数,在线段树上二分即可。

我们对于每一个岛都开一个线段树,可合并操作怎么办呢?这个时候就需要线段树合并了。

线段树合并

当两颗结构相同的线段树需要合并操作时,我们可以对应区间递归合并信息。

首先,\(n\) 棵线段树建完全二叉树空间肯定会爆炸,所以我们动态开点来记录,将所有线段树存在一个结构体里,\(root(i)\) 表示第 \(i\) 棵线段树根节点的编号。

那么合并如图:

最终时间复杂度为两棵树重叠部分,代码有两种写法,第一种是新建节点记录:

int merge(int x,int y,int l,int r){
	if(!x||!y) return x+y;//若有一个为0输出另一个值
	int p=++tot;
	tree[p].l=l;tree[p].r=r;tree[p].sum=tree[x].sum+tree[y].sum;//合并
	int mid=(l+r)>>1;
	tree[p].ls=merge(tree[x].ls,tree[y].ls,l,mid);
	tree[p].rs=merge(tree[x].rs,tree[y].rs,mid+1,r);//递归继续合并
	return p;//返回合并后的根节点
}

第二种是在原本基础上合并:

void merge(int &x,int &y,int l,int r){
	if(!x||!y){
		x+=y;//同上
		return;
	}
	if(l==r){
		tree[x].sum+=tree[y].sum;//合并
		return;
	}
	int mid=(l+r)>>1;
	merge(tree[x].ls,tree[y].ls,l,mid);
	merge(tree[x].rs,tree[y].rs,mid+1,r);
	push_up(x);//更新原本x的值
}

那么例题的思路就很简单了:

  • 对于每一个节点建一个权值线段树
  • 合并操作即线段树合并
  • 查询第 \(k\) 大在线段树上二分即可。
#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
#define INF 1e12
using namespace std;
const int N=1e5+5;
int n,m,tot,q;
struct st{
	int ls,rs,l,r,sum;
}tree[N<<6];
int p[N],fa[N],root[N],rk[N];
int find(int x){
	if(fa[x]==x) return x;
	return fa[x]=find(fa[x]);
}
int insert(int x,int l,int r){//动态开点建树
	int p=++tot;
	tree[p].l=l;tree[p].r=r;tree[p].sum=1;
	if(l==r) return p;
	int mid=(l+r)>>1;
	if(x<=mid) tree[p].ls=insert(x,l,mid);
	else tree[p].rs=insert(x,mid+1,r);
	return p;
}
int query(int p,int k){//线段树上二分
	if(tree[p].sum<k) return -1;
	if(tree[p].l==tree[p].r) return rk[tree[p].l];
	if(tree[tree[p].ls].sum>=k) return query(tree[p].ls,k);
	else return query(tree[p].rs,k-tree[tree[p].ls].sum);
}
int merge(int x,int y,int l,int r){
	if(!x||!y) return x+y;
	int p=++tot;
	tree[p].l=l;tree[p].r=r;tree[p].sum=tree[x].sum+tree[y].sum;
	int mid=(l+r)>>1;
	tree[p].ls=merge(tree[x].ls,tree[y].ls,l,mid);
	tree[p].rs=merge(tree[x].rs,tree[y].rs,mid+1,r);
	return p;
}
void Merge(int x,int y){//合并
	int xx=find(x),yy=find(y);
	if(xx==yy) return;
	fa[xx]=yy;
	root[yy]=merge(root[xx],root[yy],1,n);
}
signed main(){
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	ios::sync_with_stdio(false);
#define da cin
#define na cout
#define lv tie(0)
	da.lv;
	na.lv;
#undef da
#undef na
#undef lv
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>p[i];
		fa[i]=i;
		rk[p[i]]=i;	
		root[i]=insert(p[i],1,n);
	}
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v;
		Merge(u,v);
	}
	cin>>q;
	while(q--){
		char op;
		int x,y;
		cin>>op>>x>>y;
		if(op=='B'){
			Merge(x,y);
		}
		else{
			cout<<query(root[find(x)],y)<<"\n";
		}
	}
	return 0;
}

习题

[HNOI2009]梦幻布丁

\(N\) 个布丁摆成一行,进行M次操作.每次将某个颜色的布丁全部变成另一种颜色的,然后再询问当前一共有多少段颜色.例如颜色分别为 \(1,2,2,1\) 的四个布丁一共有 \(3\) 段颜色.


每个颜色建一颗线段树,算每一段该颜色的数量,以及左右两边是否填了改颜色。

\(x\) 变成 \(y\) 则是将 \(x\) 合并到 \(y\) 的线段树上。

线段树合并左右儿子类似于最大子段和合并即可。

[ONTAK2010] Peaks
在Bytemountains有 \(N\) 座山峰,每座山峰有他的高度 \(h_i\)
有些山峰之间有双向道路相连,共 \(M\) 条路径,每条路径有一个困难值,这个值越大表示越难走,
现在有 \(Q\) 组询问,每组询问询问从点 \(v\) 开始只经过困难值小于等于 \(x\) 的路径所能到达的山峰中第 \(k\) 高的山峰,如果无解输出 \(-1\)


类似于永无乡,将操作离线下来按 \(x\) 排序,根据询问依次加边。

加边即合并两端点的线段树,查询则线段树上二分查 \(k\) 大。

「POI2011」旋转树木 Tree Rotations

给定一颗有 \(n\)叶节点的二叉树。每个叶节点都有一个权值 \(p_i\)(注意,根不是叶节点),所有叶节点的权值构成了一个 \(1 \sim n\) 的排列。
对于这棵二叉树的任何一个结点,保证其要么是叶节点,要么左右两个孩子都存在。
现在你可以任选一些节点,交换这些节点的左右子树。
在最终的树上,按照先序遍历遍历整棵树并依次写下遇到的叶结点的权值构成一个长度为 \(n\) 的排列,你需要最小化这个排列的逆序对数。


建权值线段树,不断合并左右两子树。

合并前的逆序对树:左子树的线段树右儿子的值 \(\times\) 右子树的线段树左儿子的值。

合并前的逆序对树:左子树的线段树左儿子的值 \(\times\) 右子树的线段树右儿子的值。

两者取 \(min\) 相加答案即可。

inline void merge(int &x,int &y,int l,int r){
	if(!x||!y){
		x+=y;
		return;
	}
	if(l==r){
		tree[x].sum+=tree[y].sum;
		return;
	}
	int mid=(l+r)>>1;
	p+=tree[tree[x].rs].sum*tree[tree[y].ls].sum;//合并前
	q+=tree[tree[x].ls].sum*tree[tree[y].rs].sum;//合并后
	merge(tree[x].ls,tree[y].ls,l,mid);
	merge(tree[x].rs,tree[y].rs,mid+1,r);
	push_up(x);
}
inline int solve(){
	int x,rt=0;
	x=read();
	if(x!=0){
		insert(rt,1,n,x);
		return rt;
	}
	int ls=solve();
	int rs=solve();
	p=q=0;
	merge(ls,rs,1,n);
	ans+=min(p,q);//算答案
	return ls;
}
posted @ 2025-12-21 17:20  Azarole  阅读(12)  评论(0)    收藏  举报