啊啊啊之前的博文没法保存,又得重写。—2019.3.26
被迫刷新,重写×3。—2019.3.27
线段树的概念
线段树是擅长处理区间的数据结构,他可以用O(logn)的时间对任意区间进行操作。线段树是一颗完美二叉树,及如下图所示,树的所有叶子的深度都相同,并且每个节点要么是叶子要么有两个子树。树上的每个节点都维护一个区间,根维护的是整个区间,每个节点维护的是父亲的区间二等分后的其中一个子区间。
建树
首先我们要了解线段树的储存形式。和二叉树一样,线段树依旧是用一维数组储存的,每个节点的子树所在数组的位置是它的父节点的下标乘二和下标乘二加一。那么,对于上图,储存方式则为:
连接表示父子关系。由此我们大致可知,线段树由类似二分的方法建立:
struct node{
int left,right,num,tag;//左端点,右端点,值,lazytag标记(后面区间修改会讲作用)
}infor[maxn];
void build_tree(int pos,int l,int r){//当前下标,左端点,右端点
infor[pos].l = l;
infor[pos].r = r;
if(l==r){
scanf("%d",&infor[pos].num);//由二分可知,叶子节点的建立必定是从左往右的
return;
}
int mid=(l+r)>>1;
build_tree(pos<<1,l,mid); //左子树
build_tree(pos<<1|1,mid+1,r);//右子树
update_sum(pos);//更新方式
//update_min(pos);
//update_max(pos);}
由线段树所谓维护的属性不同,我们有不同的更新方式:
void update_sum(int pos){
infor[pos].num=infor[pos<<1].num+infor[pos<<1|1].num;
}
void update_max(int pos){
infor[pos].num=max(infor[pos<<1].num,infor[pos<<1|1].num);
}
void update_min(int pos){
infor[pos].num=min(infor[pos<<1].num,infor[pos<<1|1].num);
}
至此,建树完成。
关于更新函数的位置摆放:在初次学习时,我有过是否会在子树赋值前就更新了自己导致错误的想法,再读几遍后发现完全是自己傻了,更新操作是在子树建树完成后才进行的,所以子树必有值。
区间查询
单点查询就是区间查询的特化
我们经常会遇到这样的问题,让你求某区间的和、最大值、最小值,普通方法只能在O(n)时间内完成,而线段树的优良储存方式可以使时间降到O(logn)。由于建树是基于二分的,所以需查询的区间可能为多个区间组成,需要不断向下搜索直至找到符合要求的所有区间。查询方式也是类似二分。
int sum;//mx、mi等查询内容
void query(int pos,int l,int r){//当前下标,需查询的左端点、右端点
if(infor[pos].left==l&&infor[pos].right==r){//找到匹配区间
sum+=infor[pos].num;//查询内容
//mx=max(mx,infor[pos].num);
//mi=min(mi,infor[pos].num);
return;
}
pushdown(pos);//区间修改时用
int i=pos<<1;//左子树
if(l<=infor[i].right){//需查询区间与左子树有交集
if(r<=infor[i].right) query(i,l,r);//全在左子树内
else query(i,l,infor[i].right);//部分在
}
i++;//右子树
if(r>=infor[i].left){//需查询区间与右子树有交集
if(l>=infor[i].left) query(i,l,r);//全在右子树内
else query(i,infor[i].left,r);//部分在
}
}
依旧是上图,如果我要查(3,6)区间内元素的和,那么,最后查询的区间为下图:
查询的过程是这样的:
1.query(1,3,6);(无论如何查询,第一次的下标都是从1开始)
2.query(2,3,3);
3.query(5,3,3);
4.query(10,3,3);(此时下标与查询区间对应,求和一次)
5.query(3,4,6); (回溯再搜)
6.query(6,4,5); (此时下标与查询区间对应,求和一次)
7.query(7,6,6); (回溯再搜)
8.query(14,6,6)(此时下标与查询区间对应,求和一次)
结束
其余各类查询操作类似。
区间修改
区间加减
虽然O(n)的方法必然被爆,但还是得铺垫滴
需要使用区间修改的题一般会有这样的描述:把区间(x,y)内的所有元素加b,由查询的写法可知,依旧二分,找到所有相关区间,更新他们的值即可。那么此时的时间复杂度就是O(n),这肯定是不行滴。但此时如果想降低时间复杂度,只能通过降低到O(logn)的更新时间来实现。
此时,就要引进lazytag这一变量来降低时间(lazy 懒人必备啊),它用于表示当前区间的子区间需要更新的值,每次查询到符合要求的整区间后打上lazytag标记即可。具体操作先附代码:
void add(int p,int l,int r,int k){//当前区间下标,需修改的左端点,需修改的右端点
if(l==infor[p].left&&r==infor[p].right){//找整区间
infor[p].num+=(r-l+1)*k;
infor[p].tag+=k;
return;
}
pushdown(p);//向下更新数值并tag下移
int i=p<<1;//左子树
if(l<=infor[i].right){//左子树内有
if(r<=infor[i].right) add(i,l,r,k);//全在左子树
else add(i,l,infor[i].right,k);//部分在
}
i++;//右子树
if(r>=infor[i].left){//右子树内有
if(l>=infor[i].left) add(i,l,r,k);//全在右
else add(i,infor[i].left,r,k);//部分在
}
update_sum(p);//向上更新数值
//update_max(p);
//update_min(p);
}
void pushdown(int n){//lazytag的下放,在每次用到当前区间时都下放一次
if(!infor[n].tag) return;
infor[n<<1].num+=infor[n].tag*(infor[n<<1].right-infor[n<<1].left+1);//左子树更新
infor[n<<1|1].num+=infor[n].tag*(infor[n<<1|1].right-infor[n<<1|1].left+1);//右子树更新
infor[n<<1].tag+=infor[n].tag;
infor[n<<1|1].tag+=infor[n].tag;//tag下移 i
nfor[n].tag=0;//清tag
}
看不懂?没关系。看懂了当我没说 那么现在我们先来梳理一下整个过程。其实主体部分是和区间查询一致的,修改是建立在查询基础上的。当我们找到某一符合要求的区间,就对整体进行修改,此时:infor[pos].num+=(r-l+1)*k; 但我们此时更新的仅有下标pos对应的一个区间,但实际应向下向上都更新。
为了更详细地说明,我们以把区间(3,6)内所有元素都加3为例:
一次查询后直接操作结果图例:(图内仅标注修改内容,尚未更新其余区间)
在此操作完成后,我们确实实现了了对区间(3,6)整体元素加3,但具体只对这4个区间段进行了修改,若此时想得出区间(0,7)或区间(4,4)的值,都是无法直接实现的,他们暂时都未进行过更新,所包含的值都是原值。此时,就需要引入pushdown操作。pushdown操作的原理是在查询到每一个区间时都检测一次lazytag,若不为0,则更新其子区间的值和lazytag,并清空当前区间的lazytag。由于线段树查询的特性是逐层深入,那么在查询每一个子区间前,它都已经完成了更新,于是就将O(n)更新的向下更新操作在查询时解决。至于向上更新的部分,只要在函数末尾update一下即可(毕竟子区间已完成更新)。
那么对于上述解释,我们在上图基础上把区间(2,7)整体加2,来进一步解释。
完成操作后如下图所示:(仅标注此次修改和pushdown过程,尚未update)
在这第二次操作中,大部分与先前操作类似,但区间(4,5)需要额外操作。在查询到此区间时,发现tag不为0,那么执行pushdown,把它的子区间(4,4)(5,5)的值整体加3,并tag加3,原区间tag清0。再仿照上述操作修改当前区间,后续再update。这样就保证了在每次访问时,所有的区间使用时都是最新值。
总的来说,线段树O(logn)的区间修改是将O(n)的更新与查询合并,在每次查询前进行更新操作。想要详细了解,还得多读代码多看过程,在脑中模拟整个操作过程,想明白后会发现线段树并不难。
这里提供洛谷模板题P3372线段树1和AC代码供练习:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=1e5+10;
typedef long long int ll;
struct node{
int left,right,tag;//左右区间,标记
ll num;//所存的值
}infor[4*maxn];//四倍空间
int n,m;
ll all;
void get_sum(int p){
infor[p].num=infor[p<<1].num+infor[p<<1|1].num;//向上更新
}
void build_tree(int p,int l,int r){//建树
infor[p].left=l;
infor[p].right=r;//当前左右
infor[p].tag=0;
if(l==r){//末点
//cin>>infor[p].num;//输入值
scanf("%d",&infor[p].num);
return;
}
int mid=(l+r)>>1;
build_tree(p<<1,l,mid);
build_tree(p<<1|1,mid+1,r);//建左右树
get_sum(p);//全部建完之后存值
}
void pushdown(int n){//lazytag的下放,在每次用到当前区间时都下放一次
if(!infor[n].tag) return;
infor[n<<1].num+=infor[n].tag*(infor[n<<1].right-infor[n<<1].left+1);//左子树更新
infor[n<<1|1].num+=infor[n].tag*(infor[n<<1|1].right-infor[n<<1|1].left+1);//右子树更新
infor[n<<1].tag+=infor[n].tag;
infor[n<<1|1].tag+=infor[n].tag;//tag下移
infor[n].tag=0;//清tag
}
void add(int p,int l,int r,int k){//区间加法
if(l==infor[p].left&&r==infor[p].right){//找整区间
infor[p].num+=(r-l+1)*k;
infor[p].tag+=k;
return;
}
pushdown(p);//向下更新数值并tag下移
int i=p<<1;//左子树
if(l<=infor[i].right){//左子树内有
if(r<=infor[i].right) add(i,l,r,k);//全在左子树
else add(i,l,infor[i].right,k);//部分在
}
i++;//右子树
if(r>=infor[i].left){//右子树内有
if(l>=infor[i].left) add(i,l,r,k);//全在右
else add(i,infor[i].left,r,k);//部分在
}
get_sum(p);//向上更新数值
}
void query(int p,int l,int r){//区间和
if(l==infor[p].left&&r==infor[p].right){
all+=infor[p].num;
return;
}
pushdown(p);//tag下移
int i=p<<1;//左
if(l<=infor[i].right){
if(r<=infor[i].right) query(i,l,r);
else query(i,l,infor[i].right);
}
i++;//右
if(r>=infor[i].left){
if(l>=infor[i].left) query(i,l,r);
else query(i,infor[i].left,r);
}
}
void act1(){//输入1时
int a,b,c;
//cin>>a>>b>>c;
scanf("%d%d%d",&a,&b,&c);
add(1,a,b,c);
}
void act2(){//输入2时
int a,b;
//cin>>a>>b;
scanf("%d%d",&a,&b);
all=0;
query(1,a,b);
cout<<all<<endl;
}
int main(){
cin>>n>>m;
build_tree(1,1,n);//建树
for(int i=1;i<=m;i++){
int a;
//cin>>a;
scanf("%d",&a);
if(a==1) act1();
else act2();
}
return 0;
}
此外,另附同学的另一种线段树写法,貌似比我快个300ms:线段树(简单实现高效区间操作) 虽然我感觉并没有什么大的区别,估计是常数比我小。
区间乘法
在这记录一下我debug一天结果发现只是多写了一个加号的悲惨经历
对于区间中每个元素都乘以某一个数(区间记录的数为元素值的和),它的具体操作其实和区间加法并无多大区别,用相似的操作即可完成。但在lazytag的操作上我们需要多加考虑。在这里,我们指出,一切操作乘法优先。在下移时,应乘以tagmul,然后才是加法(此法更为简便)。那么,接下来的问题就是如何进行具体操作:
我们现在假设进行三次操作,对区间(4,7)整体加2,对区间(6,7)整体乘3,然后查询区间(6,6)的值。
第一步:区间(4,7)整体加2
记录tagadd=2,区间值加(7-4+1)×2;
结果:
第二步:对区间(6,7)整体乘3
首先pushdown:区间值整体加(7-6+1)×2,tagadd=2,上区间tag清0。
然后乘三操作:对区间值整体乘3,同时把tagadd乘3,更新tagmul=3;
结果:
第三步:查询区间(6,6)的值
依旧是pushdown操作:区间先整体乘3,然后再加6,并更新两个tag标记。
结果:
总的来说,在所有操作中,都是先进行乘法操作,再进行加法,且过程中tagadd必须与tagmul保持联动。具体原因可通过实验获得。
也可以尝试进行加法优先的操作,会发现无法准确的进行值的继承,或是要更繁复的步骤。有兴趣的人可以自行尝试。我懒
这里再附上含有区间乘法的线段树模板题P3373线段树2和AC代码:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 1e5+10;
typedef long long int ll;
ll n,m,mod,sum;
struct node{
ll left,right,tagadd,tagmul,num;
}infor[4*maxn];
void update(ll pos,ll mod){
infor[pos].num=(infor[pos<<1].num+infor[pos<<1|1].num)%mod;
}
void build_tree(ll pos,ll l,ll r,ll mod){
infor[pos].left=l;
infor[pos].right=r;
infor[pos].tagmul=1;
if(l==r){
//cin>>infor[pos].num;
scanf("%lld",&infor[pos].num);
infor[pos].num%=mod;
return;
}
ll mid=(l+r)/2;
build_tree(pos<<1,l,mid,mod);
build_tree(pos<<1|1,mid+1,r,mod);
update(pos,mod);
}
void pushdown(ll pos,ll mod){
if(infor[pos].tagadd==0&&infor[pos].tagmul==1) return;
infor[pos<<1].num*=infor[pos].tagmul;
infor[pos<<1].num+=infor[pos].tagadd*(infor[pos<<1].right-infor[pos<<1].left+1);
infor[pos<<1].num%=mod;
infor[pos<<1|1].num*=infor[pos].tagmul;
infor[pos<<1|1].num+=infor[pos].tagadd*(infor[pos<<1|1].right-infor[pos<<1|1].left+1);
infor[pos<<1|1].num%=mod;
infor[pos<<1].tagmul*=infor[pos].tagmul;
infor[pos<<1].tagmul%=mod;
infor[pos<<1|1].tagmul*=infor[pos].tagmul;
infor[pos<<1|1].tagmul%=mod;
infor[pos<<1].tagadd=infor[pos<<1].tagadd*infor[pos].tagmul+infor[pos].tagadd;
infor[pos<<1].tagadd%=mod;
infor[pos<<1|1].tagadd=infor[pos<<1|1].tagadd*infor[pos].tagmul+infor[pos].tagadd;
infor[pos<<1|1].tagadd%=mod;
infor[pos].tagadd=0;
infor[pos].tagmul=1;
}
void add(ll pos,ll l,ll r,ll k,ll mod){
if(l==infor[pos].left&&r==infor[pos].right){
infor[pos].num+=(r-l+1)*k;
infor[pos].num%=mod;
infor[pos].tagadd+=k;
infor[pos].tagadd%=mod;
return;
}
pushdown(pos,mod);
if(l<=infor[pos<<1].right){
if(r<=infor[pos<<1].right) add(pos<<1,l,r,k,mod);
else add(pos<<1,l,infor[pos<<1].right,k,mod);
}
if(r>=infor[pos<<1|1].left){
if(l>=infor[pos<<1|1].left) add(pos<<1|1,l,r,k,mod);
else add(pos<<1|1,infor[pos<<1|1].left,r,k,mod);
}
update(pos,mod);}
void mul(ll pos,ll l,ll r,ll k,ll mod){
if(l==infor[pos].left&&r==infor[pos].right){
infor[pos].num*=k;
infor[pos].num%=mod;
infor[pos].tagmul*=k;
infor[pos].tagadd*=k;
infor[pos].tagmul%=mod;
infor[pos].tagadd%=mod;
return;
}
pushdown(pos,mod);
if(l<=infor[pos<<1].right){
if(r<=infor[pos<<1].right) mul(pos<<1,l,r,k,mod);
else mul(pos<<1,l,infor[pos<<1].right,k,mod);
}
if(r>=infor[pos<<1|1].left){
if(l>=infor[pos<<1|1].left) mul(pos<<1|1,l,r,k,mod);
else mul(pos<<1|1,infor[pos<<1|1].left,r,k,mod);
}
update(pos,mod);
}
void query(ll pos,ll l,ll r,ll mod){
if(l==infor[pos].left&&r==infor[pos].right){
sum+=infor[pos].num;
sum%=mod;
return;
}
pushdown(pos,mod);
if(l<=infor[pos<<1].right){
if(r<=infor[pos<<1].right) query(pos<<1,l,r,mod);
else query(pos<<1,l,infor[pos<<1].right,mod);
}
if(r>=infor[pos<<1|1].left){
if(l>=infor[pos<<1|1].left) query(pos<<1|1,l,r,mod);
else query(pos<<1|1,infor[pos<<1|1].left,r,mod);
}
}
int main(){
//cin>>n>>m>>mod;
scanf("%lld%lld%lld",&n,&m,&mod);
build_tree(1,1,n,mod);
while(m--){
ll a;
//cin>>a;
scanf("%lld",&a);
if(a==1){
ll l,r,k;
//cin>>l>>r>>k;
scanf("%lld%lld%lld",&l,&r,&k);
mul(1,l,r,k,mod);
}else if(a==2){
ll l,r,k;
//cin>>l>>r>>k;
scanf("%lld%lld%lld",&l,&r,&k);
add(1,l,r,k,mod);
}else if(a==3){
sum=0;
ll l,r;
//cin>>l>>r;
scanf("%lld%lld",&l,&r);
query(1,l,r,mod);
//cout<<sum<<endl;
printf("%lld\n",sum);
}
}
return 0;
}