线段树合并


by 0htoAi 于 2021.7.31

早年作品不保证可靠。

1.关于线段树合并:

多棵大小相同的线段树,对应节点相加组成的新线段树。

新线段树既可以单独成树,也可以附在一棵参与合并的旧线段树上。

附1:大多数时候线段树合并都是权值线段树合并。
附2:大多数时候合并都是附在一棵参与合并的线段树上,节约空间。

合并代码如下,表示把线段树2的u2号节点合并到线段树1的u1号节点。

点击查看代码
int Merge(int u1,int u2,int x,int y)
{
	if(!u1||!u2)return u1+u2;
	if(x==y)
	{
		tr[u1].val+=tr[u2].val;
		return u1;
	}
	int mid=x+y>>1;
	tr[u1].ls=Merge(tr[u1].ls,tr[u2].ls,x,mid);
	tr[u1].rs=Merge(tr[u1].rs,tr[u2].rs,mid+1,y);
	tr[u1].val=tr[tr[u1].ls].val+tr[tr[u1].rs].val;
	tr[u2].val=0;
	return u1;
}

合并效果图:

  

2.关于习题

1.[USACO17JAN]Promotion Counting P

题意:

找一棵树上每个节点的子树里有多少个比这个节点权值大的节点。

思路:

从下到上递归建权值线段树,每个节点最开始建立的线段树只包含这个点的权值,也就是只有1个叶子节点有值的权值线段树。
递归每找完一个子节点就将子节点的线段树合并到当前节点上。
找完子节点后的当前节点的线段树所包含的就是这个节点子树的所有节点权值。
再查找这棵树的 \(P_u+1\)\(P_{max}\) 的和就行了。

关键代码:

建树:
点击查看代码
void BuildTree(int &u,int x,int y,int z)
{
	if(u==0)
		u=++tot;
	if(x==y)
	{
		tr[u].x=tr[u].y=x;
		tr[u].val=1;
		tr[u].ls=tr[u].rs=0; 
		return;
	}
	tr[u].x=x;
	tr[u].y=y;
	int mid=x+y>>1;
	if(z>mid)
		BuildTree(tr[u].rs,mid+1,y,z);
	else
		BuildTree(tr[u].ls,x,mid,z);
	tr[u].val=tr[tr[u].ls].val+tr[tr[u].rs].val;
}
建树解释:动态开点,不能用朴素的 $u \times 2$ 代表左儿子,$u \times 2 +1$ 代表右儿子,所以需要记录下每个节点的左右儿子下标 $ls,rs$。 $val$ 表示的是 $sum$ 的意思
查询:
点击查看代码
int Query(int &u,int x,int y)
{
	if(tr[u].x>=x&&tr[u].y<=y)
		return tr[u].val;
	if(tr[u].x==tr[u].y)
		return tr[u].val;	
	int Sum=0;
	int mid=tr[u].x+tr[u].y>>1;
	if(mid>=x&&y>=tr[u].x)
		Sum+=Query(tr[u].ls,x,y);
	if(mid
查询解释:普通的区间和查询。
递归:
点击查看代码
void dfs(int u)
{
	if(root[u]==0)
		root[u]=++tot;
	BuildTree(root[u],1,Maxm,P[u]);
	for(int i=elast[u];i;i=e[i].Next)
	{
		int v=e[i].y;
			dfs(v);
		root[u]=Merge(root[u],root[v],1,Maxm);
	}
	Ans[u]=Query(root[u],P[u]+1,Maxm);
}
递归解释:$root_u$ 是 $u$ 号节点的线段树的根节点编号,起映射作用。

小结:

线段树合并模板题,没有思维难度,第一次打比较考验码力。
注:此题需要离散化

2.[HNOI2012] 永无乡

题意:

动态找连通块第 \(k\) 小。

思路:

每当遇到连通块,第一时间想到并查集维护连通性。
同样对于每一个岛屿建立权值线段树,最开始只包含这个岛屿的重要度。
每次建桥就将桥两头的岛屿所在的的连通块合并。
对于每次询问进行线段树上二分查找,所以还得记录每个节点所包含权值区间的岛屿个数。
(小细节:对于每个询问,输出的是岛屿编号,而不是岛屿重要度,所以要进行重要度与编号的映射。这时再读题:“每座岛都有自己的独一无二的重要度”,感觉别有用心)

关键代码:

并查集:

查找:

点击查看代码
int getfather(int x)
{
	if(father[x]!=x)
		father[x]=getfather(father[x]);
	return father[x];
}
初始化:
点击查看代码
for(int i=1;i<=n;i++)
	father[i]=i;
二分查询:
点击查看代码
int Query(int u,int z)
{
	if(tr[u].val
二分查询解释:已知当前节点有 $val_u$ 个岛,$z$ 如果比初始节点节所包含的岛的数量还多,那肯定无解。这个判断 $-1$ 的操作只会在第一层递归起作用。 如果有解,那就对应2种情况:在左儿子的管辖范围内、在右儿子的管辖范围内。 因为权值是有序的,所以只需要看当前求的 $z$ 是否小于等于 $val_{lson}$ ,即在不在左儿子的管辖区间,以此二分。 **注意:因为 $z$ 记录的是当前区间第 $z$ 大,所以当递归到右儿子时 $z$ 需要减去 $val_{lson}$。**

注意2:输出的是值映射的点的编号 \(Mp_x\)

建桥操作:
点击查看代码
while(q--)
{
	char op;
	int x,y;
	scanf("%s%d%d",&op,&x,&y);
	if(op=='Q')
		printf("%d\n",Query(root[getfather(x)],y));
	else
	{
		int fx=getfather(x),fy=getfather(y);
		father[fy]=fx;
		Merge(root[fx],root[fy],1,n);
	}
}
**注意:合并的是整个连通块,而不是输入的 $x,y$。** #### 小结: 线段树合并还是模板,不过考察了对并查集的运用,很好的模板题。

3.【HNOI2009】梦幻布丁

题意:

动态连续段修改与全局查询。

思路:

对于每一种颜色建一棵线段树,维护当前颜色段数,以及当前区间最左和最右是否为此颜色(\(pushup\) 和合并时用)。
对于修改操作,即为线段树合并,易证任意一种颜色只会被修改一次颜色,所以这个合并是无后效性的。
对于全局查询操作,可以记录一个全局答案 \(ans\),初值为最开始建树后所有颜色的初始段数,在每次合并时实时修改。

关键代码:

建树:
点击查看代码
void BuildTree(int &u,int x,int y,int z)
{
	if(u==0)
		u=++tot;
	if(x==y)
	{
		tr[u].val=1;
		tr[u].ls=tr[u].rs=0; 
		tr[u].Lt=tr[u].Rt=1;		
		return;
	}
	int mid=x+y>>1;
	if(z<=mid)
		BuildTree(tr[u].ls,x,mid,z);
	else
		BuildTree(tr[u].rs,mid+1,y,z);
	tr[u].Lt=tr[tr[u].ls].Lt;
	tr[u].Rt=tr[tr[u].rs].Rt;
	tr[u].val=tr[tr[u].ls].val+tr[tr[u].rs].val-(tr[tr[u].ls].Rt&tr[tr[u].rs].Lt);
}
建树解释:当前区间的此颜色段数为左儿子的颜色段数+右儿子的颜色段数。 但是当左儿子的最右边一段和右儿子最左边一段连在一起成了一段,就需要减去1段。 $Lt,Rt$ 表示节点的左、右端是否有需要维护的颜色,而 $Lt \& Rt$ 值为1就表示需要减去多算的一段。 ##### 合并:
点击查看代码
int Merge(int u1,int u2,int x,int y)
{
	if(!u1||!u2)
		return u1+u2;
	if(x==y)
	{
		tr[u1].val|=tr[u2].val;
		tr[u1].Lt|=tr[u2].Lt;
		tr[u1].Rt|=tr[u2].Rt;
		return u1;
	}
	int mid=x+y>>1;
	tr[u1].ls=Merge(tr[u1].ls,tr[u2].ls,x,mid);
	tr[u1].rs=Merge(tr[u1].rs,tr[u2].rs,mid+1,y);
	
	tr[u1].val=tr[tr[u1].ls].val+tr[tr[u1].rs].val-(tr[tr[u1].ls].Rt&tr[tr[u1].rs].Lt);
	tr[u1].Lt=tr[tr[u1].ls].Lt;
	tr[u1].Rt=tr[tr[u1].rs].Rt;
	return u1;
}

合并解释:模板,最后的 \(pushup\) 操作同建树。

小结:

很妙的思维题,虽然代码比较模板但是思维很精髓。

3.总体小结

线段树合并操作很死板,题目主要考察的是如何建树,合并什么东西。事实上以上的例题思维难度全在线段树表示的是什么上。而且线段树合并是个很难想到的算法,一般遇到这种题不会第一时间想到线段树合并。
不过据说线段树合并很少考,所以只需要掌握思想就行了,况且这个思想并不复杂。