线段树:是一棵树,而且还是一颗二叉树,树上的每个节点,对应于一个线段(区间),同一层的节点所代表的区间,相互不会重叠,叶子节点的区间是单位长度,不能再分了。二叉树的左儿子对应的区间为【a,(a+b)/2】,右儿子表示的区间为【(a+b)/2+1,b】。
线段树的操作:
(1):建树:
void build(int l,int r,int rt)
{
if(l == r)//找到叶子节点,将要存入的值存进去
{
scanf("%d",&w[rt]);
return;
}
int mid = (r+l)/2;//找到二分的中间点 mid = (r + l) >> 1;//也可以这么写,>>为右移运算符,右移几就说明除以2的多少次方
build(l,mid,rt*2);//建立左孩子 rt * 2 也可以写成: rt << 1,<<为右移运算符,右移几位就代表乘以2的多少次方
build(mid+1,r,rt*2+1);//建立右孩子 rt * 2 + 1 也可以写成 rt << 1 | 1;"|"为亦或运算符,取二进制的最后一位,是0变成1,1变成0.
w[rt] = w[rt*2] + w[rt*2+1];//父亲节点是两个孩子的加和,或是根据题目中要求进行修改
}
void add(int p,int d,int l,int r,int w)
{
if(l==r)//找到这个值并更新
{
sum[w]+=d;
return ;
}
int m = (l+r)/2;//二分查找,选择左孩子还是右孩子
if(p<=m)
add(p,d,l,m,2*w);
else
add(p,d,m+1,r,2*w+1);
push(w);//按照题意更新相关的父亲节点
}
(3):区间求和:
int query(int p1,int p2,int l,int r,int w)
{
if(p1<=l&&p2>=r)//区间被完全覆盖
return sum[w];
int m = (l+r)/2;
int re = 0;
if(p1<=m)
re+=query(p1,p2,l,m,2*w);//说明左区间在左子树上
if(p2>m)
re+=query(p1,p2,m+1,r,2*w+1);//说明右区间在右子树上
return re;//返回该值
}
(4):区间更新
区间更新和单节点更新不同的就是区间更新是对区间内的每一个节点都要更新,所以就要用到标记数组,说明在该区间更新过,以后在访问该区间的子节点的时候,就要对其进行下放,更新其子节点。而在区间更新之后的查询也是如此操作,每访问一个子节点,都要进行下放。
long long w[maxin<<2];
long long lz[maxin<<2];
void pushdown(int rt,int m)
{
if(lz[rt])
{
lz[rt<<1]+=lz[rt];
lz[rt<<1|1]+=lz[rt];
w[rt<<1]+=lz[rt]*(m-(m>>1));
w[rt<<1|1]+=lz[rt]*(m>>1);
lz[rt] = 0;//下放之后原父节点的标记就要变成没有变化
}
}
void change(int x,int y,int z,int l,int r,int rt)//将x到y的区间内的值都加上z。
{
if(l > y || r < x)
return ;
if(l >= x && r <= y)
{
w[rt] += (long long)(l-r+1)*z;//对于每一个值都加上z
lz[rt] += z;//lz数组标记该区间加过z
return ;
}
pushdown(rt,r-l+1);//对于lz数组的标记,将其往下压至它的子节点
int mid = (l+r)>>1;
change(x,y,z,l,mid,rt<<1);
change(x,y,z,mid+1,r,rt<<1|1);
w[rt] = w[rt<<1] + w[rt<<1|1];//更新父节点
}
long long que(int x,int y,int l,int r,int rt)//查询从x到y区间内的和
{
if(l > y || r < x)
return 0;
if(l >= x && r <= y)
return w[rt];
pushdown(rt,r-l+1);
int mid = (l+r)>>1;
return que(x,y,l,mid,rt<<1)+que(x,y,mid+1,r,rt<<1|1);
}