首页 > 技术文章 > 笔记 线段树合并

anyixing-fly 2020-06-27 21:34 原文

前言

近期刚刚学了线段树合并,感觉几天后就忘了,所以写出来方便以后复习
(如有雷同,那就是我在网上借鉴的了。。。。。。。。。)

什么是线段树合并

顾名思义,线段树合并就是把两棵线段树合并到一块,废话,但是首先要考虑一个问题,什么样的线段树可以合并,比如一棵表示区间最大值,另一棵表示区间最小值,那这两棵线段树显然不能合并,于是会发现,大部分维护区间性质(如和,积,最大值,最小值,异或和等等)的线段树一般都不能合并,那么什么样的可以呢————权值线段树。

什么是权值线段树

权值线段树,基于普通线段树,但是不同。
举个栗子:对于一个给定的数组,普通线段树可以维护某个子数组中数的和,而权值线段树可以维护某个区间内数组元素出现的次数。
在实现上,由于值域范围通常较大,权值线段树会采用离散化或动态开点的策略优化空间。单次操作时间复杂度\(O(logn)\)
权值线段树的节点用来表示一个区间的数出现的次数
例如:数1和2,分别出现3次和5次,则\(l==r==1\)的点记录3,\(l==r==2\)的点记录5,1和2的父节点记录它们的和8.

怎么进行线段树的合并

线段树合并主要有两种:

  • 因为是权值线段树,所以可以将对应位置上的\(val\)加起来作为新树
  • 可以用某一棵树覆盖另一棵树作为新树

我还没写到过第二种的题,所以先来看第一种。
例如这道题,[POI2011]ROT-Tree Rotations
先看到前序遍历,可能会有点懵,但是再仔细看的话会发现,只有叶子节点有值,也就是说,前序遍历的深层意思就是,对于每棵子树而言,会先输出左子树然后再输出右子树中的值。
我们还可以再想,逆序对的情况可能会有以下三种

  • 在左子树里
  • 在右子树里
  • 跨越左右两个子树
    我们能进行的操作只有交换左右子树,所以考虑这个操作所能造成的影响,发现它只能对情况三中的逆序对造成影响,情况一和二不会受到影响,所以可以从叶子节点开始向上操作,现在考虑如何快速统计逆序对,不难想到对于每个节点开一棵权值线段树,注意由于开完的线段树内存可能会很大所以需要动态开点,然后每次将左子树和右子树合并覆盖到根节点上,至于逆序对,如果不交换左右子树,那就是左边那棵树的右半边乘上右边那棵树的的左半边的大小;交换的话则是左边那棵树的左半边乘上左边那棵树的的右半边的大小。这么做为什么可以做到不重不漏的枚举所有的逆序对呢?上边已经提到,对根节点的操作不会影响左右子树的操作,对于这棵权值线段树也是一样,每次从底层开始回溯,依次用左子树去匹配右子树。
#include<cstdio>
#include<cstring>
#include<algorithm>
const int lqs=2e5+10;
int cnt,lc[lqs*20],rc[lqs*20],siz[lqs*20];
void update(int &rt,int l,int r,int val){
	if(!rt)rt=++cnt;
	if(l==r){
		siz[rt]++;
		return;
	}
	int mid=l+r>>1;
	if(val<=mid)update(lc[rt],l,mid,val);
	else update(rc[rt],mid+1,r,val);
	siz[rt]=siz[lc[rt]]+siz[rc[rt]];
}
long long tot1,tot2,ans;
int merge(int lt,int rt,int l,int r){
	if(!lt||!rt)return lt+rt;
	if(l==r){
		siz[lt]+=siz[rt];
		return lt;
	}
	tot1+=1ll*siz[lc[lt]]*siz[rc[rt]];
	tot2+=1ll*siz[lc[rt]]*siz[rc[lt]];
	int mid=l+r>>1;
	lc[lt]=merge(lc[lt],lc[rt],l,mid);
	rc[lt]=merge(rc[lt],rc[rt],mid+1,r);
	siz[lt]+=siz[rt];
	return lt;
}
int dfs(){
	int val,p=0;
	scanf("%d",&val);
	if(!val){
		int ll=dfs(),rr=dfs();
		tot1=tot2=0;
		p=merge(ll,rr,1,lqs);
		ans+=std::min(tot1,tot2);
	}else update(p,1,lqs,val);
	return p;
}
int main(){
	int n;
	scanf("%d",&n);
	dfs();
	printf("%lld\n",ans);
}

合并的时间复杂度

来证明一下:
假设我们会加入\(k\)个点,由上面的结论,我们可以推出最多要新增\(klogk\)个点。
而正如我们所知,每次合并两棵线段树同位置的点,就会少掉一个点,复杂度为\(O(1)\),总共\(O(klogk)\)个点,全部合并的复杂度就是\(O(klogk)\)

这个应该很好理解

应用

线段树合并的应用还是很广泛的,比如用于树上差分啊或者动态维护一些东西,做题时会体会到的。

推荐阅读