线段树
线段树
线段树,一种拿来解决 \(RMQ\) 与区间和问题的高效的数据结构,对于这两种问题,线段树均能在 \(O(mlog_{2}n)\) 的时间复杂度内解决。在这两个基础的应用场景的基础上,线段树还发展出了各种丰富的应用。
总的来说,线段树是一个应用广泛,功能强大的数据结构,本章将介绍其概念及基本操作。
本章将包括:
一 概念
概括的说,线段树可以理解为“分治法+二叉树结构+ \(Lazy-tag\) 技术”。
(1.原理
线段树是分治法与树的有机结合,可将线段树上的每个节点视为“线段”(或“区间”),区间是由分治得到的,下图为一棵表示区间 \([1,10]\) 的线段树:
(2.分析
由图可以看出其有几点基本特征:
1.用分治法自顶向下建立,每次分治左右子树各一半。
2.每个节点都表示一个区间 \([L,R]\) 叶节点 \(L==R\) ,非叶结点 \(L<R\) 。
3.除最后一层其它层都是满的。
对于每个区间 \([L,R]\) 区间都有:
若 \(L \ne R\) ,即 \([L,R]\) 不为叶子结点;则可将其分为 \([L,M]\) 与 \([M + 1,R]\) 两部分,其中 \(M=(L+R)/2\) ;
线段树节点(即线段)表示的值,可以为线段上的区间和,也可定义为区间最值或其他要求值,这也是线段树为什么灵活多变的原因
由上述条件可知,线段树适合解决的问题的特征是:大区间的解可以由小区间的解合并而来。
二 实现
线段树的代码实现。
(1.建树
分治建树,顺带维护线段;
int ls(int p){return p<<1;} //左儿,2*n
int rs(int p){return p<<1|1;} //右儿,2*n+1
int a[maxn],tree[maxn]; //a数组存储数列元素,tree树组表示一个线段区间的值
void push_up(int p){ //维护线段
return tree[p]=tree[ls(p)]+tree[rs(p)];
//若题目为求最小值,则此处为 tree[p]=min(tree[ls(p)],tree[rs(p)]);
}
void build_tree(ll p,ll pl,ll pr){//p为节点编号,其指向区间[pl,pr]
if(pl==pr){
tree[p]=a[pl]; //叶子结点,赋值
return;
}
ll mid=(pl+pr)>>1; //分治,折半
build_tree(ls(p),pl,mid); //左子树
build_tree(rs(p),mid+1,pr); //右子树
push_up(p); //从下往上维护线段树数组
}
关于区间问题:
对于区间问题,我们采用与分治一样的手段,(即:大区间的解由小区间的解合并而来)。具体是怎样操作呢?模版如下
//在递归函数中
void mo(int l,int r,int p,int pl,int pr){
//要进行__操作的区间[l,r],现在查询到p线段[pl,pr];
if(l<=pl&&r>=pr){ //[pl,pr]包含于[l,r]或[pl,pr]等于[l,r]
//todo 即进行操作
return;
}
}
但如果你对分治掌握的并不好(像我一样),理解起来可能有一点难度,用下图来解释一下(掌握的可以跳过)
上图即为 l<=pl&&r>=pr
的情况。当然,情况不唯一,我们可以简单的理解为区间 \([L,R]\) 由三个小区间 \([L,pl]\) , \([pl,pr]\) , \([pr,R]\) 组成,由是,对于区间 \([L,R]\) 的操作便可由三个小区间的解合并而来。
(2.区间修改
首先,我们需要引入 \(Lazy-tag\) 的概念:技如其名,懒标记,指在进行区间修改时,不一次执行到底,而是打下标记,当用到时,再进行修改。(不用就不修改,为我们贪心到了不少时间复杂度)
int tag[maxn]; //tag数组用来存储懒标记
void add_tag(int p,int pl,int pr,int d){ //添加标记d
tag[p]+=d;
tree[p]+=d*(pr-pl);
}
void push_tag(int p,int pl,int pr){ //维护懒数组标记
if(tag[p]){ //之前留下的懒标记,为了不被覆盖,向下传给子树
int mid=(pl+pr)>>1;
add_tag(ls(p),pl,mid);
add_tag(rs(p),mid+1,pr);
tag[p]=0; //标记被传走
}
}
void up_date(int l,int r,int p,int pl,int pr,int d){
if(l<=pl&&r>=pr){
add_tag(p,pl,pr,d); //打上懒标记,毕竟我是懒人
return;
}
push_down(p,pl,pr); //如不能覆盖,把tag传给子树
ll mid=(pl+pr)>>1;
if(l<=mid) updata(l,r,ls(p),pl,mid,d ); //递归左子树
if(r>mid) updata(l,r,rs(p),mid+1,pr,d); //递归右子树
push_up(p); //维护线段树
}
代码如上所示,懒标记不止能记录加法,如乘,除,乘方,开方,异或等。都可以以懒标记的形式存储,但当存在多个不同的懒标记时,注意懒标记的处理顺序,顺序不当可能会导致精度出现差异。
当然,对于单点修改问题,区间修改也可以兼任,只需查询时让 \(l=r\) 即可。
(3.区间查询
int query(int l,int r,int p,int pl,int pr){
if(l<=pl&&r>=pr){
return tree[p];
}
push_down(p,pl,pr);
int res=0,mid=(pl+pr)>>1;
if(l>pl) res+=query(l,r,ls(p),pl,mid);
if(p<pr) res+=query(l,r,rs(p),mid+1,pr);
return res;
}
此为区间求和的的代码,挨个加起来,没什么好说的。
三 例题
最经典的班子题,区间加+区间求和
对于线段树的另一种应用方式,好好利用异或,此题会简单不少
3.P2357守墓人
花哨了一点,仔细一看,还是区间加+区间求和的版子
4.P1253 扶苏的问题
区间加和区间修改的板子,安利一下我的题解