[知识点]线段树

// 此博文为迁移而来,写于2015年3月30日,不代表本人现在的观点与看法。原始地址:http://blog.sina.com.cn/s/blog_6022c4720102vw6j.html

1、前言

一道题目:给出一个一维数组共n个结点,每次对他进行一些操作:在[l,r]范围内增加x,或是询问第i个结点当前的值为多少。这不水题嘛!直接模拟就可以了。但是如果n<=100000呢?询问次数超过100000呢?O(n^2)并不能扛住。今天要引入的内容就是线段树。这东西很早之前有提到过,我也编过,但是当时没有很好的理解。最近无意间去看了一眼,发现一下就懂了。

2、概念

线段树也是一种树结构,不一样的是,它的每个节点保存的是一条条线段。先看看他的构造:


       我们发现,线段树虽然以树结构展现出来,但是他每个节点索要维护的东西和一般的树结构问题差别还是比较大的。

2、建造线段树

线段树的根节点所表示的线段是[1,n],而后它的两个子节点left和right则是将其父节点的线段从中间劈开,如图所示,当n=8时,mid=(1+8)/2=4 (向下取整),所以子节点所代表的线段分别是[1,4]和[5,8]。从根节点一次往下建造,直到该节点的线段为一个点,即叶子节点。

-------------------------------------------------------------------------------------------------------

void buildTree(int now,int l,int r) // 建树

{

  if (l==r)

  {

    scanf("%d",&tree[now].sum);

    tree[now].max=tree[now].min=tree[now].sum;

    return;

  }

  int mid=(l+r)/2;

  buildTree(now*2,l,mid);

  buildTree(now*2+1,mid+1,r);

  tree[now].sum=tree[now*2].sum+tree[now*2+1].sum;

  tree[now].max=max(tree[now*2].max,tree[now*2+1].max);

  tree[now].min=min(tree[now*2].min,tree[now*2+1].min);

}

-------------------------------------------------------------------------------------------------------

代码中可以看出,每个节点需要包含sum,max,min。但是,不是任何时候都需要的,在此列出所有需要的,如何取舍后文将提到。建树很简单,从根到叶子递归就行了。

3、问题类型

线段树的功能很强大,所以有很多数组上的问题都可以很方便的求出来。这里可以作一个总结:

修改类型:1、单点加减(每次只在数组中的一位数进行加减);

2、单点修改(同理);

3、区间加减(每次对[l,r]内所有数进行加减);

4、区间修改(每次对[l,r]内所有数修改成相同的一个数)。

询问类型:1、单点查询(查询一个数的值);

2、查询区间和(查找[l,r]内所有数的和);

3、查询区间最大值(查找[l,r]内最大值);

4、查询区间最小值(同理)。

这些内容可以任意搭配,任意修改查询,但是一般只会同时出现2、3种。而线段树每个点需要维护的变量,就是根据它所要求的来加入。为什么有几项加了红色?因为这些类型中,这些最难,也最重要,需要重点讲。

<1>单点加减/单点修改

-------------------------------------------------------------------------------------------------------

void update1(int now,int l,int r,int loc,int val) // 单点修改,loc表示所要更改的点,val表示所增加的值

{

  int mid=(l+r)/2;

  tree[now].sum+=val;

  if (l==r) return;

  if (loc>=l && loc<=mid) update1(now*2,l,mid,loc,val);

  else update1(now*2+1,mid+1,r,loc,val);

}

-------------------------------------------------------------------------------------------------------

很好理解,从根节点到叶子节点一直走下去,将所有包含了loc这个点的线段进行修改。单点修改其实可以一起讲,只是多一个步骤,在更新之前找到原来的值和要修改的值的差,在进行单点加减操作。

<2>区间加减

-------------------------------------------------------------------------------------------------------

void update2(int now,int l,int r,int ql,int qr,int val) // 区间修改,ql和qr为查询部分,val为增加的值

{

  if (l==ql && r==qr) { tree[now].sum+=val*(r-l+1),tree[now].lazy+=val; return; } // 如果当前的线段就是所求

  int mid=(l+r)/2;

  if (tree[now].lazy!=0) pushDown(now,r-l+1); // 下放

  if (l<=ql && mid>=qr) update2(now*2,l,mid,ql,qr,val); // 如果所求线段包含于当前线段的左儿子

  else if (mid+1<=ql && r>=qr) update2(now*2+1,mid+1,r,ql,qr,val); // 右儿子

  else // 所求线段在左儿子右儿子均有一部分,所以可以分开更新

  {

    update2(now*2,l,mid,ql,mid,val);

    update2(now*2+1,mid+1,r,mid+1,qr,val);

  }

  tree[now].sum=tree[now*2].sum+tree[now*2+1].sum; // 上放

}

-------------------------------------------------------------------------------------------------------

区间加减相对而言就有点麻烦了。先不管pushDown函数。麻烦之处是在于线段树本身的一个优化,如果没有这个优化,线段树的效率就会变低。我们考虑到一个问题:假设一开始在[2,6]内增加2,又在[5,7]内增加5,那么在[5,6]内不要相当于增加7?在暂时没有询问操作的情况下,我们可以偷一点懒——对于每一条线段,加一个lazy标识,当没有询问的时候,可以不需要去对树进行更新。

       在更新的时候,我们不进行线段树本身值的加减,只修改变量lazy。也许我的思路相对于复杂一些,但是一样可以正常理解啦。。

没看懂?其实可以说得很形象:秋天到了,你是庙里的一个和尚,方丈给你一个任务:扫院子里的落叶。众所周知,落叶在秋天是会不断落下的,所以扫了一遍之后,过不一阵子就又有叶子了。在方丈还没有来检查你的工作的情况下,你可以偷一点懒——让叶子堆积在那里!反正早扫晚扫都是扫,还不如晚扫呢是不?(还没懂你还是弃疗算了。。)

现在,pushDown函数的作用可否知道了?那就是将当前线段保存的lazy标识释放出来,对线段树进行真正值的更新。什么时候要用到?如果你明白了其原理应该已经知道了——两种情况,当你需要对标了lazy标识的线段树的子节点进行操作,你不得不进行更新了;当需要进行查询操作时(就是当方丈过来的时候= =),进行更新。

-----------------------------------------------------------------------------------------------------------------

void pushDown(long long now,long long m) // 区间修改——lazy标记下放

{

  tree[now*2].lazy+=tree[now].lazy; tree[now*2+1].lazy+=tree[now].lazy;

  tree[now*2].sum+=tree[now].lazy*(m-(m/2));

  tree[now*2+1].sum+=tree[now].lazy*(m/2);

  tree[now].lazy=0;

}

-----------------------------------------------------------------------------------------------------------------

<3>单点查询

直接上代码。

-----------------------------------------------------------------------------------------------------------------

int getVal(int now,int l,int r,int loc) // 单点查询

{

  int mid=(l+r)/2;

  if (l==r && r==loc) return tree[now].val;

  else if (loc>=l && loc<=mid) return getVal(now*2,l,mid,loc);

  else return getVal(now*2+1,mid+1,r,loc);

}

-----------------------------------------------------------------------------------------------------------------

<4>查询区间和/最大值/最小值

也没什么难的,就是需要注意到前面提到的pushDown函数需要在这里用到。

-----------------------------------------------------------------------------------------------------------------

int getSum(int now,int l,int r,int ql,int qr) // 区间和

{

  if (l==ql && r==qr) return tree[now].sum;

  int mid=(l+r)/2,ans;

  if (tree[now].lazy!=0) pushDown(now,r-l+1);

  if (ql>=l && qr<=mid) ans=getSum(now*2,l,mid,ql,qr);

  else if (ql>=mid+1 && qr<=r) ans=getSum(now*2+1,mid+1,r,ql,qr);

  else ans=getSum(now*2,l,mid,ql,mid)+getSum(now*2+1,mid+1,r,mid+1,qr);

  return ans;

}

-----------------------------------------------------------------------------------------------------------------

这里只给出了区间和的代码,最大值最小值同理,大家自己去写了。

线段树就这么讲完了,完整的代码就是通过上述函数组合起来就可以了。

时间: 2024-11-04 15:00:43

[知识点]线段树的相关文章

知识点 - 线段树 权值 树套树 二维 可持续

知识点 - 线段树 权值 树套树 二维 可持续 //区间更新求和 inline int ls(int p) { return p << 1; }//左儿子 inline int rs(int p) { return p << 1 | 1; }//右儿子 void push_up(int p) { t[p] = t[ls(p)] + t[rs(p)]; }// 向上不断维护区间操作 void build(ll p, ll l, ll r) { if (l == r) { t[p] =

[知识点]线段树标记永久化

前言: 本文由Hallmeow原创,转载请注明出处! 由于打丧心病狂的 [BZOJ 4826]影魔  导致需要学习标记永久化,于是入坑OvO 知识点:线段树标记永久化 对于树套树,主席树等使用到线段树的比较复杂的数据结构,如果我们区间修改的话,打标记后pushdown或者pushup是很费劲的 那么我们能不能不用pushdown和pushup呢?当然可以啦!这样就用到标记永久化了! 原理就是: 在路过该节点的时候把修改对答案的影响加上,来省去标记下放的过程 实现起来: 线段树的每个节点维护 su

2014 summer 知识点总结1之线段树

HDU 1166 [题意]: n个阵营一字排开,每个初始有a[i]个人.现有两种操作: Q a b 查询[a,b]之间总人数并输出 A/S a b 在a号位添加/删除b个人 [分析]:最基本的单点更新和区间查询,维护节点信息sum[o] [代码]: 1 #include <iostream> 2 #include <string.h> 3 #include <stdio.h> 4 5 using namespace std; 6 7 int numv[50005<

线段树相关知识点

线段树实现功能: 区间查找和更新 时间复杂度: 更新:O(logn)查找:O(logn) 线段树内存需要开四倍大小,切记!!! 为什么需要开到四倍? https://www.cnblogs.com/FengZeng666/p/11446827.html 理论上是2n-1的空间,但是你递归建立的时候当前节点为r,那么左右孩子分别是2*r,2*r+1,此时编译器并不知道递归已结束,因为你的结束条件是在递归之前的,所以编译器会认为下标访问出错,也就是空间开小了,应该再开大2倍.有时候可能你发现开2,3

hiho一下 第二十一周(线段树 离散化)

知识点1:离散化  对于这些区间来说,其实并不会在乎具体数值是多少,而是在他们的左右端点之间互相进行比较而已.所以你就把这N个区间的左右端点——2N个整数提出来,处理一下呗?你要注意的是,这2N个数是什么其实并不重要,你可以把这2N个数替换成为任何另外2N个数,只要他们之间的相对大小关系不发生改变就可以.” 解决方法: 那么我需要额外做的事情就是在构建线段树之前对区间进行预处理:将区间的左右端点选出来,组成一个集合,然后将这个集合依次对应到正整数集合上,并且利用这个对应将原来的区间的左右端点更换

【线段树四】HDU 2795 Billboard

BillboardTime Limit: 20000/8000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 9045    Accepted Submission(s): 4021 Problem Description At the entrance to the university, there is a huge rectangular billboard of siz

hdu 1166 树状数组 线段树

敌兵布阵 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Submission(s): 51177    Accepted Submission(s): 21427 Problem Description C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了.A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务

(转)线段树的区间更新

原文地址:http://blog.csdn.net/zip_fan/article/details/46775633 写的很好,昨天刚刚开始写线段树,有些地方还不是很明白,看了这篇博文,学会了数组形式保存线段树,还学会了区间更新 以下为转载的博文内容 距离第一次接触线段树已经一年多了,再次参加ACM暑假集训,这一次轮到我们这些老家伙们给学弟学妹们讲解线段树了,所以就自己重新把自己做过的题目看了一遍,然后写篇博客纪念一下.作为一个菜鸟,文中肯定有很多表达不是很准确甚至错误的地方,欢迎各位大牛指正.

[您有新的未分配科技点]可,可,可持久化!?------可持久化线段树普及版讲解

最近跑来打数据结构,于是我决定搞一发可持久化,然后发现--一发不可收啊-- 对于可持久化数据结构,其最大的特征是"历史版本查询",即可以回到某一次修改之前的状态,并继续操作:而这种"历史版本查询"会衍生出其他一些强大的操作. 今天,我们主要讲解可持久化线段树.其实,它的另外一个名字"主席树"似乎更加为人所知(主席%%%). 主席树与普通的线段树相比,多出来的操作是在修改时复制修改的一条链,这个操作的过程大概长下面这样. 至于为什么要这样做-- 对