您的位置:首页 > 理论基础 > 数据结构算法

线段树的一点总结

2016-07-28 19:37 246 查看
线段树,顾名思义,是根据线段建成的树。每一个节点都可以是线段。对于单点查询,区间查询,单点更新,区间更新都是O(logn)级别的,所以对于大多数区间操作比较大的题,都可以用线段树解决。
0.性质
对于每一个非叶子节点下标为i的节点,它的左儿子的下标必定为I<<1,右儿子的下标必定为I<<1|1.
1.定义

const int MAXN = 100005;//线段最大长度
struct Node{
int l;//区间的左端点
int r;//区间的右端点
int v;//区间存储的数据
}tree[MAXN<<2];//一般是开4倍,但是听大牛说最多只有3点几倍


2.建树
void Buildtree(int i,int left,int right)//当前节点的下标及左右端点的坐标
{
tree[i].l=left;//给左右端点赋值
tree[i].r=right;

if(left==right)
{
scanf("%d",&tree[i].v);//给叶子节点赋值
return ;
}

int mid=(left+right)>>1;
Buildtree(i<<1,left,mid);//折半向左右递归建树
Buildtree(i<<1|1,mid+1,right);
}

3.区间查询
int Query(int i,int left,int right)//当前区间的下标和需要查询的左右端点
{
if(tree[i].l>=left&&tree[i].r<=right)//当当前区间已经被覆盖的时候,直接return节点信息
return tree[i].v;

int mid=(tree[i].l+tree[i].r)>>1;
if(left<=mid)Query(i<<1,left,right);//分别向左右子节点查询
if(mid<right)Query(i<<1|1,left,right);
}


4.单点更新
void Modi(int i,int pos,int v)//当前区间的下标,需要修改的位置以及需要被修改成的值
{
if(tree[i].l==pos&&tree[i].r==pos)//找到该位置,修改然后返回
{
tree[i].v=v;
return ;
}

int mid=(tree[i].l+tree[i].r)>>1;
if(pos<=mid)Modi(i<<1,pos,v);//分别向左右子节点查找
if(mid<pos)Modi(i<<1|1,pos,v);
}

小结:以上就是线段树最基础的实现,可以做到单点更新,区间查询。初学线段树,重点就是要体会递归的思想。

推荐题目:HDU 1166,HDU 2795,HDU 1394,HDU 1754

5.区间更新
这个是初学者的一道坎,学会了这个,才算踏入了线段树的大门。我们需要用到一个标记,又叫LazyTag,看名字就知道,这是一个很懒的标记,那么这个标记是用来干什么的呢?我们稍后再讲。首先我们来讨论一下区间更新的时间复杂度。很容易想到,如果我们把区间看做一个一个的点,那么只需要一个循环遍历所有的点然后对于每一个点单点更新即可,那么时间复杂度是多少呢?对于长度为length的区间更新,这样做的时间复杂度为O(length*logn)如果对于多次长度很大的查询,这样做显然很笨重,那么这个时候我们就需要用到LazyTag了。我们首先在我们最开始定义的结构体中加上一个成员Tag,然后在函数Buildtree里面加上它的初始化,一般的,我们初始化为0,但是对于具体情况还是要具体分析,不能一味的套模板。

void PushDown(int st)
{
if(tree[st].tag!=0)
{
tree[st<<1].v+=tree[st].v;//向左右子节点传递标记
tree[st<<1].tag+=tree[st].tag;
tree[st<<1|1].v+=tree[st].v;
tree[st<<1|1].tag+=tree[st].tag;
tree[st].tag=0;//传递过后,标记被清零
}
}

void Update(int i,int left,int right,int v)
{
if(tree[i].l>=left&&tree[i].r<=right)
{
tree[i].v+=v;//一般做题的时候当前节点的值是要被更新后才打上标记
tree[i].tag+=v;//打上标记
return ;
}

PushDown(i);
int mid=(tree[i].l+tree[i].r)>>1;
if(left<=mid)Update(i<<1,left,right,v);//左右向左右子节点递归
if(mid<right)Update(i<<1|1,left,right,v);
}

推荐题目:HDU 1556,HDU 1698,POJ 3468

6.区间合并
从这个地方开始就有点困难了,一般的线段树只能对给定的区间进行操作,但是如果我们需要查询满足某种需要的最大区间呢?那么我们就需要对具有相同性质的区间进行合并。我们一般是向我们定义的结构体中添加三个成员,lsum,rsum,msum。这三个成员分别表示当前区间从左,右端点开始满足条件的最大长度和整个区间满足条件的最大长度。
对于子节点,我们可以很容易的判断出它是否满足条件,那么我们就从子节点向上更新即可,例如:

void PushUp(int i,int len)//更新的节点下标和更新的区间长度
{
tree[i].lsum=tree[i<<1].lsum;//当前节点的lsum是由左子节点贡献而来
if(tree[i].lsum==(len-(len>>1)))tree[i].lsum+=tree[i<<1|1].lsum;//如果lsum整个区间的长度相同,那么我们需要加上右子节点的lsum
tree[i].rsum=tree[i<<1|1].rsum;
if(tree[i].rsum==(len>>1))tree[i].rsum+=tree[i<<1].rsum;
tree[i].msum=max(tree[i<<1].rsum+tree[i<<1|1].lsum,max(tree[i<<1].msum,tree[i<<1|1].msum))//整个区间的msum就是左右子区间的msum和左子区间的rsum加上右区间的lsum中的最大值
}

当然,区间合并也不是直接套用模板就行的,我们需要对具体问题进行分析,才能确定如何进行区间合并。

推荐题目:HDU 3308,POJ 3667
7.扫描线
这个一般是和离散化联系起来的,想象出一个线,沿着一个方向扫过去。一般都是与离散化的方向垂直。思考清楚然后注意一下数据突变的点就没什么难的了。
(这个东西没有什么固定的套路(可能是因为我太弱不知道),还是需要多写,熟练了就好)

推荐题目:HDU 1542 ,HDU 1828

总结:线段树虽然不算高级数据结构,但是很好用,能够解决很多与区间有关的问题,因为它优秀的时间复杂度,熟练掌握它能加深您对递归和二叉树存储的理解。当然,以上这些并不是线段树的全部,如果您熟练掌握了以上线段树的技巧之后,还意犹未尽的话,您可以去看看《【zkw线段树讲稿】统计的力量-线段树》,在那个里面,我感觉线段树的性质被淋漓尽致地展现出来,十分精彩。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息