一、概述
线段树是一种二叉搜索树,将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。
二、实例说明
以求某区间的min为例,应用线段树,来说明其优势所在。并给出源码实现。
求数组arr=[2,5,1,4,9,3]的某个区间[left,right]的最小值,原数组中的值允许随时更新。
1、直观解法
遍历数组区间找最小值,for(int i(left);i<=right;++i)minVal=min(minVal,arr[i]);
时间复杂度O(n),空间复杂度O(1),更新操作复杂度O(1),但是大数据辆,并且查询操作频繁时,会相当耗时。
2、第二种解法
用二维数组存储区间[i,j]的minVal,预处理的复杂度为O(n*n),查询时为O(1),可以应对频繁的查询操作。
mat = [ 2 2 1 1 1 1]
[-1 5 1 1 1 1]
[-1-1 1 1 1 1]
[-1-1-1 4 4 3]
[-1-1-1 -1 9 3]
[-1-1-1 -1 -1 3]
但是这种方案空间消耗很大,如果更新某个值,需要更新相应的二维数组中的minVal。
这种做法形式上类似于邻接矩阵。
3、应用线段树
构造线段树如图,预处理耗时O(n),查询、更新操作O(logn),需要额外的空间O(n)。
叶子节点是原始组数arr中的元素,非叶子节点代表它的所有子孙叶子节点所在区间的最小值。
三、源码
线段树是二叉树,形式上有点像完全二叉树,先以顺序存储为例。使用数组实现,数组大小大概是序列个数的4倍,解释如下:
第一部分,叶子节点是原arr数组中元素,为N个。
第二部分,该二叉树只含有度为0和度为2的节点,所以非叶子节点个数是N-1个。
第三部分,倒数第二层的叶节点占掉的存储位置不会超过N*2。
所以数组大小可以设为4*N-1。
结点定义如下:
struct SegTree{
int minVal;//该节点对应区间内的最小值,默认为-1
int addMark;//延迟标记,默认为0
SegTree(int minVal_ = -1, int addMark_ = 0) :minVal(minVal_), addMark(addMark_) {}
};
vector<SegTree> trees;//线段树,数组表示
trees.resize(4*N-1);
1、求区间最小值,顺序存储
#include<stdio.h>
#include<vector>
#include<algorithm>
using namespace std;
#define root 0 , 0 , N - 1
#define lRoot (rt<<1)+1
#define rRoot (rt<<1)+2
struct SegTree{
int minVal;//该节点对应的区间最值,默认为-1
int addMark;//延迟标记,默认为0
SegTree(int minVal_ = -1, int addMark_ = 0) :minVal(minVal_), addMark(addMark_) {}
};
vector<int> arr;//原数组
vector<SegTree> trees;//线段树
//建立线段树,O(n),rt为线段树当前节点的下标,[iStart,iEnd]为原数组中的对应区间
void build(const int &rt,const int &iStart,const int &iEnd){
trees[rt].addMark = 0;//SegTree中addMark初始化为0,所以build中的trees[rt].addMark = 0;可以省略掉。
if (iStart == iEnd) {
//叶子节点赋值
trees[rt].minVal = arr[iStart];
return;
}//if
int iMid = (iStart + iEnd) >> 1;
//左右子树赋值
build(lRoot, iStart, iMid);
build(rRoot, iMid + 1, iEnd);
//当前节点更新
trees[rt].minVal = min(trees[lRoot].minVal, trees[rRoot].minVal);
return;
}//build
//延迟标记相关的函数,将本节点的延迟标记向左右子节点传递,同时更新最值,清除自身标记
void pushDown(const int &rt) {
if (trees[rt].addMark) {
trees[lRoot].addMark += trees[rt].addMark;
trees[rRoot].addMark += trees[rt].addMark;
trees[lRoot].minVal += trees[rt].addMark;
trees[rRoot].minVal += trees[rt].addMark;
trees[rt].addMark = 0;
}//if
return;
}//pushDown
//区间查询,[qStart,qEnd]为查询区间,rt为当前线段树的节点下标,[nStart,nEnd]为其所代表的相应区间
int query(const int &rt, const int &nStart, const int &nEnd, const int &qStart, const int &qEnd) {
if (nStart > qEnd || nEnd < qStart)return INT_MAX;//当前区间与查询区间互不相交
if (qStart <= nStart&&nEnd <= qEnd)return trees[rt].minVal;//线段树的当前区间包含于查询区间,返回对应节点的最值
pushDown(rt);
int nMid = (nStart + nEnd) >> 1;
int qLeft = query(lRoot, nStart, nMid, qStart, qEnd);
int qRight = query(rRoot, nMid + 1, nEnd, qStart, qEnd);
return min(qLeft, qRight);
}//query
//单节点更新,index为原数组中待更新节点的下标,addVal为更新的值,rt为当前线段树的节点下标,[nStart,nEnd]为其所代表的相应区间
void updateOne(const int &rt,const int &nStart,const int &nEnd,const int &index, const int &addVal) {
if (nStart == nEnd) {
//找到该叶节点,更新
trees[rt].minVal += addVal;
return;
}//if
//二分寻找待更新的节点
int nMid = (nStart + nEnd) >> 1;
if (index <= nMid)updateOne(lRoot, nStart, nMid, index, addVal);
else updateOne(rRoot, nMid + 1, nEnd, index, addVal);
//index节点被更新了,其他受影响的节点也要更新
trees[rt].minVal = min(trees[lRoot].minVal, trees[rRoot].minVal);
return;
}//updateOne
//区间更新,[uStart,uEnd]为原数组中待更新的区间,addVal是更新值,rt为当前线段树的节点下标,[nStart,nEnd]为其所代表的相应区间
void update(const int &rt, const int &nStart, const int &nEnd, const int &uStart, const int &uEnd, const int &addVal) {
if (nStart > uEnd || nEnd < uStart)return;//区间不相交
if (uStart <= nStart&&nEnd <= uEnd) {//当前区间包含于待更新区间
trees[rt].addMark += addVal;
trees[rt].minVal += addVal;
return;
}//if
pushDown(rt);
int nMid = (nStart + nEnd) >> 1;
update(lRoot, nStart, nMid, uStart, uEnd, addVal);
update(rRoot, nMid+1, nEnd, uStart, uEnd, addVal);
trees[rt].minVal = min(trees[lRoot].minVal, trees[rRoot].minVal);
return;
}//update
int main() {
arr = { 3,4,5,7,2,1,0,3,4,5 };
int N = (int)arr.size();
trees.resize(N + N - 1 + (N << 1));
build(0, 0, N - 1);
/*updateOne(root, 6, 3);
updateOne(root, 5, 3);
updateOne(root, 6, -3);*/
/*int minVal = query(root, 5, 8);
printf("minVal=%d\n", minVal);
minVal = query(root, 2, 5);
printf("minVal=%d\n", minVal);*/
/*update(root, 5, 8, 30);
printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal,tmp.addMark); putchar(10);
printf("arr: "); for (auto tmp : arr)printf("%d ", tmp); putchar(10);
update(root, 2, 6, -40);
printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal, tmp.addMark); putchar(10);
int minVal = query(root, 0, 9);
printf("minVal=%d\n", minVal);
minVal = query(root, 5, 5);
printf("minVal=%d\n", minVal);
printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal, tmp.addMark); putchar(10);*/
return 0;
}//main
区间查询query,必选满足“相邻的区间的信息可以被合并成两个区间的并区间的信息”,在这个问题上,区间最小值问题是满足的。将待查询区间[uStart,uEnd]划分为一个个小区间,这些小区间不重不漏,组合起来正好覆盖[uStart,uEnd],完美解决[uStart,uEnd]最小值的问题。
延迟标记addMark主要应用在update和query中,是线段树的精髓,每次操作不必将更改作用在叶子节点上,而是在不影响查询结果的情况下,尽量减少对线段树的更改。这种信息就被保存在addMark这个变量中。对于minVal,可以查看源码理解其操作,对于sumVal(见后面源码),也有相应的pushDown函数实现,可对照学习。
updateOne是单节点更新,每次的操作都作用在叶节点上了,理论上可以用多个updateOne来代替update的效果,但是效率会很低。
参考图片,![](https://img-blog.csdn.net/20170527155112314)
2、求区间最小值,链式存储
结点定义如下
struct SegTree {
int minVal;//该节点对应的区间最值,默认为-1
int addMark;//延迟标记
SegTree *lch = NULL;//左节点指针
SegTree *rch = NULL;//右节点指针
SegTree(int minVal_ = -1, int addMark_ = 0) :minVal(minVal_), addMark(addMark_) {}
~SegTree() {
printf("[%d,%d]\n", minVal, addMark);
if (lch) {
delete lch;
lch = NULL;
}//if
if (rch) {
delete rch;
rch = NULL;
}//if
}//~SegTree
};
源码实现上只有build函数变化较大,声明如下SegTree *build(SegTree *root, const int &iStart, const int &iEnd) ;
结尾要做内存处理
if(root){
delete root;
root = NULL;
}//if
#include<stdio.h>
#include<vector>
#include<algorithm>
using namespace std;
#define lRoot (rt<<1)+1
#define rRoot (rt<<1)+2
#define rt root, 0, N - 1
#define CRTDBG_MAP_ALLOC
#include<stdlib.h>
#include<crtdbg.h>
struct SegTree {
int minVal;//该节点对应的区间最值,默认为-1
int addMark;//延迟标记,默认为0
SegTree *lch = NULL;;//左节点指针
SegTree *rch = NULL;;//右节点指针
SegTree(int minVal_ = -1, int addMark_ = 0) :minVal(minVal_), addMark(addMark_) {}
~SegTree() {
printf("[%d,%d]\n", minVal, addMark);
if (lch) {
delete lch;
lch = NULL;
}//if
if (rch) {
delete rch;
rch = NULL;
}//if
}//~SegTree
};
vector<int> arr;
SegTree *build(SegTree *root, const int &iStart, const int &iEnd) {
root = new SegTree();
//root->addMark = 0;
if (iStart == iEnd) {
root->minVal = arr[iStart];
return root;
}//if
int iMid = (iStart + iEnd) >> 1;
root->lch = build(root->lch, iStart, iMid);
root->rch = build(root->rch, iMid + 1, iEnd);
root->minVal = min(root->lch->minVal, root->rch->minVal);
return root;
}//build
void pushDown(SegTree *root) {
if (root->addMark) {
root->lch->addMark += root->addMark;
root->rch->addMark += root->addMark;
root->lch->minVal += root->addMark;
root->rch->minVal += root->addMark;
root->addMark = 0;
}//if
return;
}//pushDown
int query(SegTree *root, const int &nStart, const int &nEnd, const int &qStart, const int &qEnd) {
if (nStart > qEnd || nEnd < qStart)return INT_MAX;
if (qStart <= nStart&&nEnd <= qEnd)return root->minVal;
pushDown(root);
int nMid = (nStart + nEnd) >> 1;
int qLeft = query(root->lch, nStart, nMid, qStart, qEnd);
int qRight = query(root->rch, nMid + 1, nEnd, qStart, qEnd);
return min(qLeft, qRight);
}//query
void updateOne(SegTree *root, const int &nStart, const int &nEnd, const int &index, const int &addVal) {
if (nStart == nEnd) {
root->minVal += addVal;
return;
}//if
int nMid = (nStart + nEnd) >> 1;
if (index <= nMid)updateOne(root->lch, nStart, nMid, index, addVal);
else updateOne(root->rch, nMid + 1, nEnd, index, addVal);
root->minVal = min(root->lch->minVal, root->rch->minVal);
return;
}//updateOne
void update(SegTree *root, const int &nStart, const int &nEnd, const int &uStart, const int &uEnd, const int &addVal) {
if (nStart > uEnd || nEnd < uStart)return;
if (uStart <= nStart&&nEnd <= uEnd) {
root->addMark += addVal;
root->minVal += addVal;
return;
}//if
pushDown(root);
int nMid = (nStart + nEnd) >> 1;
update(root->lch, nStart, nMid, uStart, uEnd, addVal);
update(root->rch, nMid + 1, nEnd, uStart, uEnd, addVal);
root->minVal = min(root->lch->minVal, root->rch->minVal);
return;
}//update
int main() {
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
arr = { 3,4,5,7,2,1,0,3,4,5 };
int N = (int)arr.size();
SegTree *root(NULL);
root = build(rt);
updateOne(rt, 6, 3);
updateOne(rt, 5, 3);
updateOne(rt, 6, -3);
int minVal = query(rt, 5, 8);
printf("minVal=%d\n", minVal);
minVal = query(rt, 2, 5);
printf("minVal=%d\n", minVal);
/*update(rt, 5, 8, 30);
update(rt, 2, 6, -40);
int minVal = query(rt, 0, 9);
printf("minVal=%d\n", minVal);
minVal = query(rt, 5, 5);
printf("minVal=%d\n", minVal);*/
if (root) {
delete root;
root = NULL;
}//if
return 0;
}//main
3、其他相关问题
线段树适用于解决区间动态查询和修改的问题,刚才是求区间最小值的问题,现在给出求区间和的问题。
也可以使用树状数组来解决,见树状数组应用于区间求和。
求区间和,顺序存储,源码
相比于求区间最小值,pushDown有较大改动,原型为void pushDown(const int &rt, const int &len);后面的参数len是rt所对应结点区间的长度。
当前结点更新操作为trees[rt].sumVal = trees[lRoot].sumVal + trees[rRoot].sumVal;而不再是***=min(***,**)。
#include<cstdio>
#include<vector>
using namespace std;
#define lRoot (rt<<1)+1
#define rRoot (rt<<1)+2
#define root 0,0,N-1
struct SegTree {
int sumVal;
int addMark;
SegTree(int sumVal_ = -1, int addMark_ = 0) :sumVal(sumVal_), addMark(addMark_) {}
};
vector<int> arr;
vector<SegTree> trees;
void pushDown(const int &rt, const int &len) {
if (trees[rt].addMark) {
trees[lRoot].addMark += trees[rt].addMark;
trees[rRoot].addMark += trees[rt].addMark;
trees[lRoot].sumVal += trees[rt].addMark * (len - (len >> 1));//len-(len>>1)为左结点的长度,addMark为一个结点增加的值,整个trees[lRoot].sumVal增加的值为trees[rt].addMark*(len-(len>>1))。
trees[rRoot].sumVal += trees[rt].addMark * (len >> 1);//len>>1为右结点长度
trees[rt].addMark = 0;
}//if
return;
}//pushDown
void build(const int &rt, int iStart,int iEnd) {
//trees[rt].addMark = 0;
if (iStart == iEnd) {
trees[rt].sumVal = arr[iStart];
return;
}//if
int iMid = (iStart + iEnd) >> 1;
build(lRoot, iStart, iMid);
build(rRoot, iMid + 1, iEnd);
trees[rt].sumVal = trees[lRoot].sumVal + trees[rRoot].sumVal;
return;
}//build
int query(const int &rt, const int &nStart, const int &nEnd, const int &qStart, const int &qEnd) {
if (nStart > qEnd || nEnd < qStart)return 0;
if (qStart <= nStart&&nEnd <= qEnd) return trees[rt].sumVal;
pushDown(rt, nEnd - nStart + 1);
int nMid = (nStart + nEnd) >> 1;
int retSum(0);
retSum += query(lRoot, nStart, nMid, qStart, qEnd);
retSum += query(rRoot, nMid + 1, nEnd, qStart, qEnd);
return retSum;
}//query
void updateOne(const int &rt,const int &nStart,const int &nEnd,const int &index,const int &addVal) {
if (nStart == nEnd) {
trees[rt].sumVal += addVal;
return;
}//if
int nMid = (nStart + nEnd) >> 1;
if (index <= nMid)updateOne(lRoot, nStart, nMid, index, addVal);
else updateOne(rRoot, nMid + 1, nEnd, index, addVal);
trees[rt].sumVal = trees[lRoot].sumVal + trees[rRoot].sumVal;
return;
}//updateOne
void update(const int &rt,const int &nStart,const int &nEnd,const int &uStart,const int &uEnd, const int &addVal) {
if (nStart > uEnd || nEnd < uStart)return;
if (uStart <= nStart&&nEnd <= uEnd) {
trees[rt].addMark += addVal;
trees[rt].sumVal += addVal*(nEnd - nStart + 1);
return;
}//if
pushDown(rt, nEnd - nStart + 1);
int nMid = (nStart + nEnd) >> 1;
update(lRoot, nStart, nMid, uStart, uEnd, addVal);
update(rRoot, nMid + 1, nEnd, uStart, uEnd, addVal);
trees[rt].sumVal = trees[lRoot].sumVal + trees[rRoot].sumVal;
return;
}//update
int main() {
arr = { 3,4,5,7,2,1,0,3,4,5 };
int N = arr.size();
trees.resize(N + N - 1 + 2 * N);
build(root);
int sum = query(root, 5, N - 1);
printf("%d\n", sum);
update(root, 1, 4, 10);
sum = query(root, 0, N - 1);
printf("%d\n", sum);
update(root, 8, 9, -100);
sum = query(root, 0, N - 1);
printf("%d\n", sum);
sum = query(root, 0, 1);
printf("%d\n", sum);
updateOne(root, 5, 100);
//update(root,5,5,100);
sum = query(root, 0, N - 1);
printf("%d\n", sum);
printf("trees: "); for (int i(0); i < 4 * N - 1; ++i) printf("[%d,%d] ", trees[i].sumVal, trees[i].addMark); putchar(10);
return 0;
}//main
4、求区间最小值,另一种实现思路
上面的源码中,注意到arr数组没有改变过,一直都是线段树本身结点在被更新。
下面从另一个角度,给出一个区间min值的实现。线段树中存储的不是相应区间的最小值,而是相应区间最小值对应的下标。
差别在于,比如build函数中,
if (iStart == iEnd) {
trees[rt].minVal = iStart;
return;
}//if
叶结点被赋值,之前是trees[rt].minVal=arr[iStart];现在改为,trees[rt].minVal=iStart;这就是直接存储下标的结果。
每次更新结点的minVal时,trees[rt].minVal = arr[trees[lRoot].minVal] < arr[trees[rRoot].minVal] ? trees[lRoot].minVal : trees[rRoot].minVal;
还需要比较arr中的值。
#include<stdio.h>
#include<vector>
using namespace std;
#define root 0 , 0 , N - 1
#define lRoot (rt<<1)+1
#define rRoot (rt<<1)+2
struct SegTree {
int minVal;
int addMark;
SegTree(int minVal_ = -1, int addMark_ = 0) :minVal(minVal_), addMark(addMark_) {}
};
vector<int> arr;
vector<SegTree> trees;
void build(const int &rt, const int &iStart, const int &iEnd) {
//trees[rt].addMark = 0;
if (iStart == iEnd) {
trees[rt].minVal = iStart;
return;
}//if
int iMid = (iStart + iEnd) >> 1;
build(lRoot, iStart, iMid);
build(rRoot, iMid + 1, iEnd);
trees[rt].minVal = arr[trees[lRoot].minVal] < arr[trees[rRoot].minVal] ? trees[lRoot].minVal : trees[rRoot].minVal;
return;
}//build
void pushDown(const int &rt) {
if (trees[rt].addMark) {
trees[lRoot].addMark += trees[rt].addMark;
trees[rRoot].addMark += trees[rt].addMark;
arr[trees[lRoot].minVal] += trees[rt].addMark;
arr[trees[rRoot].minVal] += trees[rt].addMark;
trees[rt].addMark = 0;
}//if
return;
}//pushDown
int query(const int &rt, const int &nStart, const int &nEnd, const int &qStart, const int &qEnd) {
if (nStart > qEnd || nEnd < qStart)return -1;
if (qStart <= nStart&&nEnd <= qEnd)return trees[rt].minVal;
pushDown(rt);
int nMid = (nStart + nEnd) >> 1;
int q1 = query(lRoot, nStart, nMid, qStart, qEnd);
int q2 = query(rRoot, nMid + 1, nEnd, qStart, qEnd);
if (q1 == -1)return q2;
if (q2 == -1)return q1;
return arr[q1] < arr[q2] ? q1 : q2;
}//query
void updateOne(const int &rt, const int &nStart, const int &nEnd, const int &index, const int &addVal) {
if (nStart == nEnd) {
arr[trees[rt].minVal] += addVal;
return;
}//if
int nMid = (nStart + nEnd) >> 1;
if (index <= nMid)updateOne(lRoot, nStart, nMid, index, addVal);
else updateOne(rRoot, nMid + 1, nEnd, index, addVal);
trees[rt].minVal = arr[trees[lRoot].minVal] < arr[trees[rRoot].minVal] ? trees[lRoot].minVal : trees[rRoot].minVal;
return;
}//updateOne
void update(const int &rt, const int &nStart, const int &nEnd, const int &uStart, const int &uEnd,const int &addVal){
if (nStart > uEnd || nEnd < uStart)return;
if (uStart <= nStart&&nEnd <= uEnd) {
trees[rt].addMark += addVal;
arr[trees[rt].minVal] += addVal;
return;
}//if
pushDown(rt);
int nMid = (nStart + nEnd) >> 1;
update(lRoot, nStart, nMid, uStart, uEnd, addVal);
update(rRoot, nMid + 1, nEnd, uStart, uEnd, addVal);
trees[rt].minVal = arr[trees[lRoot].minVal] < arr[trees[rRoot].minVal] ? trees[lRoot].minVal : trees[rRoot].minVal;
return;
}//update
int main() {
arr = { 3,4,5,7,2,1,0,3,4,5 };
int N = (int)arr.size();
trees.resize(N + N - 1 + (N << 1));
build(0, 0, N - 1);
updateOne(root, 6, 3);
updateOne(root, 5, 3);
updateOne(root, 6, -3);
int minIndex = query(root, 5, 8);
printf("arr[%d]=%d\n", minIndex, arr[minIndex]);
minIndex = query(root, 2, 5);
printf("arr[%d]=%d\n", minIndex, arr[minIndex]);
printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal, tmp.addMark); putchar(10);
printf("arr: "); for (auto tmp : arr)printf("%d ", tmp); putchar(10);
/*update(root, 5, 8, 30);
printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal,tmp.addMark); putchar(10);
printf("arr: "); for (auto tmp : arr)printf("%d ", tmp); putchar(10);
update(root, 2, 6, -40);
printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal, tmp.addMark); putchar(10);
printf("arr: "); for (auto tmp : arr)printf("%d ", tmp); putchar(10);
int minIndex = query(root, 0, 9);
printf("arr[%d]=%d\n", minIndex,arr[minIndex]);
minIndex = query(root, 5, 5);
printf("arr[%d]=%d\n", minIndex, arr[minIndex]);
printf("trees: "); for (auto tmp : trees)printf("[%d,%d] ", tmp.minVal, tmp.addMark); putchar(10);
printf("arr: "); for (auto tmp : arr)printf("%d ", tmp); putchar(10);*/
return 0;
}//main
参考图,