【数据结构】线段树

今天来仔细地说一下线段树

线段树可以高效率地解决许许多多的区间操作

比如区间求和,把一个区间中所有的数加上常量k,区间求最大值最小值等等

定义

  • 线段树是一个完全二叉树

  • 它在各个节点保存一条线段(数组中的一段子数组)

  • 每个单元区间对应线段树中的一个叶结点

  • 性质:父亲的区间是[l,r],(m=(l+r)/2)左儿子的区间是[l,m],右儿子的区间是[m+1,r],线段树需要的空间为数组大小的四倍

基本操作

这里举的是区间加与区间求和的例子

先列出来
1. 建线段树
2. 区间加
3. 区间查询和


建树

  • 首先,需要一个结构体,来保存一个节点
struct node
{
    int left, right;
    //表示这个节点能够在数组中管辖的线段的范围是从left到right

    int s;
    //表示这条线段的区间和

    node *ch[2];
    //节点的两个孩子,ch[0]为左孩子,ch[1]是右孩子
}pool[MAXN], *root;//pool是内存池,root是根节点
  • 这样就能够用node来表示线段树的节点(但这还不够,详看区间加)
  • 接下来是建树操作
void Build_Tree(node *r, int left, int right)
//递归建树,指以r为根节点建线段为left至right的子树
{
    r->left = left;
    r->right = right;
    //将r->left更新为left,r->right更新为right
    if(left == right)
    //如果递归到了叶子节点
    {
        r->s = a[left];
        //单个叶子节点的区间和就是数组中的那个值
        return ;
    }
    int mid = (left + right) / 2;//详见性质
    node *lson = &pool[++cnt];//申请空间
    node *rson = &pool[++cnt];//申请空间
    r->ch[0] = lson;//让r的左子指向lson
    r->ch[1] = rson;//让r的右子指向rson
    Build_Tree(lson, left, mid);     //递归建树,详见性质
    Build_Tree(rson, mid + 1, right);//递归建树,详见性质
    r->s = r->ch[0]->s + r->ch[1]->s;//维护和
}

区间加

即给定一个区间left,right和一个需要加上的值
完成操作:让left ~ right的所有数加上一个d

  • 暴力操作:一个一个点进行单点修改,复杂度 O ( n log 2 n )
  • 显然复杂度不满足要求
  • 所以,我们换一种方式
  • 在每一个节点中加入一个懒标记,表示这个整个区间每一个数需要加上这个懒标记
  • 比如说要给 [ 1 , 4 ] 加上 5 ,那么就让表示 [ 1 , 4 ] 的区间的 l a z y (懒标记)加上 5
  • 我们需要一个pushdown函数,把一个节点的懒标记转换成区间和并且下发到自己的左右两子

pushdown:

inline void Push(node *r)
{
    if(r->lazy == 0) return ; //没有需要加的值直接return
    r->s += (r->right - r->left + 1) * r->lazy;
    //区间和加上区间的长度(right - left + 1)乘上每个数需要加上的值lazy
    if(r->ch[0]) r->ch[0]->lazy += r->lazy;
    if(r->ch[1]) r->ch[1]->lazy += r->lazy;
    //左右两子的lazy加上自己的lazy
    r->lazy = 0;
}

修改函数:

void change(node *r, int left, int right, int d)
{
    //d是要加上的数
    if(r->left == left && r->right == right)
    //找到了要修改的区间
    {
        r->lazy += d;//把每个数要加的值lazy加上d
        return ;
    }
    Push(r);
    //发放lazy
    if(r->ch[0]->right >= right) change(r->ch[0], left, right, d);
    //如果左子的右端点比要查找的右端点大,则在左子中继续修改
    else if(r->ch[1]->left <= left) change(r->ch[1], left, right, d);
    //如果右子的左端点比要查找的左端点小,则在右子中继续修改
    {
          change(r->ch[0], left, r->ch[0]->right, d);
          change(r->ch[1], r->ch[1]->left, right, d);
          //否则把需要查找的区间砍成两半,分别在左右端点分别修改
    }
    r->s = r->ch[0]->s + r->ch[1]->s
    //因为修改了值,所以需要重新维护s
}

区间和

  • 给你一个区间 [ l e f t , r i g h t ] ,求 i = l e f t r i g h t a i
  • 与区间修改相似,给一个代码
int query(node *r, int left, int right)
{
    Push(r);//最开始Push,免得返回时再算一遍
    if(r->left == left && r->right == right) return r->s;
    if(r->ch[0]->right >= right) return query(r->ch[0], left, right);
    else if(r->ch[1]->left <= left) return query(r->ch[1], left, right);
    else
        return query(r->ch[0], left, r->ch[0]->right) + 
               query(r->ch[1], r->ch[1]->left, right);

}

猜你喜欢

转载自blog.csdn.net/tle666/article/details/79501185