数据结构——线段树

Posted ackers

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构——线段树相关的知识,希望对你有一定的参考价值。

线段树是一种基于分治思想的类似于二叉树的数据结构,一般用于数组的信息统计,相比于树状数组,线段树有着更广阔的应用空间,但是相对的其代码量长,且常数大


一.

首先我们来讲线段树的建树过程,请看下图:

技术分享图片

 

这张图就是线段树的存储结构,我们从最长的区间开始依次分成两部分,每一部分都有一个需要维护的权,建树过程比较简单,代码如下:

inline void build(int l,int r,int rt) //l表示当前的左端点,r表示右端点,rt是当前区间的编号
{
  if(l == r) //当左右端点相同,说明当前的区间是一个点,给予该点一个初值
  {
    scanf("%d",c[rt]);
    return ;
  }
  int m = (l + r) >> 1;
  build(l,m,rt<<1);
  build(m + 1,r,rt<<1|1);
  //分成两个区间继续进行建树操作
  update(rt); //更新当前区间的值
  return ;
}

build(1,n,1);

 

关于上面的update函数,一般是根据你需要维护的条件进行的,下面给出几个例子:

inline void update(int rt)
{
  c[rt] = c[rt<<1] + c[rt<<1|1]; //维护区间和
  c[rt] = std::max(c[rt<<1],c[rt<<1|1]);//维护区间最大值
  c[rt] = std::min(c[rt<<1],c[rt<<1|1]);//维护区间最小值
  return ;
}

现在建树过程大概都了解了,我们开始讲查询和修改操作

 

首先我们先讲单点修改,具体操作就是从根节点开始,每次比较我们插入的位置与中点位置的大小,从而选择左右区间,做法类似于二分,在这里不多讲,代码如下:

inline void modify(int l,int r,int rt,int x,int y) //给x加上y
{
  if(l == r)
  {
    c[rt] += y;
    return ;
  }
  int m = (l + r) >> 1;
  if(x <= m) modify(l,m,rt<<1,x,y);
  else modify(m + 1,r,rt<<1|1,x,y);
  update(rt);
  return ;
}

modify(1,n,1,x,y);

 

单点查询操作的实现在做法上与单点修改一样,代码如下:

inline void query(int l,int r,int rt,int p) //p为我们需要查询的点
{
  if(l == r && l == p)
  {
    ans = c[rt];
    return ;
  }
  int m = (l + r) >> 1;
  if(p <= m) query(l,m,rt<<1,p);
  else query(m + 1,r,rt<<1|1,p);
  return ;
}

query(1,n,1,p);

 

然后我们考虑区间查询操作,我们可以发现,当我们要查询的区间的左端点小于等于中点时,那么它与左区间一定有交集,我们就可以进入左子树查询,相应的,当它的右端点大于中点时,与右区间有交集,我们进入右子树查询,并且当我们当前的区间左端点大于等于查询区间左端点,且右端点小于等于查询区间右端点时,那么这个区间一定在查询区间内,我们就直接用该区间权值维护答案,不必再向下搜索,于是我们得到区间查询的方法,代码如下:

inline void query(int l,int r,int rt,int nl,int nr) //nl,nr为当前需要查询的区间
{
  if(nl <= l &&r <= nr)
  {
    ans += c[rt]; //这里我们用求区间和举例
    return ;
  }
  int m = (l + r) >> 1;
  if(nl <= m) query(l,m,rt<<1,nl,nr);
  if(nr > m) query(m + 1,r,rt<<1|1,nl,nr);
  return ;
}

query(1,n,1,nl,nr);

 


二.

关于区间修改,一个显然的想法是,我们像区间查询那样做,但是在这里有个问题,我们在修改区间的时候,如果整个区间都被覆盖,那么每一个节点都需要更新,复杂度会上升到O(n),因此,我们来考虑如何进行优化,在这里,我们就引入延迟标记,当一个区间l ~ r被修改后,我们需在[l ~ r]这个区间加上一个 延迟标记,当我们在查询的时候,如果当前区间的若干个子树需要修改或查询,我们就检验当前区间是否有延迟标记,然后下发标记,这时我们发现,区间修改的复杂度会降至O(nlogn)

下面给出区间修改和加法延迟标记的实现(这里以区间查询为例):

 

inline void sign(int l,int r,int rt,int v) //延迟标记
{
  c[rt] += (r - l + 1)*v;
  sg[rt] += v;
  return ;
}

inline void push_down(int l,int r,int rt) //下放标记
{
  if(sg[rt])
  {
    int m = (l + r) >> 1;
    sign(l,m,sg[rt]);
    sign(m + 1,r,sg[rt]);
    sg[rt] = 0;
  }
  return ;
}

inline void modify(int l,int r,int rt,int nl,int nr,int v) 
{
  if(nl <= l && r <= nr)
  {
    sign(l,r,rt,v);
    return ;
  }
  push_down(l,r,rt);
  int m = (l + r) >> 1;
  if(nl <= m) modify(l,m,rt<<1,nl,nr,v);
  if(nr > m) query(m + 1,r,rt<<1|1,nl,nr,v);
  return ;
}


inline void query(int l,int r,int rt,int nl,int nr) 
{
  if(nl<=l&&r<=nr)
  {
    ans += c[rt];
    return ;
  }
  push_down(l,r,rt);
  int m = (l + r) >> 1;
  if(nl <= m) query(l,m,rt<<1,nl,nr);
  if(nr > m) query(m + 1,r,rt<<1|1,nl,nr);
  return ;
}

modify(1,n,1,nl,nr,v);
query(1,n,1,nl,nr);

 

值得注意的是,在复杂的线段树操作中可能会出现多种延迟标记,这时需要我们考虑运算顺序,如一个线段树中同时存在着加法标记和乘法标记,运算的顺序不同,结果就可能会不同

关于延迟标记这里给出两道例题:

洛谷 P3372

洛谷 P3373

这两道例题都非常的简单,在这里我给出第一个例题的代码,第二个例题可以参照第一题的做法

#include <cstdio>
#include <cstring>
#include <algorithm>
#define root 1,n,1
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
#define mid (l+r)>>1

const int maxn = 1e6 + 6;
int n,tm;
long long c[maxn];
int sg[maxn];
long long ans;

inline int read()
{
  char ch = getchar();int x = 0,f = 1;
  while(ch<0||ch>9){if(ch == -) f = -1;ch = getchar();}
  while(ch>=0&&ch<=9){x = x*10 + ch -0;ch = getchar();}
  return x*f;
}

inline void update(int rt)
{
  c[rt] = c[rt<<1] + c[rt<<1|1];
  return ;
}

inline void sign(int l,int r,int rt,int v)
{
  c[rt] += 1ll*(r-l+1)*v;
  sg[rt] += v;
  return ;
}

inline void push_down(int l,int r,int rt)
{
  if(sg[rt])
  {
    int m = mid;
    sign(lson,sg[rt]);
    sign(rson,sg[rt]);
    sg[rt] = 0;
  }
  return ;
}

inline void build (int l,int r,int rt)
{
  if(l==r)
  {
    c[rt] = read();
    return ;
  }
  int m = mid;
  build(lson);
  build(rson);
  update(rt);
}

inline void modify(int l,int r,int rt,int nl,int nr,int v)
{
  if(nl<=l&&r<=nr)
  {
    sign(l,r,rt,v);
    return ;
  }
  push_down(l,r,rt);
  int m = mid;
  if(nl<=m) modify(lson,nl,nr,v);
  if(nr>m) modify(rson,nl,nr,v);
  update(rt);
}

inline void query(int l,int r,int rt,int nl,int nr)
{
  if(nl<=l&&r<=nr)
  {
    ans += c[rt];
    return ;
  }
  push_down(l,r,rt);
  int m = mid;
  if(nl<=m) query(lson,nl,nr);
  if(nr>m) query(rson,nl,nr);
  update(rt);
}

int main(int argc, char const *argv[]) 
{
  n = read();
  tm = read();
  build(root);
  for(int i = 1;i <= tm;i ++)
  {
    int flag,x,y,k;
    flag = read();
    if(flag == 1)
    {
      x = read();
      y = read();
      k = read();
      modify(root,x,y,k);
    }
    else
    {
      ans = 0;
      x = read();
      y = read();
      query(root,x,y);
      printf("%lld
",ans);
    }
  }
  return 0;
}

 

以上是关于数据结构——线段树的主要内容,如果未能解决你的问题,请参考以下文章

高级数据结构线段树

数据结构——线段树(C++)

线段树

数据结构线段树笔记2

数据结构--线段树

[数据结构] 主席树初识(理论,代码待补)