从零开始的线段树浅谈

从零开始的线段树浅谈

写在前面

其实早就学会了线段树 但是学得特别夹生 所以经常写挂
今天终于立下决心要做一做真正有技术含量的裸题 才发现自己真的没学会什么
调题很虐心 但是收获很多
这里带来线段树讲解以及比较难的裸题

题目链接

简单题可以直接找RMQ的裸题熟悉操作 不进行赘述
这是第一题 相对简单
这是第二题 难度稍大

先说说线段树

线段树是一种数据结构(废话
功能很强大 我们可以用来维护很多的事情
比如说我们可以维护区间和 最大值 最小值以及支持修改的操作
给出一张线段树的图片 这样好说话一些
《从零开始的线段树浅谈》
我们可以看到对于一个数列 我们可以采用如下的方式构造线段树
接下来如果我们要进行操作 针对的都是树上的节点 这样大大减小了我们操作的复杂度
暴力做法我们可以用O(n)的复杂度进行修改并进行预处理 然后用O(1)的复杂度进行查询
但是注意到线段树我们构造的时候采用的是不断二分区间的思想 查询也是从根节点一点一点向下找
所以我们把查询和修改操作的时间复杂度控制在O(logn)。
这样的话我们的复杂度均摊 相对更优
但是美中不足的一点就是 我们在构建线段树的时候 对空间的要求很高
通常都是2n的空间(上图有所展示)
但是有的时候毒瘤出题人卡的话会把线段树卡成大概差不多是一条链
这样的话为保险起见 我们用4n的空间
所以有的时候受到空间的限制 我们需要更高端的操作比如离散化 这个到题目里再分析

接下来是线段树的操作

刚才我已经提到了 对于线段树来讲 我们把数列中的操作转化成为了树上节点的操作
那么第一个出现问题的地方自然是怎么构造这样一棵特别方便的线段树
一般我们的线段树都是通过递归来实现
比如说还是刚才的树
《从零开始的线段树浅谈》
这样的话我们在构建的时候
会不断把区间缩小
(1,10)——(1,5)——(1,3)——(1,2)——(1,1)
当最后区间长度只剩下1的时候 我们会发现非常简单了
这个时候节点只有原数列对应的数 很好操作
这个时候处理完我们按照刚才的区间划分
回溯的思想 送回去
这里我给出一段代码 用维护区间和作为例子
小贴士
当前节点的左儿子标号是他的2倍 右节点标号是2倍加1
我用leftson(ls)rightson(rs)表示

inline void buildtree(ll q,ll l,ll r)//q是当前的节点编号 l,r是节点对应的左右端点
{
  tree[q].begi=l,tree[q].endd=r;
  if(l==r)
  {
    tree[q].sum=a[l];//当只剩下一个数的时候 我们的区间和就是这个数
    return;
  }
  ll mid=(l+r)>>1;
  buildtree(ls,l,mid);
  buildtree(rs,mid+1,r);//递归思想 二分区间继续建树 
  tree[q].sum=tree[ls].sum+tree[rs].sum;//这里是把我们的叶子节点往根节点推的过程
}

接下来就是修改操作了 这个跟建树的思想差不多 我们修改分为区间修改和单点修改
大体的想法就是我们找到需要修改的位置 然后修改之后上传
对于区间修改接下来的例题涉及到 一会再说 这里我给出单点修改代码
为了有普遍性 这次我给出的是维护最小值

inline void change(int q,int x,int y)//在第q个节点把第x个位置的数改成y
{
    if(tree[q].begi==tree[q].endd)//叶子节点 直接修改
    {
        tree[q].xiao=y;
        return;
    }
    int mid=(tree[q].begi+tree[q].endd)>>1;
    if(mid>=x) change(ls,x,y);//这里是二分的思想找x这个位置在哪个儿子那边
    else change(rs,x,y);
    tree[q].xiao=min(tree[ls].xiao,tree[rs].xiao);//别忘了上传
}

查询操作不用我说太多 大体的思路是一样的
还是通过二分找到合适的区间之后再合起来

inline int que(int q,int l,int r)//意义同上
{
    if(tree[q].begi==l&&tree[q].endd==r) return tree[q].xiao;//区间正好符合 返回
    int mid=(tree[q].begi+tree[q].endd)>>1;
    if(r<=mid) return que(ls,l,r);//查找对应区间
    else if(l>mid) return que(rs,l,r);
    else return (min(que(ls,l,mid),que(rs,mid+1,r)));//由于区间被mid分开 所以我们还要再处理一下
}

敲黑板 这里是重点
对于线段树有特别坑爹的标记
这个标记经常用在区间修改的时候 我们通常把它叫做lazymark
这个标记就是线段树最难的部分
因为我们在区间修改的时候 不能只改这一个区间我们维护的值 对于这个区间的每个子区间 我们都要维护
于是乎我们要用到一个标记来存储修改的值
这个标记要不断下传给他的子区间 告诉他的儿子们你们应该怎么修改
这里给出区间加上一个数,维护区间和的lazymark下传方式
这里很难

inline void pushdown(long long q)//q节点的信息传下去
{
  tree[ls].lz+=tree[q].lz;//由于是区间加 所以我们修改的值也要叠加起来
  tree[ls].sum+=(tree[ls].endd-tree[ls].begi+1)*tree[q].lz;//区间里的每一个数都加上lazy 一共加了这么多
  tree[rs].lz+=tree[q].lz;
  tree[rs].sum+=(tree[rs].endd-tree[rs].begi+1)*tree[q].lz;
  tree[q].lz=0;//下传结束 使命完成 清空
}

两道模板题

上面是题目链接
第一题 一句话题意:维护区间和
支持操作:区间加 和 询问区间和
直接给出代码 操作已经都提到了
区间的修改我会以注释的方式呈现

#include <cmath>
#include <queue>
#include <cstdio>
#include <iomanip>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 100001
#define M 400001
#define ls q<<1
#define rs q<<1|1//为了方便我定义儿子 上文提及
#define ll long long
using namespace std;
struct treenode
{
  long long begi,endd,lz,sum;
}tree[M];//每个节点的左右端点,区间和和标记
long long a[N],n,m;
inline void buildtree(long long q,long long l,long long r)
{
  tree[q].begi=l;tree[q].endd=r;
  if(l==r)
  {
    tree[q].sum=a[l];
    return;
  }
  long long mid=(l+r)>>1;
  buildtree(ls,l,mid);
  buildtree(rs,mid+1,r);
  tree[q].sum=tree[ls].sum+tree[rs].sum;//建树不进行过多赘述
}
inline void pushdown(long long q)
{
  tree[ls].lz+=tree[q].lz;
  tree[ls].sum+=(tree[ls].endd-tree[ls].begi+1)*tree[q].lz;
  tree[rs].lz+=tree[q].lz;
  tree[rs].sum+=(tree[rs].endd-tree[rs].begi+1)*tree[q].lz;
  tree[q].lz=0;
}//下传操作跟刚才一样
inline void modify(long long q,long long l,long long r,long long v)
{
  if(tree[q].begi==l&&tree[q].endd==r)
  {
    tree[q].sum+=(r-l+1)*v;
    tree[q].lz+=v;//如果区间正好相符 我们不但要修改这个区间的和 而且要改lazymark 因为我们加的值要记录下来
    return;
  }
  pushdown(q);//不相符的话我们就要让这个节点告诉他所有的儿子去修改
  long long mid=(tree[q].begi+tree[q].endd)>>1;
  if(mid>=r) modify(ls,l,r,v);//区间在左儿子
  else if(l>mid) modify(rs,l,r,v);//区间在右儿子
  else modify(ls,l,mid,v),modify(rs,mid+1,r,v);//区间被分开 左右儿子都要修改
  tree[q].sum=tree[ls].sum+tree[rs].sum;//别忘了把修改的结果上传
}
inline long long query(long long q,long long l,long long r)
{
  if(tree[q].begi==l&&tree[q].endd==r) return tree[q].sum;
  pushdown(q);//仍然区间不相符要下传
  long long mid=(tree[q].begi+tree[q].endd)>>1;
  if(mid>=r) return query(ls,l,r);
  else if(l>mid) return query(rs,l,r);
  else return query(ls,l,mid)+query(rs,mid+1,r);//区间在中间我们只能分两次查询然后加起来了
}
inline long long read(){
    char ch=getchar();long long sum=0,w=1;
    while(!(ch>='0'&&ch<='9')) {if(ch=='-') w=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')sum=sum*10+ch-48,ch=getchar();
    return sum*w;
}
int main()
{
  n=read(),m=read();
  for(long long i=1;i<=n;i++) a[i]=read();
  buildtree(1,1,n);
  for(long long i=1;i<=m;i++)
  {
    long long opt=read();
    if(opt==1)
    {
      long long e,f,g;
      e=read(),f=read(),g=read();
      modify(1,e,f,g);
    }
    else
    {
      long long e,f;
      e=read(),f=read();
      printf("%lld\n",query(1,e,f));
    }
  }
}

小结:这个题的难度在于区间修改的标记打法
难度适中 比较适合初学者

第二题 一句话题意
比第一题多了一个区间乘法
就是、每个数乘上一个数
询问是对于一个p取模
其他不变

正解也比较好想 就是我们需要维护两个标记就好了
然后我们对于乘法的把初值记为1 切记!!!不是0
加法跟刚才一样
然后对于两种操作 lazymark具有优先级问题
乘法影响加法 加法不影响乘法(小学学的四则运算)
然后下传比刚才麻烦 毕竟涉及到了两个标记
大家可以自己先操作一下然后得到下传规律
这里我给出标程

#include <cmath>
#include <queue>
#include <cstdio>
#include <iomanip>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 100001
#define ll long long
#define ls q<<1
#define rs q<<1|1
using namespace std;
#define M 400001
struct treenode
{
  ll begi,endd,lzadd,lzmul,sum;//lzadd 加法标记 lzmul 乘法标记
}tree[M];
ll n,m,p,a[N];
inline void buildtree(ll q,ll l,ll r)
{
  tree[q].begi=l,tree[q].endd=r;
  tree[q].lzadd=0;
  tree[q].lzmul=1;//乘法一定要赋初值 不然乘起来全是0
  if(l==r)
  {
    tree[q].sum=a[l]%p;//中间取模不然爆类型 后文一样 不进行赘述
    return;
  }
  ll mid=(l+r)>>1;
  buildtree(ls,l,mid);
  buildtree(rs,mid+1,r);
  tree[q].sum=(tree[ls].sum+tree[rs].sum)%p;
}
inline void pushdown(ll q)
{
  tree[ls].lzmul=(tree[ls].lzmul*tree[q].lzmul)%p;//这里就是我说的标记的下传 乘法和加法之间 对于区间和的关系
  tree[ls].lzadd=(tree[ls].lzadd*tree[q].lzmul+tree[q].lzadd)%p;
  tree[ls].sum=(tree[ls].sum*tree[q].lzmul+tree[q].lzadd*(tree[ls].endd-tree[ls].begi+1))%p;
  tree[rs].lzmul=(tree[rs].lzmul*tree[q].lzmul)%p;
  tree[rs].lzadd=(tree[rs].lzadd*tree[q].lzmul+tree[q].lzadd)%p;
  tree[rs].sum=(tree[rs].sum*tree[q].lzmul+tree[q].lzadd*(tree[rs].endd-tree[rs].begi+1))%p;
  tree[q].lzadd=0,tree[q].lzmul=1;//完成使命之后乘法一定还是1
}
inline void modify(ll q,ll l,ll r,ll add,ll mul)//区间加上add 乘上mul 
//至于后文每次只进行一个操作 不进行加法就是加0 不乘就是乘1嘛
{
  if(tree[q].begi==l&&tree[q].endd==r)
  {
    tree[q].sum=(tree[q].sum*mul+add*(tree[q].endd-tree[q].begi+1))%p;
    tree[q].lzmul=(mul*tree[q].lzmul)%p;
    tree[q].lzadd=(mul*tree[q].lzadd+add)%p;
    return;
  }
  pushdown(q);//这里仍然考虑的是优先级关系以及及时的下传
  ll mid=(tree[q].begi+tree[q].endd)>>1;
  if(mid>=r) modify(ls,l,r,add,mul);
  else if(mid<l) modify(rs,l,r,add,mul);
  else modify(ls,l,mid,add,mul),modify(rs,mid+1,r,add,mul);
  tree[q].sum=(tree[ls].sum+tree[rs].sum)%p;
}
inline ll query(ll q,ll l,ll r)
{
  if(tree[q].begi==l&&tree[q].endd==r) return tree[q].sum%p;
  int mid=(tree[q].begi+tree[q].endd)>>1;
  pushdown(q);
  if(mid>=r) return query(ls,l,r)%p;
  else if(mid<l) return query(rs,l,r)%p;
  else return (query(ls,l,mid)+query(rs,mid+1,r))%p;
}
inline ll read(){
    char ch=getchar();ll sum=0,w=1;
    while(!(ch>='0'&&ch<='9')) {if(ch=='-') w=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')sum=sum*10+ch-48,ch=getchar();
    return sum*w;
}
int main()
{
  n=read(),m=read(),p=read();
  for(int i=1;i<=n;i++) a[i]=read();
  buildtree(1,1,n);
  for(int i=1;i<=m;i++)
  {
    ll opt=read();
    if(opt==1)
    {
      ll e,f,g;
      e=read(),f=read(),g=read();
      modify(1,e,f,0,g);
    }
    else if(opt==2)
    {
      ll e,f,g;
      e=read(),f=read(),g=read();
      modify(1,e,f,g,1);
    }
    else {ll e,f;
      e=read(),f=read();printf("%lld\n",(query(1,e,f)%p+p)%p);}//防止出现负数的常用技巧
  }
}

写在最后

线段树是一种非常常用 用途很广泛的数据结构
不算难但是内容很多 用起来很灵活
我觉得精髓还是多做题
理解lazymark是重中之重
至于其他的优化神马的就不多说了
总之学好线段树 肯定是受益匪浅的
有疑问可以评论留言神马的 我们一起沟通学习
各种神犇请绕路
我这种蒟蒻真的是太菜了

    原文作者:B树
    原文地址: https://blog.csdn.net/Michael_Byrant/article/details/79156549
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞