目录
一:线段树简介
二:线段树原理----
1.自上而下的分解(建树)
2.自下而上的合并(区间求和)
3.单点修改
4.区间修改
三:模板例题
四:个人模板
给定一个数组a[maxn],提出如下问题
问题1:求这个数组的所有值的总和。
方法一:定义变量sum,遍历数组使sum+=a[i]。
方法二:定义另一数组pre,使pre[i]为原数组的前缀和。a[0]+a[1]+...a[i],则pre[last]=pre[last-1]+a[last]即为答案。
问题2:在问题1条件下,修改任意一个a[i]值(设+=change)的同时修改总和
方法一:直接令sum+=change。
方法二:使pre[i],pre[i+1]...pre[last]所有的值都+=change。
问题3:求连续区间[l,r]的总和
方法一:遍历数组,使ans+=a[l]+a[l+1]...a[r]。
方法二:令ans=pre[r]-pre[l]。
问题4:修改连续区间[l,r]的值(+=change)
方法一:直接令sum+=(l-r+1)*change。
方法二:令pre[l]+=(l-r+1)*change,pre[l+1]+=(l-r)*change,pre[l+2]+=(l-r+1)*change...
可以发现,如上例子中的问题讨论的都是连续区间或者是单点修改的问题。传统的遍历数组或者是前缀和数组在处理不同问题的时候都有不同的优势或劣势。那么,是否有数据结构可以解决上述类型问题的同时,还有着不错的时间空间复杂度呢?
在树状数组的文章中,我们可以发现树状数组树状数组并不能很好的应对问题4这类区间操作问题。树状数组能进行前缀和单点的维护,但相比于线段树而言,树状数组只能维护前缀“操作和”(前缀和,前缀积,前缀最大最小),区间操作和也仅仅只能做到区间加和,而线段树可以维护更多功能的区间操作和。
在面对需要进行更多区间操作和的问题时,线段树往往是理想的选择。
线段树
简介
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。
来自百度百科。
线段树是一个树状结构,形态是一个满二叉树,空间为O(nlogn),时间为O(nlogn),往往用于动态维护连续区间或者连续区间中的单点修改。
原理与建树
自上而下的区间分解(建树)
线段树的建立首先在于自上而下的区间划分。划分方式为以区间左边界和右边界的中点,即中点m = (l+r)/2,将原来的区间划分为左区间【l,m】和右区间【m+1,r】(想想为什么右区间左边界不是m)。这样不断的分割区间,直到子区间的左右边界重合。
如此一来,树的节点信息就一目了然了,即为所管理区域的区间和。
自下而上的区间合并(求和)
在上述过程后,区间已被划分。那么如何将区间合并呢?也就是如何求区间和呢?
从树的最底层开始考虑。假如给定求和区间【4,13】。若所被包含的节点有含有相同父节点,则直接向上合并,比如【5】,【6】在【4,13】内,同时他们的父节点都是【5,6】,那么可以直接向上合并。如此一来,最后一层最多还剩下两个节点,但由于他们不满足上述条件(被包含且与其他被包含节点有相同父节点),因此不向上合并。
因此【4,13】区间和等于【4】+【5,7】+【8,13】。
代码如下:
void PushUp(int rt){
Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];}
//Build函数建树
void Build(int l,int r,int rt){
//l,r表示当前节点区间,rt表示当前节点编号
if(l==r) {
//若到达叶节点
Sum[rt]=A[l];//储存数组值
return;
}
int m=(l+r)>>1;
//左右递归
Build(l,m,rt<<1);
Build(m+1,r,rt<<1|1);
//更新信息
PushUp(rt);
}
int Query(int L,int R,int l,int r,int rt){
//[L,R]表示操作区间,[l,r]表示当前区间,rt:当前节点编号
if(L <= l && r <= R){
return Sum[rt];
}
int m=(l+r)>>1;
int ANS=0;
if(L <= m) ANS+=Query(L,R,l,m,rt<<1);
if(R > m) ANS+=Query(L,R,m+1,r,rt<<1|1);
return ANS;
}
//注:头节点编号为1,其左子树为2*n即rt<<1,右子树为2*n+1,即rt<<1|1.
单点修改
通过递归找到修改的单点区间,然后将该点的信息进行向上合并,即pushup。
void update(int L,int C,int l,int r,int rt){
//l,r表示当前节点区间,rt表示当前节点编号(点修改,A[L] += C)
if(l==r){
//到叶节点,修改
Sum[rt]+=C;
return;
}
int m=(l+r)>>1;
//根据条件判断往左子树调用还是往右
if(L <= m) update(L,C,l,m,rt<<1);
else update(L,C,m+1,r,rt<<1|1);
PushUp(rt);//子节点更新了,所以本节点也需要更新信息
}
区间修改
传统方法下,我们会考虑递归找到树底层的每一个区间,更新信息后将修改信息Pushup到父节点进行维护。但这样做的复杂度太过于大,甚至比传统的简单数组结构进行区间遍历更新还要麻烦(思考一下为什么)。因此,我们便引入了懒惰标记这一概念。
懒惰标记表示本节点的统计信息已经根据标记更新过了,但是本节点的子节点仍需要进行更新。简单的说,就是等你进行查询操作的时候,我再给你更新。
假如说给一个区间的所有值都加上1(比如【4,13】)在修改的时刻并没有将所有子树都加1,而是将该区间打上懒惰标记1,表示我还欠这个区间的子树+1,这是一个向下的延迟修改。
在向上合并的时候,我们可以通过查询该节点有无懒惰标记,从而进行更新。向上显示的信息是修改以后的信息,所以查询的时候可以得到正确的结果。
有的标记会相互影响,所以每递归到一个区间,首先下推(若本节点有标记,就下推标记),然后再打上新的标记,这样仍然每个区间操作的复杂度是O(log2(n))。
void PushDown(int rt,int ln,int rn){
//ln,rn为左子树,右子树的数字数量。
if(Add[rt]){
//下推标记
Add[rt<<1]+=Add[rt];
Add[rt<<1|1]+=Add[rt];
//修改子节点的Sum使之与对应的Add相对应
Sum[rt<<1]+=Add[rt]*ln;
Sum[rt<<1|1]+=Add[rt]*rn;
//清除本节点标记
Add[rt]=0;
}
}
ll Query(int L,int R,int l,int r,int rt){
//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号
if(L <= l && r <= R){
//在区间内,直接返回
return Sum[rt];
}
int m=(l+r)>>1;
//下推标记,否则Sum可能不正确
PushDown(rt,m-l+1,r-m);
//累计答案
ll ANS=0;
if(L <= m) ANS+=Query(L,R,l,m,rt<<1);
if(R > m) ANS+=Query(L,R,m+1,r,rt<<1|1);
return ANS;
}
例题
HDU 1166 敌兵布阵
#include <iostream>
#include <cstdio>
#include <cstring>
#include <map>
#include <cmath>
#include <algorithm>
#define inf 0x3f3f3f3f
#define sd(a) scanf("%d",&a)
#define mem0(a) memset(a,0,sizeof(a))
#define ls l,m,rt<<1
#define rs m+1,r,rt<<1|1
typedef long long ll;
const int mod = 1e9+7;
const int maxn = 50010;
using namespace std;
int Sum[maxn<<2],Add[maxn<<2];//Sum求和,Add为懒惰标记
int A[maxn],n;//存原数组数据下标[1,n]
int len;
void PushDown(int rt,int ln,int rn){
//ln,rn为左子树,右子树的数字数量。
if(Add[rt]){
//下推标记
Add[rt<<1]+=Add[rt];
Add[rt<<1|1]+=Add[rt];
//修改子节点的Sum使之与对应的Add相对应
Sum[rt<<1]+=Add[rt]*ln;
Sum[rt<<1|1]+=Add[rt]*rn;
//清除本节点标记
Add[rt]=0;
}
}
//PushUp函数更新节点信息 ,这里是求和
void PushUp(int rt){
Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];}
//Build函数建树
void Build(int l,int r,int rt){
//l,r表示当前节点区间,rt表示当前节点编号
if(l==r) {
//若到达叶节点
Sum[rt]=A[l];//储存数组值
return;
}
int m=(l+r)>>1;
//左右递归
Build(l,m,rt<<1);
Build(m+1,r,rt<<1|1);
//更新信息
PushUp(rt);
}
void update(int L,int C,int l,int r,int rt){
//l,r表示当前节点区间,rt表示当前节点编号(点修改,A[L] += C)
if(l==r){
//到叶节点,修改
Sum[rt]+=C;
return;
}
int m=(l+r)>>1;
//根据条件判断往左子树调用还是往右
if(L <= m) update(L,C,l,m,rt<<1);
else update(L,C,m+1,r,rt<<1|1);
PushUp(rt);//子节点更新了,所以本节点也需要更新信息
}
void Update(int L,int R,int C,int l,int r,int rt){
//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号 (区间修改,A[L,R] += C)
if(L <= l && r <= R){
//如果本区间完全在操作区间[L,R]以内
Sum[rt]+=C*(r-l+1);//更新数字和,向上保持正确
Add[rt]+=C;//增加Add标记,表示本区间的Sum正确,子区间的Sum仍需要根据Add的值来调整
return ;
}
int m=(l+r)>>1;
PushDown(rt,m-l+1,r-m);//下推标记
//这里判断左右子树跟[L,R]有无交集,有交集才递归
if(L <= m) Update(L,R,C,l,m,rt<<1);
if(R > m) Update(L,R,C,m+1,r,rt<<1|1);
PushUp(rt);//更新本节点信息
}
int Query(int L,int R,int l,int r,int rt){
//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号
if(L <= l && r <= R){
//在区间内,直接返回
return Sum[rt];
}
int m=(l+r)>>1;
//下推标记,否则Sum可能不正确
PushDown(rt,m-l+1,r-m);
//累计答案
int ANS=0;
if(L <= m) ANS+=Query(L,R,l,m,rt<<1);
if(R > m) ANS+=Query(L,R,m+1,r,rt<<1|1);
return ANS;
}
int main()
{
int t;
int cur = 0;
sd(t);
while(t--)
{
printf("Case %d:\n",++cur);
int n;
sd(n);
for(int i = 1;i <= n;i++)
{
cin>>A[i];
}
Build(1,n,1);
string s;
while(cin>>s)
{
if(s == "End")break;
int a,b;
sd(a),sd(b);
if(s == "Query")
{
int res;
res = Query(a,b,1,n,1);
cout<<res<<endl;
}
else if(s == "Add")
{
update(a,b,1,n,1);
}
else if(s == "Sub")
{
update(a,-b,1,n,1);
}
}
//for(int i = 1;i <= len*4;i++)cout<<Sum[i];
}
}
Just a Hook HDU - 1698

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
const int maxn = 1e5+10;
using namespace std;
int Sum[maxn<<2];//A[maxn<<2];//Sum求和,A为原数组(根据题目更改)
int num[maxn<<2];
int len;
void PushUp(int node)//向上更新节点信息
{
Sum[node]=Sum[node<<1]+Sum[node<<1|1];
}
void pushdown(int node,int node1){
if(num[node]){
int t=num[node];
num[node<<1]=t;
num[node<<1|1]=t;
Sum[node<<1]=(node1-(node1>>1))*t;
Sum[node<<1|1]=(node1>>1)*t;
num[node]=0;
}
}
void Build(int l,int r,int node){
//[l,r]表示当前节点区间,node表示当前节点的实际存储位置
if(l==r) {
//若到达叶节点
Sum[node]=1;//存储A数组的值
return;
}
int m=(l+r)>>1;
//左右递归
Build(l,m,node<<1);
Build(m+1,r,node<<1|1);
//更新信息
PushUp(node);
}
void update(int l,int r,int add,int le,int ri,int node){
if(l<=le&&ri<=r){
num[node]=add;
Sum[node]=add*(ri-le+1);
return;
}
pushdown(node,ri-le+1);
int t=(le+ri)>>1;
if(l<=t) update(l,r,add,le,t,node<<1);
if(r>t) update(l,r,add,t+1,ri,node<<1|1);
PushUp(node);
}
int Query(int L,int R,int l,int r,int node){
//求和
//[L,R]表示操作区间,[l,r]表示当前区间,node表示当前节点编号
if(L <= l && r <= R){
//在区间内直接返回
return Sum[node];
}
int m=(l+r)>>1;
//左子区间:[l,m] 右子区间:[m+1,r] 求和区间:[L,R]
//累加答案
int ANS=0;
if(L <= m) ANS+=Query(L,R,l,m,node<<1);//左子区间与[L,R]有重叠,递归
if(R > m) ANS+=Query(L,R,m+1,r,node<<1|1); //右子区间与[L,R]有重叠,递归
return ANS;
}
int main()
{
int t,T;
int R = 1;
int a,b,c;
scanf("%d",&t);
while(t--)
{
cin>>len>>T;
memset(num,0,sizeof(num));
//for(int i = 1;i <= len;i++)A[i] = 1;
Build(1,len,1);
while(T--)
{
scanf("%d%d%d",&a,&b,&c);
update(a,b,c,1,len,1);
}
printf("Case %d: The total value of the hook is %d.\n",R++,Sum[1]);
//for(int i = 1;i <= len*4;i++)cout<<Sum[i];
}
}
个人模板
#include <iostream>
#include <cstdio>
#include <cstring>
#include <map>
#include <cmath>
#include <algorithm>
#define inf 0x3f3f3f3f
#define sd(a) scanf("%d",&a)
#define mem0(a) memset(a,0,sizeof(a))
#define ls l,m,rt<<1
#define rs m+1,r,rt<<1|1
typedef long long ll;
const int mod = 1e9+7;
const int maxn = 30010;
using namespace std;
ll Sum[maxn<<2],Add[maxn<<2];//Sum求和,Add为懒惰标记
ll A[maxn],n;//存原数组数据下标[1,n]
void PushDown(int rt,int ln,int rn){
//ln,rn为左子树,右子树的数字数量。
if(Add[rt]){
//下推标记
Add[rt<<1]+=Add[rt];
Add[rt<<1|1]+=Add[rt];
//修改子节点的Sum使之与对应的Add相对应
Sum[rt<<1]+=Add[rt]*ln;
Sum[rt<<1|1]+=Add[rt]*rn;
//清除本节点标记
Add[rt]=0;
}
}
//PushUp函数更新节点信息 ,这里是求和
void PushUp(int rt){
Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];}
//Build函数建树
void Build(int l,int r,int rt){
//l,r表示当前节点区间,rt表示当前节点编号
if(l==r) {
//若到达叶节点
Sum[rt]=A[l];//储存数组值
return;
}
int m=(l+r)>>1;
//左右递归
Build(l,m,rt<<1);
Build(m+1,r,rt<<1|1);
//更新信息
PushUp(rt);
}
void update(int L,int C,int l,int r,int rt){
//l,r表示当前节点区间,rt表示当前节点编号(点修改,A[L] += C)
if(l==r){
//到叶节点,修改
Sum[rt]+=C;
return;
}
int m=(l+r)>>1;
//根据条件判断往左子树调用还是往右
if(L <= m) update(L,C,l,m,rt<<1);
else update(L,C,m+1,r,rt<<1|1);
PushUp(rt);//子节点更新了,所以本节点也需要更新信息
}
void Update(int L,int R,int C,int l,int r,int rt){
//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号 (区间修改,A[L,R] += C)
if(L <= l && r <= R){
//如果本区间完全在操作区间[L,R]以内
Sum[rt]+=C*(r-l+1);//更新数字和,向上保持正确
Add[rt]+=C;//增加Add标记,表示本区间的Sum正确,子区间的Sum仍需要根据Add的值来调整
return ;
}
int m=(l+r)>>1;
PushDown(rt,m-l+1,r-m);//下推标记
//这里判断左右子树跟[L,R]有无交集,有交集才递归
if(L <= m) Update(L,R,C,l,m,rt<<1);
if(R > m) Update(L,R,C,m+1,r,rt<<1|1);
PushUp(rt);//更新本节点信息
}
ll Query(int L,int R,int l,int r,int rt){
//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号
if(L <= l && r <= R){
//在区间内,直接返回
return Sum[rt];
}
int m=(l+r)>>1;
//下推标记,否则Sum可能不正确
PushDown(rt,m-l+1,r-m);
//累计答案
ll ANS=0;
if(L <= m) ANS+=Query(L,R,l,m,rt<<1);
if(R > m) ANS+=Query(L,R,m+1,r,rt<<1|1);
return ANS;
}