线段树
线段树可维护的信息类型
线段树可以维护的信息类型,父范围上的某个信息,可以用O(1)的时间,从子范围的信息加工得到,例如求某个范围的最大最小值,给某个范围每个位置加相同的数字,进行求和。
0到2范围上的最大值是5,3到5范围上的最大值是7,如果求0到5范围的最大值,就是5和7进行比较,这样子就很快。 已知2到4范围上的和是15,那么给这三个位置的值都增加2,那么和就是15+2*3=21,也很快;
那么还有一些不能够被维护的信息类型,例如:某个范围出现次数最多的数字
对于0到4范围上,出现最多的数字是3,5到9范围上,出现最多的数字是1,但是在0到9范围上出现的最多的数字不是1和3,而是5,父范围上的信息,不能够O(1)的时间由子范围得到,那么这样子的就是不能被线段树维护的信息。
线段树的经典功能:范围查询和范围修改操作单次调用的时间复杂度为O(logN)
线段树的操作
以经典的累加和为例:
- 线段树可以1下标开始,也可以0下标开始,1下标开始是经典的设定。
- 线段树可以在初始化时,指定范围的规模[1,n],一旦确定不能改变
- 任何一个大范围[l,r],严格从中点拆分为左[l,mid]和右[mid+1,r]
- 每个范围的信息,填在独立的,连续的数组中,最大范围的[1,n]填在sum[1]
- 如果父信息在sum[i],那么左范围sum[i*2],右范围[i*2+1]
- 范围[l,r]和i值对应,由递归去维护,无需记录对应关系
对于维护节点个数不是2^n次方的范围来说
由于子范围和父范围有相对应的关系,那么可以不用记忆一个范围对应在sum数组中的什么位置,直接由父信息在sum[i],那么左范围sum[i*2],右范围[i*2+1]进行维护,那么实际上应该开多大的数组?2*n+1?不可以,用实际的例子展示下。
数组多大空间
如图:这样子超出了2*n+1的范围
那么应该如何去做,求得实际的空间。应该是根据树高去求得节点的个数如果节点个数是2^n次方,那么展开就是一颗完全二叉树,树的高度(log以2为底n的对数+1,)对于不是2^n节点的,需要的节点个数是大于当前节点且最接近的节点个数,那么树高就是log以2为底n的对数+2,因此可以得出需要开辟的空间为4*n,也可以通过代码进行演示
// [l,r]在数组的i位置
// 返回递归过程展开的最大编号
int maxi(int l, int r, int i) {if (l == r) {// 递归到了1个节点,直接填return i;}else {// 不止一个int mid = (l + r) >> 1;// i << 1 | 1 --> (i * 2 + 1) return max(maxi(l, mid, i << 1), maxi(mid + 1, r, i << 1 | 1));}
}int main() {int n = 10000; // 测试范围int a = 0; // 最大倍数的iint b = 0; // 最大倍数的需要的空间double t = 0; // 最大倍数for (int i = 1; i <= n; i++) {int space = maxi(1, i, 1); // 获取最大编号double times = space / (double)i; // 倍数cout << "范围[1~" << i << "]," << "需要空间" << space << ",倍数=" << times << endl;if (times > t) {a = i;b = space;t = times;}}cout << "其中的最大倍数,范围[1~" << a << "]," << "需要空间" << b << ",倍数=" << t << endl;return 0;
}
建树
void up(int i) {// 父范围的累加和 = 左范围累加和 + 右范围累加和sum[i] = sum[i << 1] + sum[i << 1 | 1];
}// 建树
void build(int l, int r, int i) {if (l == r) {// 只有一个,直接填sum[i] = arr[l];}else {int mid = (l + r) >> 1;// 左右调用求和build(l, mid, i << 1);build(mid + 1, r, i << 1 | 1);up(i);}
}
查询
query(jobl,jobr,l,r,i):jobl和jobr代表需要查询任务的范围,l和r代表当前在的范围,i代表当前是sum数组的哪一个位置。
例如:求得[2,7]范围上的和。query(2,7,1,8,1),从顶部开始向下寻找,[1,4]位置包含,拆分为[1,2]和[3,4],对于[1,2]只有右边的2在[2,7]中,向上返回sum中9位置的数字,对于[3,4]都包含在[2,7]中,向上返回sum中5位置的数字,数字相加返回到顶部;同理,[5,8]范围包含,得到sum中6位置的数字和sum中14位置的数字,数字相加返回到顶部。
从这个过程可以看出,某些位置可以不遍历到树的根节点,因此大大节省了时间。时间复杂度为logn,对于根节点的两边展开,大致是沿着一条线下去的,部分返回的过程中不需要完全展开到根节点。
// 任务范围[jobl,jobr]
// 线段树维护范围[l,r]
// 在sum中的位置i,在[l,r]的和是i
long query(int jobl, int jobr, int l, int r, int i) {if (jobl <= l && r <= jobr) {// 范围全命中,不用向下展开了// 需要[2,5],现在是[3,4]return sum[i];}// 没有全命中,任务下发 int mid = (l + r) >> 1;// 信息下发 -- 后面的方法讲了信息下发的过程// 如果没有down,可能更新不对,例如查询[2,3]范围的和// 假设上次的懒更新到了[1,2]和[3,4],没有往下更新,那么进行查询时// 获取到的2和3位置的值是错误的,即add数组相关位置是错误的down(i, mid - l + 1, r - mid);long ans = 0;if (jobl <= mid) {// 任务命中,左侧还需要调用ans += query(jobl, jobr, l, mid, i << 1);}if (jobr > mid) {// 任务命中,右侧还需要调用ans += query(jobl, jobr, mid + 1, r, i << 1 | 1);}return ans;
}
懒更新
void add(jobl,jobr,jobv,l,r,i):在[jobl,jobr]范围上,每个数组增加jobv;来到[l,r]范围上,信息存储的位置是i.
调用add(jobl,jobr,jobv,1,n,1),范围增加的递归,懒更新机制
- 任务[jobl,jobr]把当前范围[l,r]全覆盖,不在向下传递任务
add[i] += jobv;sum[i] += jobv * (r - l + 1) - 如果任务不能把当前范围全包,把当前范围积攒的懒信息只往下发一层,发过了不必要重复发送,然后决定往左有范围,进行递归,递归完成后,利用左右sum信息,把当前范围的sum[i]信息修改正确
- 退出递归
当原数组长的是8,每个位置是0,可以构建出15个节点的线段树,现在在某个范围上增加相同的值。需要add数组和sum数组,长度为线段树节点的个数(15),add数组存储某位置或者某段位置增加的数值,sum数组存储进行更新后得到的增加的和。
初始状态:
进行[1,8]位置增加5:由于根节点位置包含在[1,8]中,因此只更新数组的1位置,得到:
[5,6]位置增加3:由于[5,6]没有把[1,8]覆盖掉,因此要进行懒更新,将积攒的信息发下去:add[2] = 5,add[3] = 5,add[1] = 0,由于add[2]和add[3]进行更新,那么sum数组对应的sum[2] = 20,sum[]3] = 20,总和不变,因此sum[1]不变,还是40 。[1,4]中没有[5,6],不用向下,[5,6]在[5,8]但是没有被完全覆盖,因此进行懒更新下发:add[6] = 5,add[7] = 5,同理sum数组进行更新sum[6] = 10,sum[7] = 10.
此时[5,6]范围包住了需要增加的[5,6],直接进行更新,不用下发了,进行更新,add[6] = 5+3;对应的sum[6] = 10+2*3;那么sum[3] = 20+2*3;sum[1] = 40+2*3得到:
[2,6]位置增加-2:没有被[1,8]完全包住,由于上次已经进行了懒更新下发,因此直接到[1,4]和[5,8]位置,没有被[1,4]完全包住,进行懒更新下发,没有被[5,8]完全包住,由于已经下发,直接到下一层。
同理[1,2]进行更新
左侧无懒更新下发,进行数值更新,add[9] = 5 - 2,add[5] = 5- 2 ,相对应的sum进行更新得到如下表格 :
右侧更新,add[6] = 8 - 2 ,sum更新。任务完成
// 来到[l,r]对应的下标是i,范围上数字个数n == r - l + 1
void lazy(int i, long v, int n) {// sum和add进行调整sum[i] += v * n;add[i] += v;
}void up(int i) {// 父范围的累加和 = 左范围累加和 + 右范围累加和sum[i] = sum[i << 1] + sum[i << 1 | 1];
}// 懒信息的下发
// [l,r] i 懒信息
// [l,mid] i * 2 ln表示左侧几个数
// [mid+1,r] i*2+1 rn表示右侧几个数
void down(int i, int ln, int rn) {// 留着信息才下发if (add[i] != 0) {// 发左lazy(i << 1, add[i], ln);// 发右lazy(i << 1 | 1, add[i], rn);// 父范围懒信息清空add[i] = 0;}
}// jobl ~ jobr范围上每个数字增加jobv
void add(int jobl, int jobr, long jobv, int l, int r, int i) {// 当前任务把整个范围全包了,不用往下发了if (jobl <= l && r <= jobr) {lazy(i, jobv, r - l + 1);}else {// 没有全包int mid = (l + r) >> 1;// 懒更新下发,下发左和下发右down(i, mid - l + 1, r - mid);if (jobl <= mid) {// 左边add(jobl, jobr, jobv, l, mid, i << 1);}if (jobr > mid) {// 右边add(jobl, jobr, jobv, mid + 1, r, i << 1 | 1);}// 信息汇总up(i);}
}