b 树查找时间复杂度_线段树合并

2fd0197c0553dff023315ff3521d1a01.png

前置知识:动态开点线段树和权值线段树

动态开点线段树和普通线段树的区别比较明显的就是x的左儿子和右儿子不一定是x<<1 (x*2)与x<<1|1 (x*2+1),而是要另外储存为左儿子与右儿子。

动态开点有的优点,就是空间上有一定的优越性,比如说当你让某个节点继承另一个节点的左儿子或者右儿子的时候,你可以不用新建一棵线段树,直接将该节点的左右儿子赋成那个节点的左右儿子就行了。

线段树合并如果每次都完整建一棵线段树的话,空间爆炸,so我们选择动态开点。

我们知道,普通线段树维护的信息是数列的区间信息,比如区间和、区间最值等等。在维护序列的这些信息的时候,我们更关注的是这些数本身的信息,换句话说,我们要维护区间的最值或和,我们最关注的是这些数统共的信息。

而权值线段树维护一列数中数的个数


一、线段树合并的思想

线段树合并,顾名思义,就是建立一棵新的线段树保存原有的两颗线段树的信息。

如果A有p位置,B没有,新的线段树p位置赋成A,返回 A
如果B有p位置,A没有,新的线段树p位置赋成B,返回 A
如果合并到两棵线段树共有的节点了,按照所需合并A,B,把新线段树上的p位置赋成A,返回 A
递归左子树
递归右子树
更新A (当前节点)
返回 A


这个思想非常简单,代码大概长这样:

int merge(int L,int R,int A,int B) {
	if(!A) return B;
	if(!B) return A;
	if(L==R) {
		tre[A].v=L;
		tre[A].sum+=tre[B].sum;
		tre[A].ans=L;
		return A;
	}
	int mid=L+R>>1;
	tre[A].ls=merge(L,mid,tre[A].ls,tre[B].ls);
	tre[A].rs=merge(mid+1,R,tre[A].rs,tre[B].rs);
	up(A);
	return A;
}

思路easy, 那这么搞的复杂度是多少?


二、复杂度

这样子做时间复杂度取决于重合节点个数,一次最坏复杂度是O(nlogn),因为满二叉树的结点数是O(n),对每个结点进行处理是O(logn),但是实际应用中需要合并的两颗树重合部分一般较少,所以复杂度可以近似看为O(logn)的;

如果用动态开点线段树的话,一次合并只需要合并一条链,所以时间复杂度是O(操作数logn)的

可见,复杂度只与插入点的个数有关,也就是插入次数在1e5左右的题目,线段树合并还是比较稳的。


三、线段树合并的用处

处理一些用其他数据结构不好处理的子树问题


大概是本来你对每个点开一棵线段树然后能维护的东西,线段树合并都能维护,因为他们的本质是一样的

常见的比如说查询子树内出现最多的数,总和最大的数等等


四、一道例题 CF600E Lomsat gelral

这题只要dfs一下,将每个子节点的线段树与父亲节点线段树合并,再将父亲节点的颜色插入该节点对应的线段树,然后直接查询答案就可以了。

code:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll M=1e5+5;
ll n,Mx;
ll C[M],rt[M],tot;
ll ans[M];
vector <ll> E[M];
struct per {
	ll v;
	ll sum,ans;
	ll ls,rs;
} tre[M<<5];

void op(ll a,ll b) {
	tre[a].sum=tre[b].sum;
	tre[a].v=tre[b].v;
	tre[a].ans=tre[b].ans;
}
void up(ll p) {
	ll ls=tre[p].ls,rs=tre[p].rs;
	if(tre[ls].sum>tre[rs].sum) op(p,ls);
	else if(tre[ls].sum<tre[rs].sum) op(p,rs);
	else {
		op(p,ls);
		tre[p].ans+=tre[rs].ans;
	}
}

ll update(ll L,ll R,ll x,ll p) {
	ll nw=p;
	if(!nw) nw=++tot;
	if(L==R) {
		tre[nw].v=L;
		tre[nw].sum++;
		tre[nw].ans=L;
		return nw;
	}
	ll mid=L+R>>1;
	if(x<=mid) tre[nw].ls=update(L,mid,x,tre[nw].ls);
	else tre[nw].rs=update(mid+1,R,x,tre[nw].rs);
	up(nw);
	return nw;
}
ll merge(ll L,ll R,ll A,ll B) {
	if(!A) return B;
	if(!B) return A;
	if(L==R) {
		tre[A].v=L;
		tre[A].sum+=tre[B].sum;
		tre[A].ans=L;
		return A;
	}
	ll mid=L+R>>1;
	tre[A].ls=merge(L,mid,tre[A].ls,tre[B].ls);
	tre[A].rs=merge(mid+1,R,tre[A].rs,tre[B].rs);
	up(A);
	return A;
}
void dfs(ll x,ll fa) {
	for(ll i=0; i<E[x].size(); i++) {
		ll y=E[x][i];
		if(fa==y) continue;
		dfs(y,x);
		rt[x]=merge(1,Mx,rt[x],rt[y]);
	}
	rt[x]=update(1,Mx,C[x],rt[x]);
	ans[x]=tre[rt[x]].ans;
}
int main() {
	scanf("%lld",&n);
	for(ll i=1; i<=n; i++)
		scanf("%lld",&C[i]);
	for(ll i=1; i<=n; i++) {
		Mx=max(Mx,C[i]);
		rt[i]=i;
		tot++;
	}
	for(ll i=1; i<n; i++) {
		ll a,b;
		scanf("%lld%lld",&a,&b);
		E[a].push_back(b);
		E[b].push_back(a);
	}
	dfs(1,1);
	for(ll i=1; i<=n; i++)
		printf("%lld ",ans[i]);
	return 0;
}

easy~~

复杂度O(nlogn)

这道题还有种树上启发式合并的写法,O(nlogn),安利~

hit me​www.zhihu.com
ec0e4ba34477db34d8e5950575fd4e1e.png

五、一些例题

[POI2011]ROT-Tree Rotations

题意:

给你一颗树,只有叶子节点有权值,你可以交换一个点的左右子树,问你最小的逆序对数

题解:

线段树维护权值个个数即可

然后左右子树合并时计算交换和不交换的贡献取一个min即可


[Vani有约会]雨天的尾巴

题意:

首先村落里的一共有n座房屋,并形成一个树状结构。然后救济粮分m次发放,每次选择两个房屋(x,y),然后对于x到y的路径上(含x和y)每座房子里发放一袋z类型的救济粮。
然后深绘里想知道,当所有的救济粮发放完毕后,每座房子里存放的最多的是哪种救济粮。

题解:

树链剖分的写法很明显了,维护一个max即可

讲一下线段树合并的写法

区间更新用单点更新和差分来代替,求一个LCA,x->y的更新即可用在点x+1,点y+1,点lca(x,y)-1,点fa(lca(x,y))-1 后,线段树合并来取代, 线段树维护最多的救济粮编号val,最多救济粮的数量sum,然后在合并的时候就可以统计出u节点的答案了


[noip2016]天天爱跑步

呵呵呵呵~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值