线段树(从入门到入坟)

其实网上已经有很多很好的博客了,我在写这篇文章前也看了不少(为什么说是看了不少,可能是因为这些博主对线段树的掌握已经很透彻了,他们代码的写法追求的是尽量从简,可能导致我们这些初学者不好了解,故我打算写一篇博客站在我们初学者的角度来写)
此博客可能借鉴以下几篇博客比较经典的图片或代码以及思路;
{https://www.cnblogs.com/jason2003/p/9676729.html
https://blog.csdn.net/huangzihaoal/article/details/81813454
https://www.jianshu.com/p/5769dcb06221
https://www.xuebuyuan.com/3236946.html
}
1.为什么需要线段树 ?
学过"分块"的同学应该知道,很多线段树的问题都可以转变成分块来做;
但我们为什么还需要线段树了,因为分块的复杂度比起线段树来还是很高的,并且分块的操作远不如线段树灵活,因此人们发明了线段树

2.所谓树,第一步当然是建树(这里我们指的都是二叉树),如下下图所示在这里插入图片描述

  • 这里我们假设有8个变量,每一个叶子节点存储一个变量,因为它们左右端点相等,如八号节点,它存储的就是第一个变量,4号结点(如果我们是要求区间和的话)储存的就是8号结点(1号变量)和9号结点(2号变量的和),也就是它的左右儿子之和;我们假设一个结点编号是x,从图中不难看出其左结点编号为2x(也就是x<<1),右结点为2x+1(也就是x<<1|1),那么我们计算a[x]时就可以用**a[x]=a[2x]+a[2x+1]**来计算;
  • 对于2个儿子的边界,从图中不难看出; 如下左儿子的左边界就是父亲的左边界,右儿子的右边界就是父亲的右边界,左儿子的右边界就是父亲的左右边界之和除2,右儿子的左边界自然就是左儿子边界➕1
  • 代码实现如下`
struct node
{int left,right;//表示左右边界
 int mid;//表示中点(left+right)/2
 int ans;//表示权值  
    
}tree[20];

void build(int l,int r,int i)//分别表示左边界,右边界,和该节点编号
{tree[i].left=l;
 tree[i].right=r;
 tree[i].mid=(l+r)/2;
 if(l==r)//说明找到了叶子结点
 {tree[i].ans=(附的初值)
  return
  //如果初值不是固定的,这里我们以输入一串数为例子
  int x;
  cin>>x;
  tree[i].ans=x;//假设我们要输入1 2 3 4 5 ,这里不用担心输入后存入叶子节点后顺序会打乱,因为叶子结点从左到右编号是在增加的,小编号的节点一定比大编号的结点先输入,故在叶子结点中,依旧保持顺序 
     
 }
 build(l,tree[i].mid,i*2);//没找到叶子结点,找左儿子
 build(tree[i].mid+1,r,i*2+1);
 tree[i].ans=tree[i*2].ans+tree[i*2+1];//假设考虑的是求和问题
}`

很多人肯定会存在疑问,为什么开这么大个空间去存,可能就是花内存换时间吧,毕竟时间比内存值钱啊
2.单点修改,区间查询(如果是单点查询就没必要用线段树了)
*这里主要是递归思想,还是上面的图,假设我们要修改2号变量的值在这里插入图片描述
我们就会按如下线路找到2号变量,
在这里插入图片描述
然后我们又要按如下线路返回,并修改沿路因二号变量改变而需要改变的值
代码如下

void updata(int l,int r,int i,int x)//代表左右,和节点编号,x是要修改的元素编号
{if(l==r&&r==x)//找到这个点
      {
          tree[i].ans=x;
          return ;
      }
 if(x<=tree[i].mid) updata(l,tree[i].mid,i*2,x);//在左孩子
 else  updata(tree[i].mid+1,r,i*2+1,x);//在右孩子  
 
tree[i].ans=tree[2*i].ans+tree[i*2+1].ans;//执行这个时说明递归已经到底(也就是它以前的节点已经修改完了),回溯到了这里
}

剩下的就是查询,代码如下

int ans;//假设是最后的答案
void query(int l,int r,int i)//l~r代表要查询的区间,i代表节点号
{if(tree[i].l==l&&tree[i].r==r)//找到目标区间
   {
       ans+=tree[i].ans;不能用等于,因为要找的区间可能被分成了几个小区间
       return ;
   }
if(tree[i*2].r>=l)//左儿子的右边界比要找的区间左边界大 肯定存在交集 
    query(l,r,i*2);//如果这个区间的左儿子和目标区间又交集,那么搜索左儿子 
if(tree[i*2+1].l<=r)
    query(l,r,i*2+1);
}

这样 ans就是最后的结果;

*接下来的区间修改,单点查询和区间修改和区间查询我们会引入懒标记(延迟标记),因为懒是贬义词,而它的存在却提高了程序效率,感觉很对不起它,故我喜欢称它为可爱标记
在这里插入图片描述
*首先我们说明为什么需要可爱标记,假设我们修改了[5,8]的变量的值,使它们全部加一,而某一次我们的访问需要的是[4,8]的合我们就只需要3号节点和11号节点的值,如果我们在修改[5,8]时只改3号节点,效率就会高很多,但如果我们把3号节点的孩子节点,孙子结点全部修改,效率就会降低,但我们根本不需要这些结点,所以我们就没必要修改,所以我们引入了可爱标记,使得我们只修改我们需要的节点,效率就会高很多
*实现思路(重点):
a.原结构体中增加新的变量,存储这个可爱标记。
b.递归到这个节点时,只更新这个节点的状态,并把当前的更改值累积到标记中。
c.什么时候才用到这个可爱标记?当需要递归这个节点的子节点时,标记下传给子节点。这里不必管用哪个子节点,两个都传下去。
d.下传操作:
3部分:
①当前节点的可爱标记累积到子节点的可爱标记中。
②修改子节点状态。(具体怎么修改根据具体情况而定)
③父节点可爱记清零。这个可爱标记已经传下去了,不清零后面再用这个可爱标记时会重复下传影响结果。(可爱标记实现思路取至可爱的鲁学姐)
下面是代码


struct node
{int l,r,ans,mid;//意义和前面一样
 int cute;//可爱标记
    
}tree[20];

void updata(int l,int r,int x,int i)//假设是要将l~r号变量的值改为x,现在在i号节点,然后求区间和
{ if(tree[i].l==l&&tree[i].r)//找到要修改的区间
     {
         tree[i].ans=(l-r+1)*x;//l到r有(l-r+1)个变量 ,故区间和为(l-r+1)*x
         tree[i].cute=x;//可爱标记
         return ; 
    }
  if(tree[i].cute)//如果可爱标记不为0
    cute(i);
  updata(l,tree[i].mid,x,i<<1);//这些操作和之前一样
  updata(tree[i].mid+1,r,x,i<<1|1);
  tree[i].ans=tree[i<<1].ans+tree[i<<1|1].ans;  
}
void cute(int i)
{   tree[i<<1].ans=(tree[i<<1].r-tree[i<<1].l+1)*tree[i].cute;//通过可爱标记修改2个孩子的值
    tree[i<<1|1].ans=(tree[i<<1|1].r-tree[i<<1|1].l+1)*tree[i].cute;
    tree[i<<1].cute=tree[i].cute;
    tree[i<<1|1].cute=tree[i].cute;//把可爱标记传给2个孩子
    tree[i].cute=0;//自己的清零
    
}

无论是小查询某个区间的和,还是某个固定变量的值,按之前的方法查询即可;
接下来就是比较困难的了,我会尽量比较详细;
我们先看几个问题:
1.如果我们要求区间和,父节点的和自然是左孩子的合和右孩子的合;
2.我们看01序列的最长连续零——只知道左右区间的最长连续零,没法知道总的最长连续零;如00010和00010 ;
这时我们就要引入区间合并的概率;
*这里我们以01序列的最长连续零为例子;在构造结构体时会有所不同
这里我们用代码说明;

struct node
{
int l,r,mid;//这些意义不变
int la,ra,ans;//分别表示从右边开始最长的0序列,从左边开始,和该段最长的0序列


}tree[20];

初始化代码如下,相比于之前也略有改动 代码如下

void build(int l,int r,int i)//初值一般是1,n,1;n为序列长度
{tree[i].l=l;
 tree[i].r=r;
 if(l==r)
 {int x,u;
 cin>>x;//输入0或1
 if(x==0)u=1;
 else u=0;
 tree[i].ans=u;
 tree[i].la=u;
 tree[i].ra=u;
 }


build(l,tree[i].mid,i*2);
build(tree[i].mid+1,r,i*2+1);
tree[i].ans=max(tree[2*i].ans,tree[2*i+1].ans,tree[i*2].ra+tree[i*2+1].la);//因为左孩子的右最大和右孩子的左最大可能组称一个更大的
if(tree[2*i].la==(tree[2*i].l-tree[2*i].r+1))//左孩子的左最大可以延伸到右边界 
 tree[i].la=tree[2*i].la+tree[2*i+1].la//2个左最大就可以拼起来
else//拼不起来,仍然是左儿子的左最大
   tree[i].la=tree[2*i].la;
右最大同样考虑就行
}

剩下的就是修改//假设是单点修改,区间修改引入可爱标记就行

void  update(int x,int t,int i)//x号节点改成t
{
 if(l==r)
 {int u;

 if(t==0)u=1;
 else u=0;
 tree[i].ans=u;
 tree[i].la=u;
 tree[i].ra=u;
 }


build(tree[i].l,tree[i].mid,i*2);
build(tree[i].mid+1,tree[i].r,i*2+1);
tree[i].ans=max(tree[2*i].ans,tree[2*i+1].ans,tree[i*2].ra+tree[i*2+1].la);//因为左孩子的又最大和右孩子的左最大可能组称一个更大的
if(tree[2*i].la==(tree[2*i].l-tree[2*i].r+1))//左孩子的左最大可以延伸到右边界 
 tree[i].la=tree[2*i].la+tree[2*i+1].la//2个左最大就可以拼起来
else//拼不起来,仍然是左儿子的左最大
   tree[i].la=tree[2*i].la;
右最大同样考虑就行
}

最后查询遵照区间查询即可;
下面给出几个例题:
题目直通车
题解链接
单点修改区间查询;
代码如下

#include<iostream>
#include<stdio.h>
#include<math.h>
using namespace std;
   
const int maxn = 200000 + 10;
int a[maxn], s[4 * maxn];
int n, q;
   
void build(int l, int r, int rt)//建树树
 {
  if(l == r) {
    s[rt] = a[l + 1] - a[l];
    return;
  }
  int mid = (l + r) / 2;
  build(l, mid, 2 * rt);
  build(mid + 1, r, 2 * rt + 1);
  s[rt] = max(s[2 * rt], s[2 * rt + 1]);
}
   
void update(int pos, int val, int l, int r, int rt)//如同单点修改
 {
  if(l == r) {
    s[rt] = val;
    return;
  }
  int mid = (l + r) / 2;
  if(pos <= mid) update(pos, val, l, mid, 2 * rt);//说明在左边,找左孩子
  else if(pos > mid) update(pos, val, mid + 1, r, 2 * rt + 1);
  s[rt] = max(s[2 * rt], s[2 * rt + 1]);//递归回溯的时候修改值
}
   
int main() {
  while(~scanf("%d", &n)) {
    for(int i = 1; i <= n; i ++) {
      scanf("%d", &a[i]);
    }
    build(1, n - 1, 1);
    scanf("%d", &q);
    while(q --) {
      int x, y;
      scanf("%d%d", &x, &y);
      a[x] = y;
      if(x != 1) update(x - 1, a[x] - a[x - 1], 1, n - 1, 1);
      if(x != n) update(x, a[x + 1] - a[x], 1, n - 1, 1);
      printf("%d.00\n", s[1]);
    }
  }
  return 0;
}

hdu1698
题目链接
题解好像写的不详细,可以参考其他人的博客;
区间修改,区间查询

#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
const int N=100000;

struct node
{int l,r,sum;
  int mid;

}tree[4*N];
int yan[4*N];//因为每次修改我们不可能等找到了叶子节点再进行修改,故我们需要一个延迟节点
void  build(int l,int r,int i)
{tree[i].l=l;
 tree[i].r=r;
 tree[i].mid=(l+r)/2;
 if(l==r)//叶子节点
 {
   tree[i].sum=1;
   return ;
 }

 build(l,tree[i].mid,i*2);
 build(tree[i].mid+1,r,i*2+1);
 tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;


}

void pushdown(int i)//因为每个节点的权值取决于他的长度故需要一个 len变量
{  if(yan[i]>0)//存在延迟
     {yan[2*i]=yan[i];
      yan[2*i+1]=yan[i];//把延迟传个2个儿子;
      tree[2*i].sum=yan[i]*(tree[2*i].r-tree[2*i].l+1);
      tree[2*i+1].sum=yan[i]*(tree[2*i+1].r-tree[2*i+1].l+1);//对2个儿子进行更新
       yan[i]=0;
       

     }


}



void updata(int l,int r,int x,int i)
{  if(tree[i].r==r&&tree[i].l==l)
    {
        tree[i].sum=x*(tree[i].r-tree[i].l+1);
        yan[i]=x;
        return ;
    }
      pushdown(i);
  if(tree[2*i].l<=l&&r<=tree[2*i].r)//全部在左儿子
    updata(l,r,x,2*i);
  else if(tree[2*i+1].l<=l&&r<=tree[2*i+1].r)//全部在右儿子
    updata(l,r,x,2*i+1);
  else//脚踏2只船
                  

    {updata(l,tree[i].mid,x,2*i);
     updata(tree[i].mid+1,r,x,2*i+1);


    }

 tree[i].sum=tree[2*i].sum+tree[2*i+1].sum;//因为他的儿子的权值更新,所以他的权值也要更新

}



int main()
{ int t;
  cin>>t;
  int u=0;
  while(t--)
  {int n,q;
   scanf("%d%d",&n,&q);
    memset(yan,0,sizeof(yan));
    build(1,n,1);

   int x,y,z;
   for(int j=1;j<=q;j++)
        {scanf("%d%d%d",&x,&y,&z);
         updata(x,y,z,1);//从根节点开始找

        }

      cout<<"Case "<<++u<<": The total value of the hook is "<<tree[1].sum<<"."<<endl;
  }

}

如有任何疑问或不对的地方,请下方指出;我很乐意回答大家的问题和接受大家指出的错误

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值