To_Heart—总结——线段树
概念
线段树(Segment Tree)是一种基于分治思想的二叉树结构,用于在区间上进行信息统计。
接下来,我们以查找区间最大值为例,给大家讲解线段树。
现在我们假设一个数组a,以1开头,长度为10。
如果我们要查找a[1]~a[1]的区间最大值,那很好办到,就是a[1]本身。
查找a[1]~a[2]的也很简单,直接比较一下a[1]和a[2]谁更大就好了。
同理,查找a[3]~a[4]直接比较a[3],a[4]即可。
那么如果我们要查找a[1]~a[4]的区间最大值呢?
我们可以从a[1]到a[4]一个一个的比较,但是刚刚我们已经得出了a[1] ~ a[2]以及a[3] ~ a[4]的最大值,所以我们直接比较它们两个就可以了。
那么一样的道理,如果我们想知道a[1] ~ a[10]的区间最大值,就可以先知道a[1] ~ a[5]以及 a[6] ~ a[10]各自的区间最大值,再在它们之间比较,从而得出a[1] ~ a[10] 的区间最大值。
而要求a[1] ~ a[5]的区间最大值,就可以先求a[1] ~ a[3]的,a[4] ~ a[5]的。
就这样类比下去,当我们只需要求一个长度为1的序列的区间最大值时,我们就可以直接用a数组表示出来。
这样我们就可以得到如下图的一颗二叉树。
其中,每个节点中的上层数字表示当前节点的编号,下层用中括号括起来的两个数字表示该节点维护的区间头尾的下标。
那么很容易发现,我们建立这样的一颗树所需的时间是n*logn,但我们发现,之后的每次查询所需要的时间只是logn。
建树
为了更好的理解,我们为a数组赋值。
a={3,6,4,8,1,2,9,5,7,0};
复制后,这棵树也可以表现为下图:
首先我们观察这棵树,任何一个除叶子结点以外的节点都一定有两个儿子,并且左儿子的编号为该节点的2倍,右儿子的编号为该节点的2倍加1。所以我们可以利用这个规律来建树
//Build_Tree(1,1n)
void Build_Tree(int q,int l,int r){ //p为当前需要建立的结点,l为当前需要建立区间的左端点,r则为右端点
t[q].l=l; //左端点等于右端点,即为叶子节点,直接赋值即可
t[q].r=r;
if(l==r){
t[q].val=a[l];
return;
}
int mid=(l+r)>>1; //mid则为中间点,左儿子的结点区间为[l,mid],右儿子的结点区间为[mid+1,r]
Build_Tree(q*2,l,mid); //递归构造左儿子结点
Build_Tree(q*2+1,mid+1,r); //递归构造右儿子结点
t[q].val=t[q*2].val+t[q*2+1].val; //通过左右儿子构造父节点
}
基本操作
接下来,我会向大家介绍线段树最简单的两种操作至于区间修改那就有时间了再来填吧
操作1:单点修改
由上图可知,a[7]的值为9,如果把a[7]改成1,那么那些节点需要更新呢?放图:
可见,被标黄的节点都需要更新。可以发现,所有标黄的节点所表示的区间中均包含我们改变的这个数的下标。
//Change_Tree(1,id,val) 其中id表示改变的数的下标,val表示改变后的值
void Change_Tree(int p,int id,ll val){ //p为当前遍历到的节点,id和val的意思同上
if(t[p].l==t[p].r){ //如果当前这个节点的左端点等于右端点则说明它是叶子结点,可以直接赋值
t[p].val=val;
return;
}
int mid=(t[p].r+t[p].l)>>1; //mid则为中间点,左儿子的结点区间为[l,mid],右儿子的结点区间为[mid+1,r]
if(id<=mid) //如果需要更新的是左儿子,那么就更新右儿子
Change_Tree(p*2,id,val);
else //当前节点不是叶子结点,且左儿子不需要更新,那么一定是右儿子需要更新
Change_Tree(p*2+1,id,val);
t[p].val=t[p*2].val+t[p*2+1].val;
}
操作2:区间查询
在修改后的基础上,我们查询a[2]到a[8]的区间最大值
我们知道线段树的每个结点存储的都是一段区间的信息 ,如果我们刚好要查询这个区间,那么则直接返回这个结点的信息即可。
下图标黄的节点则是我们需要查找的节点。
//Find_Tree(1,l,r) 其中l,r表示所查询区间的头和尾。
ll Find_Tree(int p,int l,int r){ //p表示当前节点编号,l和r意思同上
ll val=0;
if(t[p].l>=l&&t[p].r<=r) //说明当前节点的区间包含要查询区间的一部分,则直接返回这部分的最大值
return t[p].val;
int mid=(t[p].l+t[p].r)>>1; //mid则为中间点,左儿子的结点区间为[l,mid],右儿子的结点区间为[mid+1,r]
if(l<=mid)
val=max(val,Find_Tree(p*2,l,r));//如果左子树和需要查询的区间交集非空
if(mid<r)
val=max(val,Find_Tree(p*2+1,l,r));//如果右子树和需要查询的区间交集非空,因为此条件和上一条件不矛盾,所以不用else
return val;
}