线段树是一种二叉搜索树,它的每一个结点对应着一个区间[L, R],叶子结点对应的区间就是一个单位区间,即L == R。对于一个非叶子结点[L, R],它的左儿子所表示的区间是[L, (L +R)/2],右儿子所代表的的区间是[(L + R) / 2 +1, R]。

  拿一个简单的例子来说,我们需要维护一个数列,每次进行以下两种操作:

  • 修改一个元素
  • 查询一段区间的最大值

这是一道经典的RMQ(range minimum/maximum query,区间最值查询问题)问题,用线段树怎么解决呢?更新是点更新,查询是区间查询。

具体操作如下:

  建树的时候,始终遵循每个结点维护结点所代表的左右端点和该区间的最值,建树的时候如果到叶子结点,那么这个结点的最值就是对应位置的数列的值,否则递归的建立左子树和右子树,然后将当前结点的区间最值设置为自己左子树和右子树最值的较大值。

  先定义线段树的结点:

const int maxn = ;

struct Node {
int l, r, mx;//左右区间端点和最大值
}tr[maxn<<];

建树:

 void build(int d, int l, int r) {//递归建树
tr[d].l = l, tr[d].r = r;
if(l == r) { //叶子结点
tr[d].mx = b[l];
return;
}
int mid = (l + r) / ;
int lc = d * ;
int rc = d * + ;
build(lc, l ,mid); //递归构建左子树
build(rc, mid + , r); //递归构建右子树
tr[d].mx = max(tr[lc].mx, tr[rc].mx);//该区间的最值是左子结点和右子结点的最大值
}

  如果是查询操作,从根结点开始查询:如果查询区间在该结点的左子内,则查询左子;如果查询区间在该结点的右子树内,则查询右子树;否则,查询左子树相应区间和右子树相应区间,并将两者的返回值的较大值返回。代码如下:

 int query(int d, int l, int r) {
if(tr[d].l == l && tr[d].r == r) {//待查询区间等于当前结点的区间范围
return tr[d].mx;
}
int mid = (tr[d].l + tr[d].r) / ;//取当前结点的中点
int lc = d * ;
int rc = d * + ;
if(r <= mid) return query(lc, l, mid);//查询区间属于当前结点的左子树就查询左子树
else if(l > mid) return query(rc, mid + , r);//查询区间属于当前结点的右子树就查询右子树
else return max(query(lc, l, mid), query(rc, mid + , r));//查询区间分布在两侧
}

  如果是修改操作,则从根结点开始修改,一直修改到叶子结点,同时对路径上相应结点的最值进行更新。

 void modify(int d, int pos, int v) {//将位置为pos的元素改成v
if(tr[d].l == tr[d].r && tr[d].l == pos) {//如果当前结点是叶子结点且是该结点
tr[d].mx = v;
return;
}
int mid = (tr[d].l + tr[d].r) / ;
int lc = d * ;
int rc = d * + ;
if(pos <= mid) modify(lc, pos, v);//如果要修改的位置在当前结点的左子树
else modify(rc, pos, v); // 右子树
tr[d].mx = max(tr[lc].mx, tr[rc].mx);
}

  以上是点更新加上区间查询,运用的时候将元素存入数组b中,建树,直接修改、查询即可。

  明白了基本的原理,下面介绍一种实现起来更简短,使用更方便的写法。

 const int INF = ;
int ql, qr;//查询区间
int query(int o, int L, int R) {
int M = L + (R - L) / ;
int ans = -INF;
if(ql <= L && R <= qr) return maxv[o]; //当前结点完全包含在查询区间内
if(ql <= M) ans = max(ans, query(o * , L, M)); //往左走
if(M < qr) ans = max(ans, query(o * + , M + , R));//往右走
return ans;
} int p, v;//修改A[p] = v
void update(int o, int L, int R) {
int M = L + (R - L) / ;
if(L == R) maxv[o] = v;
else {
if(p <= M) update(o * , L, M);
else update(o * + , M + , R);
maxv[o] = max(maxv[o * ], maxv[o * + ]);
}
}

  使用的时候建树的过程是每次读入一个数,使用update函数更新A[i] = x。然后直接查询、修改即可。

  以上是点更新加上区间查询,如果没有点更新,只是查询某个区间的最值,则直接使用ST算法(简单不易写错)。

  但是通常在题目中会遇到对区间进行更新的操作,比如给出一个n个元素的数组A1,A2,A3...An,你的任务是设计一个数据结构,支持一下两种操作。

  • Add(L,R,v):把AL,AL+1,...,AR的值全部增加v。
  • Query(L,R):计算子序列AL,AL,...AR的元素和、最小值和最大值。

  我们需要在线段树中维护3个信息sum,min,max,分别对应三个查询值。其中如果还是使用sum[o]表示“结点o对应区间中所有数之和”,则add操作最坏情况下会修改所有的sum。解决的办法是把sum[o]的定义改成“如果只执行结点o及其子孙结点中的add操作,结点o对应区间中所有数之和”。信息维护的代码如下:

 //维护结点o,对应区间[L,R]
void maintain(int o, int L, int R) {
int lc = o * ;
int rc = O * + ;
sumv[o] = minv[o] = maxv[o] = ;
if(R > L) {//考虑左右子树
sumv[o] = sumv[lc] + sumv[rc];
minv[o] = min(minv[lc], minv[rc]);
maxv[o] = max(maxv[lc], maxv[rc]);
}
minv[o] += addv[o];
maxv[o] += addv[o];
sumv[o] += addv[o] * (R - L + );
}

  上述维护结点o的maintain函数在递归访问到的结点都需要调用,并且在递归返回后调用。代码如下:

 //其中y1,y2表示修改和查询的区间
void update(int o, int L, int R) {
int lc = o * ;
int rc = o * + ;
if(y1 <= L && y2 >= R) {//递归边界
addv[o] += v;
} else {
int M = L + (R - L) / ;
if(y1 <= M) update(lc, L, M);
if(y2 > M) update(rc, M + , R);
}
maintain(o, L, R);//递归结束后重新计算本结点附加信息
}

  接下来就是查询操作了,基本思路仍然是把查询区间递归分解为若干不相交子区间,把各个子区间的查询结果加以合并,但是需要注意的是每个边界区间的结果不能直接使用,还得考虑祖先结点对它的影响。为了方便,我们在递归查询函数中增加了一个参数,表示当前区间的所有祖先结点add值之和。代码如下:

 int _min, _max, _sum;//对应查询结果
void query(int o, int L, int R, int add) {
if(y1 <= L && y2 >= R) {
_sum += sumv[o] + add * (R - L + );
_min = min(_min, minv[o] + add);
_max = max(_max, maxv[o] + add);
} else {//递归统计累加参数add
int M = L + (R - L) / ;
if(y1 <= M) query(o * , L, M, add + addv[o]);
if(y2 > M) query(o * + , M + , R, add + addv[o]);
}
}

  上述讲解的是区间增减,还有一种情况是区间赋值。即给出一个有n个元素的数组,A1,A2,...,An,你的任务是设计一个数据结构,支持一下两种操作:

  • Set(L, R, v):把AL,AL+1,...AR的值全部修改成v(v>=0)
  • Query(L,R):计算子序列AL,AL,...AR的元素和、最小值和最大值。

  同理我们将set操作也进行分解,记录在结点中,但是出现了一个新的问题,即add操作没有先后的时效性,但是set操作是有的。

  解决的办法是设计一个向下传递函数,用来做一个标记。

  新的修改操作代码如下:

 void update(int o, int L, int R) {
int lc = o * ;
int rc = o * + ;
if(y1 <= L && y2 >= R) {//递归边界,将set标记修改
setv[o] = v;
} else {
pushdown(o);
int M = L + (R - L) / ;
if(y1 <= M) update(lc, L, M); else maintain(lc, L, M);
if(y2 > M) update(rc, M + , R); else maintain(rc, M + , R);
}
maintain(o, L, R);//递归结束后重新计算本结点附加信息
}

其中需要注意的有两个地方,首先是pushdown函数,它的作用就是把set值往下传递。

 void pushdown(int o) {
int lc = o * ;
int rc = o * + ;
if(setv[o] >= ) {//由于赋的值是大于等于0的,所以>= 0表示有标记
setv[lc] = setv[rc] = setv[o];
setv[o] = -; //清除标记
}
}

  另一个值得注意的地方是代码出多了两处maintain的调用。对于本来就要递归访问的子树,递归访问结束之后自然会调用maintain,因此只需要针对不进行递归访问的子树调用maintain即可。

  接下来就是关键的查询问题了,怎么解决任意两个set操作不会存在祖先-后代关系的问题。

  其实我们只需规定在这种情况下,以祖先结点上的操作为准即可,在递归查询的时候,碰到到一个set操作就立即停止即可。代码如下:

 void query(int o, int L, int R) {
if(setv[o] >= ) { //递归边界1:有set标记
_sum += setv[o] * (min(R, y2) - max(L, y1) + );
_min = min(_min, setv[o]);
_max = max(_max, setv[o]);
} else if(y1 <= L && y2 >= R) {//递归边界2:边界区间
_sum += sumv[o]; //此区间没有被任何set操作影响
_min = min(_min, minv[o]);
_max = max(_max, maxv[o]);
} else { //递归统计
int M = L + (R - L) / ;
if(y1 <= M) query(o * , L, M);
if(y2 > M) query(o * + , M + , R);
}
}

  暂时线段树的讲解就到这里,理解的还不是太透彻,之后会补上几道例题。

  

线段树(segment tree)的更多相关文章

  1. 『线段树 Segment Tree』

    更新了基础部分 更新了\(lazytag\)标记的讲解 线段树 Segment Tree 今天来讲一下经典的线段树. 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间 ...

  2. 线段树(Segment Tree)(转)

    原文链接:线段树(Segment Tree) 1.概述 线段树,也叫区间树,是一个完全二叉树,它在各个节点保存一条线段(即“子数组”),因而常用于解决数列维护问题,基本能保证每个操作的复杂度为O(lg ...

  3. BZOJ.4695.最假女选手(线段树 Segment tree Beats!)

    题目链接 区间取\(\max,\ \min\)并维护区间和是普通线段树无法处理的. 对于操作二,维护区间最小值\(mn\).最小值个数\(t\).严格次小值\(se\). 当\(mn\geq x\)时 ...

  4. 【数据结构系列】线段树(Segment Tree)

    一.线段树的定义 线段树,又名区间树,是一种二叉搜索树. 那么问题来了,啥是二叉搜索树呢? 对于一棵二叉树,若满足: ①它的左子树不空,则左子树上所有结点的值均小于它的根结点的值 ②若它的右子树不空, ...

  5. 线段树(segment tree)

    线段树在一些acm题目中经常见到,这种数据结构主要应用在计算几何和地理信息系统中.下图就为一个线段树: (PS:可能你见过线段树的不同表示方式,但是都大同小异,根据自己的需要来建就行.) 1.线段树基 ...

  6. 浅谈线段树 Segment Tree

    众所周知,线段树是algo中很重要的一项! 一.简介 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点. 使用线段树可以快速的查找某一个节点在 ...

  7. 线段树 Interval Tree

    一.线段树 线段树既是线段也是树,并且是一棵二叉树,每个结点是一条线段,每条线段的左右儿子线段分别是该线段的左半和右半区间,递归定义之后就是一棵线段树. 例题:给定N条线段,{[2, 5], [4, ...

  8. 线段树(I tree)

    Codeforces Round #254 (Div. 2)E题这题说的是给了一个一段连续的区间每个区间有一种颜色然后一个彩笔从L画到R每个区间的颜色都发生了 改变然后 在L和R这部分区间里所用的颜色 ...

  9. segment树(线段树)

    线段树(segment tree)是一种Binary Search Tree或者叫做ordered binary tree.对于线段树中的每一个非叶子节点[a,b],它的左子树表示的区间为[a,(a+ ...

  10. RMQ问题(线段树+ST算法)

    转载自:http://kmplayer.iteye.com/blog/575725 RMQ (Range Minimum/Maximum Query)问题是指:对于长度为n的数列A,回答若干询问RMQ ...

随机推荐

  1. 单台机器安装zookeeper

    先给一堆学习文档,方便以后查看 官网文档地址大全: OverView(概述) http://zookeeper.apache.org/doc/r3.4.6/zookeeperOver.html Get ...

  2. spass按位置编码,进行排序题处理与分析

    本范例即需建立Q4_1至Q4_4 等四个变项, 各变量的数值则是排序的内容,共有0.1.2.3.4 等五种可能,0代表该选项没有被受测者选取,1.2.3.4分别代表被受测者指为第一至第四顺位. htt ...

  3. Localization

    Localization (using Histogram Filters) 定位指的是在传感器和移动之间来回的迭代,使得能够保持跟踪目标对象的位置.方向和速度. 这篇将写一个程序来实施定位,与GPS ...

  4. Linux 线程编程1.0

    在编译多线程程序的时候,需要连接libpthread文件: gcc pthread.c  -o  pthread  -lpthread: 所有线程一律平等,没有父子关系,线程属于进程. 创建线程用 p ...

  5. 普通java程序,maven打包

    pom.xml文件: <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://ww ...

  6. nodejs从http升级到https(阿里云证书的使用)

    升级原因 1.各大搜索引擎中,https的网页的权重比一般的http的网页权重要高. 2.从用户体验的角度,一个老是被浏览器提醒该网页不可信的网页,总不会让用户感到安心 所以将网站从http升级为ht ...

  7. ElasticSearch核心知识总结(二)

    如何超出扩容极限,以及如何提升容错性 primary&replica自动负载均衡,6个shared,3个primary,3个replica,随着机器扩容,会被均衡分配到多台机器上 6个shar ...

  8. 聊聊我面试过的一个最奇葩的 Java 程序猿!

    上周我聊了聊最让我反感的 10 种程序猿,无奈一个小时就进行了删除,详细原因就不说了,容易招黑. 今天聊的我面试过的最奇葩的一个程序猿,绝对是奇葩中的奇葩,简直是程序猿中的另类,最让我反感的程序猿又添 ...

  9. Java 8 停止维护,Java 9 难产,IDEA 2018 发布,还有……

    祝大家五一劳动节快乐,工作顺利! 又到了总结上个月干货的时候了,这个月我们带来了各种Java技术干货,各种送书抽奖福利,各种面试题分享,各种最新动态资讯等. 5.1重磅活动 | 区块链免费送书 &am ...

  10. redis pipeline 独占链接

    pipeline期间将“独占”链接,此期间将不能进行非“管道”类型的其他操作,直到pipeline关闭:如果你的pipeline的指令集很庞大,为了不干扰链接中的其他操作,你可以为pipeline操作 ...