[日常]线段树空间
线段树空间到底该开多大?(转载自luogu @小粉兔)
这里的线段树指的是,每个节点都有00或22个孩子的二叉树。
编号为xx的节点的左孩子(如果存在的话)的编号为2x2x,右孩子的编号为2x+12x+1。而根节点的编号一般为11。
一棵拥有xx个叶子节点的线段树被称为xx-线段树。
除此之外,一棵xx-线段树(x>1)(x>1)还需要满足其左子树为一棵\lfloor x/2\rfloor⌊x/2⌋-线段树,其右子树为一棵\lceil x/2\rceil⌈x/2⌉-线段树。
有上述条件可以唯一确定一棵xx-线段树(x>0)(x>0)的形态。
接下来要探讨的是:一棵nn-线段树的节点中的最大的编号id_{\max}idmax,与nn有什么关系。因为最大的编号和在程序中实现时所用的数组大小以及使用的内存大小有密切的联系。
有人认为id_{\max}idmax小于4n4n,有人认为id_{\max}idmax不会超过5n5n。有人提出可以证明id_{\max}\leq 3n+1idmax≤3n+1。
真相到底是什么?
① 线段树的高度和形态
11-线段树高度为11。
22-线段树高度为22。
3,43,4-线段树高度均为33。
5\sim 85∼8-线段树高度均为44。
9\sim 169∼16-线段树高度均为55。
可以看出,一棵xx-线段树的高度为\lceil \log_2x\rceil +1⌈log2x⌉+1或等于\lceil \log_2 2x\rceil⌈log22x⌉。
根据此,结合二叉树的简单性质,我们可以得到一个线段树id_{\max}idmax的比较松的上估计:id_{\max}\leq 2^{\lceil \log_2 2x\rceil}-1< 4n-1\leq 4n-2idmax≤2⌈log22x⌉−1<4n−1≤4n−2。证实了id_{\max}idmax小于4n4n的断言。
而观察线段树的形态可以得出另一个结论:一棵线段树除了最后一层,其他层都是满的。
据此,有人提出了另一个声称更紧的估计:2^{\lceil \log_2 x\rceil}+n-12⌈log2x⌉+n−1。并据此计算得出id_{\max}\leq 3n+1idmax≤3n+1的结论。然而这是错误的。因为这个估计依赖于一个错误的假设:线段树最后一层的节点是靠左连续的,或称“完全二叉树”的形态。
实际上,在n=36n=36时,nn-线段树的id_{\max}idmax就等于113>3n+1113>3n+1。这是不满足估计的最小的nn。
那么什么界才是精确的呢?线段树的数组到底要开多大呢?
② 线段树的结构
只要知道了nn-线段树的nn值,就能确定线段树的形态。只要知道了线段树的根节点编号,就能确定线段树的id_{\max}idmax是多少。
那么任何快速地计算id_{\max}idmax呢?考虑以下算法:
若线段树为11-线段树,那么其id_{\max}idmax为根节点编号。
若线段树为xx-线段树(x>1,x\equiv 0(mod\;2))(x>1,x≡0(mod2)),且根节点编号为yy,那么其id_{\max}idmax为根节点编号为2y+12y+1的x/2x/2-线段树的id_{\max}idmax。
若线段树为xx-线段树(x>1,x\equiv 1(mod\;2))(x>1,x≡1(mod2)),根节点编号为yy,且(x+1)/2(x+1)/2-线段树的深度等于(x-1)/2(x−1)/2-线段树的深度,那么其id_{\max}idmax为根节点编号为2y+12y+1的(x-1)/2(x−1)/2-线段树的id_{\max}idmax。
若线段树为xx-线段树(x>1,x\equiv 1(mod\;2))(x>1,x≡1(mod2)),根节点编号为yy,且(x+1)/2(x+1)/2-线段树的深度不等于(x-1)/2(x−1)/2-线段树的深度,那么其id_{\max}idmax为根节点编号为2y2y的(x+1)/2(x+1)/2-线段树的id_{\max}idmax。
不难证明上述算法可以在\Theta(\log_2 n)Θ(log2n)的时间复杂度内计算出正确的结果。
这是一个可接受的算法,接下来我用 c++ 语言实现了计算id_{\max}idmax的函数,并用它计算了nn与id_{\max}idmax的关系:
1 #include<cstdio> 2 int Dep[100000001]; 3 int max(int i,int j){return i>j?i:j;} 4 void Init(){ 5 Dep[1]=1; 6 for(int i=2;i<=100000000;++i) 7 Dep[i]=max(Dep[i>>1],Dep[i+1>>1])+1; 8 } 9 int GetNum(int RootNum,int Size){ 10 if(Size==1) return RootNum; 11 if(!(Size&1)) return GetNum(RootNum<<1|1,Size>>1); 12 if(Dep[Size>>1]!=Dep[Size+1>>1]) 13 return GetNum(RootNum<<1,Size+1>>1); 14 return GetNum(RootNum<<1|1,Size>>1 ); 15 } 16 int main(){ 17 Init(); 18 double Max=0; 19 for(int i=1;i<=1000000;++i){ 20 int id=GetNum(1,i); 21 double R=1.*id/i; 22 if(R>Max) Max=R; 23 // printf(" # %d : %d , %.15lf\n",i,id,R); 24 } 25 printf("%.15lf\n",Max); 26 return 0; 27 }
输出的结果是3.992197027439024
,这意味着在1000000(10^6)1000000(106)内,id_{\max}idmax最大能达到3.992\cdots3.992⋯倍的nn那么大。这的确不超出我们的预期。
所以结论是什么?线段树的数组到底要开多大?
结论就是:线段树开44倍总没错。你也可以算出一个nn-线段树(n\leq k\times 10^5)(n≤k×105)的id_{\max}idmax最大能到多少。更简单且内存占用不大的方法是算出第一个比2n2n大的22的次幂,并把数组开到那么大。