线段树(从入门到入坟)

其实网上已经有很多很好的博客了,我在写这篇文章前也看了不少(为什么说是看了不少,可能是因为这些博主对线段树的掌握已经很透彻了,他们代码的写法追求的是尽量从简,可能导致我们这些初学者不好了解,故我打算写一篇博客站在我们初学者的角度来写)
此博客可能借鉴以下几篇博客比较经典的图片或代码以及思路;
{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
    评论
1. 基础概念 汇编语言是一种低级语言,它是计算机硬件指令的助记符。汇编语言直接反映了计算机的硬件结构,可以直接操作计算机的硬件资源。 汇编语言的基本单位是汇编指令,它由操作码和操作数两部分组成。操作码表示要执行的操作,操作数表示操作的对象或数据。 汇编语言的程序由若干条汇编指令组成,程序的执行顺序由程序中的指令顺序决定。 2. 寄存器 寄存器是计算机中用来暂时存储数据的高速存储器件,是汇编语言中最常用的数据存储方式。 常用的寄存器有通用寄存器、段寄存器、指针寄存器、标志寄存器等。 通用寄存器包括AX、BX、CX、DX等,可以用来存储数据、地址、偏移量等。 段寄存器包括CS、DS、SS、ES等,用来存储段地址。 指针寄存器包括SP、BP、SI、DI等,用来存储栈指针、基址指针、源地址指针、目的地址指针等。 标志寄存器包括CF、PF、AF、ZF、SF、OF等,用来存储运算结果的状态信息。 3. 指令集 汇编语言的指令集包括数据传送指令、算术指令、逻辑指令、比较指令、跳转指令、循环指令等。 数据传送指令用来把数据从一个地方传送到另一个地方,包括MOV、XCHG、LEA等。 算术指令用来进行加、减、乘、除等运算,包括ADD、SUB、MUL、DIV等。 逻辑指令用来进行位运算,包括AND、OR、NOT、XOR等。 比较指令用来比较两个数据的大小关系,包括CMP、TEST等。 跳转指令用来改变程序的执行顺序,包括JMP、JZ、JNZ、JE、JNE等。 循环指令用来重复执行一段程序,包括LOOP、LOOPE、LOOPZ、LOOPNE、LOOPNZ等。 4. 程序设计 汇编语言的程序设计需要掌握一定的技巧和方法,包括程序的结构、数据的处理、流程控制等。 程序的结构包括程序的头部、数据段、代码段、堆栈段等。 数据的处理包括数据类型、数据的存储和读取、数据的转换等。 流程控制包括条件判断、循环控制、函数调用等。 5. 汇编器和调试器 汇编器是将汇编语言程序翻译成机器语言程序的工具,可以将汇编语言程序转换成目标代码或可执行文件。 调试器是用来调试程序的工具,可以帮助程序员查找程序中的错误、调试程序的执行流程等。 6. 总结 学习汇编语言需要掌握基本概念、寄存器、指令集、程序设计、汇编器和调试器等知识,需要进行大量的实践和练习,才能掌握汇编语言的编程技巧和方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值