线段树原理 and 例题

线段树的原理是根据满二叉树得出的一棵满二叉树一定可以储存在一个一位的数组中其中

如果当前节点为u那么其子节点为u<<1和u<<1|1

一颗不包括pushdown的操作的线段树可以实现单点修改区间查询而包括了懒标记和pushup就可以区间修改区间查询了

主要的原理就是我们把一个完全的区间看成一个根把区间分为左右两个 [l,mid] [mid+1,r]那么这就是它的左右子树 直到分的只有一个元素的时候就停止 一般情况下节点的个数就是N*4

#include<bits/stdc++.h>

using namespace std;

#define endl '\n'

const int N=2e5+10;

typedef long long ll;

int m,p;

struct node{

       int l,r,v;//根据求的性质不同 结构体存储的数据也不同 pushup也可能不同

}tr[N*4];

void pushup(int u){

       tr[u].v=max(tr[u<<1].v,tr[u<<1|1].v);

}

void build(int u,int l,int r){

       tr[u]={l,r};

       if(l==r)return ;//如果只有一个元素就停止

       int mid=l+r>>1;

       build(u<<1,l,mid);//左

       build(u<<1|1,mid+1,r);//右

}

int query(int u,int l,int r){

       if(tr[u].l>=l&&tr[u].r<=r)return tr[u].v;//如果查询区间完全包括当前节点区间那么直接返回信息

       int mid=tr[u].l+tr[u].r>>1;

       int v=0;

       if(l<=mid)v=query(u<<1,l,r);//如果和左边的有交集就查询左边

       if(r>mid)v=max(v,query(u<<1|1,l,r));如果和右边的有交集就查询右边

       return v;//返回信息

}

void modify(int u,int x,int v){

       if(tr[u].l==x&&tr[u].r==x)tr[u].v=v;

       else{

              int mid=tr[u].l+tr[u].r>>1;

              if(x<=mid)modify(u<<1,x,v);

              else modify(u<<1|1,x,v);

              pushup(u);

       }

}

int main(){

       ios::sync_with_stdio(false);

       cin.tie(0);

       cout.tie(0);

       int n=0,last=0;

       int x;

       char op[2];

       cin>>m>>p;

       build(1,1,m);

       while(m--){

              cin>>op>>x;

              if(*op=='Q'){

                     last=query(1,n-x+1,n);

                     cout<<last<<endl;

              }

              else{

                     modify(1,n+1,((LL)last+x)%p);

                     n++;

              }

       }

}

例题1你能回答这些问题吗

        

e944cf429e6040a29a5754e584add531.png

不需要懒标记的线段树 但是一个节点储存的信息不再是最大值这么简单

首先 我要对query操作进行一些理解和补充 在build操作里我们pushup是直接根据

已经划分好的区间 通过子节点对父节点信息 进行更新 但是在query的时候我们的所查询的区间可能不会刚刚好就来自通一个父节点的直接子节点就是说

88f8545548364f6fbfac53999ccc7e39.jpeg

像这样我们查询的区间来自两个红色的节点 那么

我们的u就不能更新通过 tr[u] tr[u<<1] tr[u<<1|1]而是通过这两个区间来更新

那么我们就分别需要三个node

一个表示 答案 res 一个表示右区间right 一个表示左区间left

push(res,right,left);

retrun res;

具体代码

node query(int u,int l,int r)

{

 If(tr[u].l>=l&&tr[u].r<=r)return tr[u];//当我们的当前节点的区间完全被查询的时候直接返回

int mid=tr[u].l+tr[u].r>>1;

if(r<=mid)return query(u<<1,l,r);//当我们查询的区间完全在左边就直接返回左边查询结果

else if(l>mid) return query(u<<1|1,l,r);//当我们查询的区间完全在右边就直接返回右边查询结果

else{//两边皆包括一部分

node res;

node left=query(u<<1,l,r);

node right=query(u<<1|1,l,r);

pushup(res,left,right);

return res;

}

而在有些简单的只储存一个信息的节点的时候 我们也可以不pushup而是直接计算 然后讲答案返回

那么这个题目我们最大连续区间

可能完全来自左边 可能完全来自右边 可能有一部分来自左边 有一部分来自右边

而一部分来自左边一部分来自右边的一定是左边的最大后缀和与右边的最大前缀和的和

而最大后缀和可能是其右区间的最大后缀和也可能是其右区间的总和和其左区间的最大后缀和最大前缀和同理

所以一个节点需要储存的信息 有:最大连续子段和,最大前缀和,最大后缀和,总和

Code:

#include<bits/stdc++.h>

#define endl '\n'

using namespace std;

const int N=500010;

typedef long long ll;

typedef pair<int,int>PII;

int w[N];

struct node{

       int l,r;

       int sum;

       int tsum;

       int lsum,rsum;

}tr[N*4];

void pushup(node&l,node&r,node&u){

       u.sum=l.sum+r.sum;

       u.lsum=max(l.lsum,l.sum+r.lsum);

       u.rsum=max(r.rsum,r.sum+l.rsum);

       u.tsum=max(max(l.tsum,r.tsum),l.rsum+r.lsum);

}

void pushup(int u){

       pushup(tr[u<<1],tr[u<<1|1],tr[u]);

}

void build(int u,int l,int r){

       if(l==r){

              tr[u]={l,r,w[l],w[l],w[l],w[l]};

              return ;  

       }

       tr[u]={l,r};

       int mid=l+r>>1;

       build(u<<1,l,mid);

       build(u<<1|1,mid+1,r);

       pushup(u);

}

node query(int u,int l,int r){

       if(l<=tr[u].l&&tr[u].r<=r)return tr[u];

       int mid=tr[u].l+tr[u].r>>1;

       if(r<=mid)return query(u<<1,l,r);

       if(l>mid)return query(u<<1|1,l,r);

       node left=query(u<<1,l,r);

       node right=query(u<<1|1,l,r);

       node res;

       pushup(left,right,res);

       return res;

}

void modify(int u,int x,int v){

       if(tr[u].l==x&&tr[u].r==x){

              tr[u].lsum=tr[u].rsum=tr[u].sum=tr[u].tsum=v;

              return ;

       }

       int mid=(tr[u].l+tr[u].r)>>1;

       if(x<=mid)modify(u<<1,x,v);

       else modify(u<<1|1,x,v);

       pushup(u);

}

int main(){

       ios::sync_with_stdio(false);

       cin.tie(0);

       cout.tie(0);

       int n,m;

       cin>>n>>m;

       for(int i=1;i<=n;i++)cin>>w[i];

       build(1,1,n);

       while(m--){

              int k,x,y;

              cin>>k>>x>>y;

              if(k==1){

                     if(x>y)swap(x,y);

                     node res=query(1,x,y);

                     cout<<res.tsum<<endl;

              }

              else{

                     w[x]=y;

                     modify(1,x,y);

              }

       }

}

例题2区间最大公约数

751865d794774c90bb07b0d6fa050d68.png

没有懒标记的线段树 适用于单点修改区间查询

但是这个题目对区间修改了 乍一看没法用线段树 但是只是对区间加减没有直接使得一个区间变成一个元素这种操作 那么我们可以用差分处理 仍是单点修改

值得一提的是gcd存在这样一个性质

求gcd(a1,a2)=gcd(a1,a2-a1)

gcd(a1,a2,a3,a4...)=gcd(a1,a2-a1,a3-a2,a4-a3...)

那么 差分数组 bi=ai-ai-1那么这不就是一个差分数组吗

只不过第一个点不是而已

而第一个点可以通过计算差分数组的前缀和得出

也可以用线段树来实现

所以线段树里存的信息就是 gcd(al-al-1,al+1-al...,ar-1-ar-2,ar-ar-1)和sum[r]-sum[l-1]

查询的时候需要查询两次一次是找sum[l]一次是找gcd(bl+1,bl+2,..br-1,br+2)

bl=al-al-1;

修改的时候也是两次

bl+d br+1-d(差分技巧)

code:

#include<bits/stdc++.h>

#define endl '\n'

using namespace std;

const int N=500010;

typedef long long ll;

typedef pair<int,int>PII;

ll b[N];

int n,m;

struct node{

       int l,r;

       ll d,sum;

}tr[4*N];

ll gcd(ll a,ll b){

       return b==0?a:gcd(b,a%b);

}

void pushup(node&u,node&l,node&r){

       u.d=gcd(l.d,r.d);

       u.sum=l.sum+r.sum;

}

void pushup(int u){

       pushup(tr[u],tr[u<<1],tr[u<<1|1]);

}

void build(int u,int l,int r){

       if(l==r){

              tr[u]={l,r,b[l],b[l]};

              return ;

       }

       tr[u].l=l,tr[u].r=r;

       int mid=l+r>>1;

       build(u<<1,l,mid);

       build(u<<1|1,mid+1,r);

       pushup(u);

}

node query(int u,int l,int r){

       if(tr[u].l>=l&&tr[u].r<=r)return tr[u];

       int mid=tr[u].l+tr[u].r>>1;

       ll d=0;

       if(r<=mid)return query(u<<1,l,r);

       else if(l>mid)return query(u<<1|1,l,r);

       else{

              node res;

              node left=query(u<<1,l,r);

              node right=query(u<<1|1,l,r);

              pushup(res,left,right);

              return res;

       }

}

void modify(int u,int x,ll v){

       if(tr[u].l==x&&tr[u].r==x){

              tr[u].d+=v;

              tr[u].sum+=v;

              return ;

       }

       int mid=tr[u].l+tr[u].r>>1;

       if(x<=mid)modify(u<<1,x,v);

       else modify(u<<1|1,x,v);

       pushup(u);

}

int main(){

       ios::sync_with_stdio(false);

       cin.tie(0);

       cout.tie(0);

       cin>>n>>m;

       ll now,last=0;

       for(int i=1;i<=n;i++){

              cin>>now;

              b[i]=now-last;

              last=now;

       }

       build(1,1,n);

       char op[2];

       int l,r;

       ll d;

       while(m--){

              cin>>op>>l>>r;

              if(op[0]=='Q'){

                     node res=query(1,1,l);

                     node tt={0,0,0,0};

                     if(l+1<=r)tt=query(1,l+1,r);

                     ll ans=abs(gcd(res.sum,tt.d));

                     cout<<ans<<endl;

              }

              else{

                     cin>>d;

                     modify(1,l,d);

                     if(r!=n)modify(1,r+1,-d);

              }

       }

}

例题3 一个简单的小问题2

 

1773e8219c554024bd78c7b765291385.png

这个就不得不用懒标记了

懒标记的意思是如果我们要修改的区间完全包括了当前节点的区间 那么在这个节点标记即可 然而多次修改的时候 如果当前节点区间并不是完全被包括了 那么我们就需要讲当前节点的懒标记给到其两个子节点而自己的懒标记清0为什么需要这样做呢?看下图

e8c32f1e1a3b45b0bde971e86048578c.jpeg

如在当前节点已经标记了+10 那么我们想在红色区域+5当前节点肯定无法表示 那么就需要往下分 那么被红色框起来的区域+15而其他区域+10同一个标记无法表示所以需要pushdown也就是把标记给到下面

而查询的时候也是如此

如果当前节点的区间并不完全被查询区间包括我们就需要讲当前节点的标记给到左右两个区间

因为懒标记是标记到当前节点就结束了那么下面的节点并没有被更新 我们需要用到下面节点的信息 所以需要pushdown

Code:

#include<bits/stdc++.h>

#define endl '\n'

using namespace std;

const int N=1e5+10;

typedef long long ll;

typedef pair<int,int>PII;

struct node{

       int l,r;

       ll sum,add;

}tr[N*4];

int a[N];

void pushup(node&u,node&l,node&r){

       u.sum=l.sum+r.sum;

}

void pushup(int u){

       pushup(tr[u],tr[u<<1],tr[u<<1|1]);

}

void pushdown(int u){

       if(tr[u].add){

              tr[u<<1].sum+=(ll)(tr[u<<1].r-tr[u<<1].l+1)*tr[u].add;

              tr[u<<1].add+=tr[u].add;

              tr[u<<1|1].sum+=(ll)(tr[u<<1|1].r-tr[u<<1|1].l+1)*tr[u].add;

              tr[u<<1|1].add+=tr[u].add;

              tr[u].add=0;

       }

}

void build(int u,int l,int r){

       if(l==r){

              tr[u]={l,r,a[l],0};

              return ;

       }

       tr[u]={l,r};

       int mid=l+r>>1;

       build(u<<1,l,mid);

       build(u<<1|1,mid+1,r);

       pushup(u);

}

node query(int u,int l, int r){

       if(tr[u].l>=l&&tr[u].r<=r)return tr[u];

       pushdown(u);

       int mid=tr[u].l+tr[u].r>>1;

       if(r<=mid)return query(u<<1,l,r);

       else if(l>mid)return query(u<<1|1,l,r);

       else{

              node res;

              node left=query(u<<1,l,r);

              node right=query(u<<1|1,l,r);

              pushup(res,left,right);

              return res;

       }

}

void modify(int u,int l,int r,int d){

       if(tr[u].l>=l&&tr[u].r<=r){

              tr[u].add+=d;

              tr[u].sum+=(ll)(tr[u].r-tr[u].l+1)*d;

              return ;

       }

       pushdown(u);

       int mid=tr[u].l+tr[u].r>>1;

       if(l<=mid)modify(u<<1,l,r,d);

       if(r>mid)modify(u<<1|1,l,r,d);

       pushup(u);

}

int main(){

       ios::sync_with_stdio(false);

       cin.tie(0);

       cout.tie(0);

       int n,m;

       cin>>n>>m;

       for(int i=1;i<=n;i++)cin>>a[i];

       build(1,1,n);

       while(m--){

              char op[2];

              int l,r,d;

              cin>>op>>l>>r;

              if(op[0]=='C'){

                     cin>>d;

                     modify(1,l,r,d);

              }

              else{

                     node res=query(1,l,r);

                     cout<<res.sum<<endl;

              }

       }

}

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值