拿这道题来学了一下线段树合并。
主要是照着这个课件学的,但是他那一句
整个过程的开销不会比向一棵空树顺序插入n个整数来的大
我完全没有看明白。。自己想了很久,终于想明白了。
之前有一句话是很关键的:
合并的开销正比于两棵树的公共节点数
所以我们考虑线段树中一个节点[l,r],它作为公共节点出现的次数是多少呢?显然是将[l,r]中所有元素合并的代价,就是r-l次!所以总的时间复杂度就是
O(nlogn)
。能不能有元素相同呢?能不能支持删除呢?当然是可以的。相同元素和删除元素的情况可以视为让一条链上的节点大小+1,所以显然没有问题。不过方便起见,我们下面讨论没有删除和没有相同元素的情况。
让我们再仔细算一下这个开销。实际上我们还会访问到公共节点的儿子节点,所以我们考虑一下一个节点被访问的次数,其实它等于它父亲节点作为公共节点被访问的次数。因为只要它父亲是公共节点,它就会被访问到;而如果不是的话,它就不会被访问到,所以这两者是等价的。而根节点的被访问到的次数就是n-1。再算上一开始的n棵线段树。我们知道线段树的树高是
⌈log2n⌉+1
,所以总访问次数就是
n−1+2(n⌈log2n⌉−(n−1))+n(⌈log2n⌉+1)=3n⌈log2n⌉+1
。这里算的是一个非常紧的上界,当且仅当
n=2k
时可以取得这个上界。
我们为什么要算这么细呢?首先是为了分析这个玩意儿的常数,注意到它自带3的常数;其次是为了可持久化这个东西,我们知道可持久化是个超级烧内存的奢侈品,所以把内存算细是很有必要的。
但是如果我们不需要可持久化,比如说这道题,我们就可以只开
n(⌈log2n⌉+1)
的内存,合并两棵线段树的时候,直接把一棵合并到另一棵上就可以了。
课件里说什么mle。。我并不是很理解。而且我感觉如果
3n⌈log2n⌉+1
mle了的话,他的搞法应该也会被一棵满二叉树卡掉吧。。
代码:
#include<cstdio>
#include<iostream>
using namespace std;
#include<algorithm>
#include<cstring>
#include<cmath>
const int N=2e5+5;
typedef long long LL;
int n,ptot=2;
void in(int &x){
char c=getchar();
while(c<'0'||c>'9')c=getchar();
for(x=0;c>='0'&&c<='9';c=getchar())x=x*10+(c^'0');
}
const int Log=19;
int size[N*Log],ls[N*Log],rs[N*Log],root[N<<1],ftot=1;
LL cnt[N<<1][2];
void build(int &node,int l,int r,int x){
node=ftot++;
size[node]=1;
//printf("build %d:[%d,%d]\n",node,l,r);
if(l!=r)
if(x<=l+r>>1)build(ls[node],l,l+r>>1,x);
else build(rs[node],(l+r>>1)+1,r,x);
}
void merge(int node,int &u,int v,int ucnt,int vcnt){
if(!u){
u=v;
cnt[node][0]+=ucnt*size[v];
return;
}
if(!v){
cnt[node][1]+=vcnt*size[u];
return;
}
merge(node,rs[u],rs[v],ucnt+size[ls[u]],vcnt+size[ls[v]]);
merge(node,ls[u],ls[v],ucnt,vcnt);
size[u]=size[ls[u]]+size[rs[u]];
}
LL ans;
void dfs(int node){
int x;
in(x);
if(x==0){
int lson,rson;
dfs(lson=ptot++),dfs(rson=ptot++);
root[node]=root[lson];
//printf("---merge(%d,%d)---\n",lson,rson);
merge(node,root[node],root[rson],0,0);
ans+=min(cnt[node][0],cnt[node][1]);
}
else build(root[node],1,n,x);
}
int main(){
in(n);
dfs(1);
cout<<ans<<endl;
}
总结:
①线段树合并的空间开销是:
n(⌈log2n⌉+1)
(不可持久化),
3n⌈log2n⌉+1
(可持久化)。