线段树专题

线段树

由小区间更新大区间

局限性: 问题的答案必须是两个子区间的答案值运算可以得出

可以用于区间修改,区间查询最值, 最大子段和... 不能用于求解区间众数, 区间最长连续问题

  1. 上拉操作是线段树所有操作的基础,就是用左右儿子的信息更新自己的信息

  2. 分治建树

只有对给定好的数组计算的时候才需要buildTree, 有跟着整个数组扫一遍不需要buildTree

  1. lazytag 是区间修改和查询保证时间复杂度的关键

    用于表示某个节点o还有lz[o]的值尚未更新给子节点, 这个 值x节点个数=实际增加的权值

    当lz[o]==0 说明当前节点的左右儿子都已经被更新了, 得到了正确的值

    使用懒标记后必须配合下放操作, 若一个点的父亲链lz[o]都为0,则这个给节点的值准确

快速线段树(单点修改+区间最值)

注意n为2的幂次,下标从0开始,查询时左闭右开

struct Segt {
    vector<int> w;
    int n;
    Segt(int n) : w(2 * n, (int)-2E9), n(n) {}

    void modify(int pos, int val) {
        for (w[pos += n] = val; pos > 1; pos /= 2) {
            w[pos / 2] = max(w[pos], w[pos ^ 1]);
        }
    }

    int ask(int l, int r) {
        int res = -2E9;
        for (l += n, r += n; l < r; l /= 2, r /= 2) {
            if (l % 2) res = max(res, w[l++]);
            if (r % 2) res = max(res, w[--r]);
        }
        return res;
    }
};
int a[N];
void solve(){
    Segt t(128);
    int n;cin>>n;
    rep(i,1,n)cin>>a[i];
    rep(i,1,5){
        t.modify(i-1,a[i]);//i-1
    }   
    int l,r;cin>>l>>r;
    cout<<t.ask(l-1,r);//l-1 r
}

基础线段树(区间加法和查询)

start, end, node 用来描述线段树, l r 用来描述查询区间

线段是确定的,因此 对于每个编号o(node),它的 start 和end 也是确定的, o==1时,s一定是1,e一定是n

const int N=2e5+5;
int n;
int a[N];
int t[N<<2],lz[N<<2];
#define ls (o<<1)
#define rs (o<<1|1)
void update(int s, int e, int o, int x){//把当前的懒标记更新到树上
    t[o]+=x*(e-s+1);
    lz[o]+=x;
}
void pushup(int o){
    t[o]=t[ls] + t[rs]; 
}
void pushdown(int s, int e, int o){
    if(lz[o]==0)return;
    int mid=(s+e)>>1;//ls=o<<1, rs=o<<1|1;
    update(s,mid,ls,lz[o]);//t[ls]+=lz[o]*(mid-s+1);lz[ls]+=lz[o];标记也要下放
    update(mid+1,e,rs,lz[o]);//t[rs]+=lz[o]*(e-mid);lz[rs]+=lz[o];
    lz[o]=0; //lazytag 下放完毕
}
void buildTree(int s=1, int e=n, int o=1){// start, end, node
    if(s==e){
        t[o]=a[s];return;//当t的某个节点所代表的s和e重合时
    }
    int mid=(s+e)>>1;
    buildTree(s,mid,ls),buildTree(mid+1,e,rs);
    pushup(o);
}
void add(int l, int r, int x, int s=1, int e=n, int o=1){// 给[l,r] 加上x, l,r,x在这个过程中保持不变
	if(l<=s && e<=r){
        // [s,e]区间是[l,r]的子区间时就可以偷懒了
        update(s,e,o,x); //t[o]+=1ll*(e-s+1)*x;lz[o]+=x;
        return;
    }

    pushdown(s,e,o); // [s,e]不是目标区间的子区间,o编号所代表的区间[s,e]的懒标记下放给两个儿子线段 
    int mid=s+e>>1;
    if(mid>=l)add(l,r,x,s,mid,ls);
    if(mid+1<=r)add(l,r,x,mid+1,e,rs);
    pushup(o);
}
int query(int l, int r, int s=1, int e=n, int o=1){
    if(l<=s && e<=r)return t[o];
    int res=0;
    int mid=s+e>>1;
    
    pushdown(s,e,o);
    if(mid>=l)  res+=query(l,r,s,mid,ls);
    if(mid+1<=r)res+=query(l,r,mid+1,e,rs);
    
    return res;
}
void solve(){
    int q;cin>>n>>q;
    rep(i,1,n)cin>>a[i];
    buildTree();//注意要建树!!!
    while(q--){
        int op;cin>>op;
        if(op==1){
            int l,r,x;cin>>l>>r>>x;
            add(l,r,x);
        }else{
            int l,r;cin>>l>>r;
            cout<<query(l,r)<<endl;
        }
    }
}

异或和线段树

void pushup(int o){
    t[o]=t[o<<1]^t[o<<1|1]; 
}
void update(int s, int e, int o, int x){
    t[o]^=((e-s+1)&1)?x:0; //区间长度都异或上奇数个x等于t[o]异或一个x,否则异或0等于没有异或
    lz[o]^=x;
}
void buildTree(int s=1, int e=n, int o=1){// start, end, node
    if(s==e){
        t[o]=a[s];return;
    }
    int mid=(s+e)>>1;
    buildTree(s,mid,o<<1),buildTree(mid+1,e,o<<1|1);
    pushup(o);
}
void pushdown(int s, int e, int o){
    if(lz[o]==0)return;
    int mid=(s+e)>>1, ls=o<<1, rs=o<<1|1;
    update(s,mid,ls,lz[o]);
    update(mid+1,e,rs,lz[o]);
    lz[o]=0; 
}
void add(int l, int r, int x, int s=1, int e=n, int o=1){
	if(l<=s && e<=r){
        update(s,e,o,x);
        return;
    }
    int mid=s+e>>1;
    
    pushdown(s,e,o); 
    if(mid>=l)add(l,r,x,s,mid,o<<1);
    if(mid+1<=r)add(l,r,x,mid+1,e,o<<1|1);
    pushup(o);
}
int query(int l, int r, int s=1, int e=n, int o=1){
    if(l<=s && e<=r){
        return t[o];
    }
    int res=0;
    int mid=s+e>>1;
    pushdown(s,e,o);
    // 上面是一样的
    if(mid>=l)res^=query(l,r,s,mid,o<<1);
    if(mid+1<=r)res^=query(l,r,mid+1,o<<1|1);
    return res;
}

最值线段树

const int inf=4e18;
int n;
int a[N],lz[N<<2];
int tmax[N<<2],tmin[N<<2];
#define ls o<<1
#define rs o<<1|1
void pushup(int o){
   tmax[o]=max(tmax[ls],tmax[rs]);
   tmin[o]=min(tmin[ls],tmin[rs]);
}

void update(int s, int e, int o, int x){
   tmax[o]+=x;tmin[o]+=x;
   lz[o]+=x;//lz^=x;
}
void buildTree(int s=1, int e=n, int o=1){
   if(s==e){
       tmax[o]=tmin[o]=a[s];return;
   }
   //下面是一样的
   int mid=(s+e)>>1;
   buildTree(s,mid,ls),buildTree(mid+1,e,rs);
   pushup(o);
}
void pushdown(int s, int e, int o){
   if(lz[o]==0)return;
   int mid=(s+e)>>1;
   update(s,mid,ls,lz[o]);
   update(mid+1,e,rs,lz[o]);
   lz[o]=0; 
}
void add(int l, int r, int x, int s=1, int e=n, int o=1){
   if(l<=s && e<=r){
       update(s,e,o,x); 
       return;
   }
   int mid=s+e>>1;
   pushdown(s,e,o);
   if(mid>=l)add(l,r,x,s,mid,ls);
   if(mid+1<=r)add(l,r,x,mid+1,e,rs);
   pushup(o);
}
int queryMax(int l, int r, int s=1, int e=n, int o=1){
   if(l<=s && e<=r){
       return tmax[o];
   }
   int res=-inf;
   int mid=s+e>>1;
   
   pushdown(s,e,o);
   if(mid>=l)res=max(res,queryMax(l,r,s,mid,ls));
   if(mid+1<=r)res=max(res,queryMax(l,r,mid+1,e,rs));
   
   return res;
}
int queryMin(int l, int r, int s=1, int e=n, int o=1){
   if(l<=s && e<=r){
       return tmin[o];
   }
   int res=inf;
   int mid=s+e>>1;
   pushdown(s,e,o);
    
   if(mid>=l)res=min(res,queryMin(l,r,s,mid,ls));
   if(mid+1<=r)res=min(res,queryMin(l,r,mid+1,e,rs));
   
   return res;
}
//solve()中,注意 buildtree();

线段树进阶(加法, 乘法, 赋值)

给定一个长度为n的数组,你可以执行以下操作共q次:

1 l r x: 将区间[l,r][l,r]的数字都加上x。

2 l r x: 将区间[l,r][l,r]的数字都乘上x。

3 l r x: 将区间[l,r][l,r]的数字都赋值为x。

4 l r: 求区间[l,r][l,r]的数字之和。

对于每次操作4,输出结果,结果对998244353取模。

#define ls o<<1
#define rs o<<1|1
int n,q;
int a[N],mul[N << 2],add[N << 2],t[N << 2];// mul 和 add 应该是懒标记

int mo(int x){return (x % p + p) % p;}

void update(int s,int e,int o,int k,int x){
    t[o] = mo(mo(t[o] * k) + mo((e - s + 1) * x));
    mul[o] = mo(mul[o] * k % p);
    add[o] = mo(mo(add[o] * k) + x);
}

void pushdown(int s,int e,int o){
    int mid = (s + e) >> 1;
    update(s,mid,ls,mul[o],add[o]);
    update(mid+1,e,rs,mul[o],add[o]);

    mul[o] = 1;add[o] = 0;
}

void pushup(int o){
    t[o] = mo(t[ls] + t[rs]);
}

void buildTree(int s = 1,int e = n,int o = 1){
    mul[o] = 1;
    if(s == e){
        t[o]=a[s];
        return;
    }
    int mid = (s + e) >> 1;
    buildTree(s,mid,ls);
    buildTree(mid+1,e,rs);
    pushup(o);
}

// [l,r]内的所有数*k+x
void modify(int l,int r,int k,int x,int s = 1,int e = n,int o = 1){
    if(s >= l && e <= r){
        update(s, e, o, k, x);
        return;
    }

    pushdown(s,e,o);
    int mid = (s + e) >> 1;
    if(mid >= l)modify(l,r,k,x,s,mid,ls);
    if(mid+1<= r)modify(l,r,k,x,mid+1,e,rs);
    pushup(o);
}

int query(int l,int r,int s = 1,int e = n,int o = 1){
    if(s >= l && e <= r)return t[o];
    
    pushdown(s,e,o);

    int mid = (s + e) >> 1;
    int ans = 0;
    if(mid >= l)ans = mo(ans + query(l,r,s,mid,ls));
    if(mid+1 <= r)ans = mo(ans + query(l,r,mid + 1,e,rs));

    return ans;
}
void solve(){
    cin>>n>>q;
    rep(i,1,n)cin>>a[i];
    buildTree();
    while(q--){
        int op;cin>>op;
        if(op==1){
            int l,r,x;cin >> l >> r >> x;
            modify(l,r,1,x);
        }
        else if(op == 2){
            int l,r,k;cin >> l >> r >> k;
            modify(l,r,k,0);
        }
        else if(op == 3){
            int l,r,x;cin >> l >> r >> x;
            modify(l,r,0,x);
        }
        else if(op == 4){
            int l,r;cin >> l >> r;
            cout << query(l,r) << endl;
        }
    }
}

静态权值线段树(没有实现可持久化)

接下来的线段树都没有 lazy 和 buildTree, 而且都是手动插入建树

之前的线段树是直接维护一个数组, 现在是维护一个(权值).

  1. 插入一个元素
  2. 查询整个数组第k小(大)的元素值
  3. 查询元素大小在[l,r]之间的元素个数
#define ls o<<1
#define rs o<<1|1
int n=2e5,t[N<<2];//t是桶
void pushup(int o){
    t[o]=t[ls]+t[rs];
}
void insert(int val,int s=1,int e=n,int o=1){
    if(s==e)return t[o]++,void();
    int mid =s+e>>1;
    if(val<=mid)insert(val,s,mid,ls);
    else insert(val,mid+1,e,rs);
    pushup(o);
}
int queryCnt(int l,int r,int s=1,int e=n,int o=1){
    if(l<=s&&e<=r)return t[o];
    int mid=s+e>>1;
    int res=0;
    if(max(s,l)<=min(mid,r))res+=queryCnt(l,r,s,mid,ls);
    if(max(mid+1,l)<=min(e,r))res+=queryCnt(l,r,mid+1,e,rs);
    return res;
}
int queryVal(int k,int s=1,int e=n,int o=1){
    if(s==e)return s;
    int left_sum=t[ls];
    int mid=s+e>>1;
    if(k<=left_sum)return queryVal(k,s,mid,ls);
    return queryVal(k-left_sum,mid+1,e,rs);
}

可持久化权值线段树(主席树)

静态的权值线段树只能维护整个区间的第k小(大)的数,和[l,r]上的值的个数, 由于桶很大, 插入的数范围大, 范围不是给定的

主席树问题一般给定一个长度为n的数组a,有q次询问,每次询问区间[l,r][l,r]中排名为k的元素值(即第k小的元素)。桶的目标都已知, 所以可以离散化桶

主席树类似前缀和思想,把几棵线段树相减, 从而得到符合这个区间的 一颗(计算)虚构出来的树, 这个树的性质符合 所询问区间的 静态权值线段树

因此,主席树可以求 区间 第k小(大)

  1. 离散化处理:将原始数组映射到紧凑的整数范围,便于主席树处理
  2. 建树过程:每个元素依次插入,构建版本1~n的持久化记录
  3. 查询操作:通过传入左右版本根节点,查询区间第k小值
  4. 结果转换:将查询结果的离散值映射回原始数值
const int N=2e5+5;
int n,q;
int a[N];
int rt[N],idx;
vector<int> v;

int bin(int x){
    return lower_bound(all(v),x)-v.begin()+1;
}
struct node{
    int ls,rs,val;
}t[N<<5];

void insert(int &o, int pre, int val, int s=1,int e=n){ //传入当前节点,且需要进行操作, pre节点只需要复制不需要操作, val是新插入的值
    o=++idx;//分配节点,o是编号, 树的点编号在主席树中没有意义,线段树只由e,s控制
    t[o]=t[pre];//复制上一个版本
    t[o].val++;//修改自身权值
    if(s==e)return ;//表明到了叶子节点s,e==要插入的值
    int mid = s+e>>1;
    if(val<=mid)insert(t[o].ls, t[pre].ls, val, s, mid);
    else insert(t[o].rs, t[pre].rs, val, mid+1, e);
}
int queryVal(int lo,int ro, int k, int s=1,int e=n){
    if(s==e)return s;// 返回的是v数组下标的索引
    int left_sum=t[t[ro].ls].val-t[t[lo].ls].val;
    int mid=s+e>>1;
    if(k<=left_sum)return queryVal(t[lo].ls, t[ro].ls, k, s, mid); 
    return queryVal(t[lo].rs, t[ro].rs, k-left_sum, mid+1, e);
}
void solve(){
    cin>>n>>q;
    rep(i,1,n)cin>>a[i];
    rep(i,1,n)v.push_back(a[i]);
    sort(all(v));
    v.erase(unique(all(v)),v.end());
    rep(i,1,n)insert(rt[i],rt[i-1],bin(a[i]));// rt[i],里面的值是树的点的编号,但是用i来控制方便是第几棵树,
    									  // bin(a[i])返回离散后数组v下标, 表示的是第几个数
    while(q--){
        int l,r,k;cin>>l>>r>>k;
        cout<< v[queryVal(rt[l-1],rt[r],k)-1] <<endl;
    }
}

a= 100,1,6,10, 2, 7,2,6

v= 1,2,6,7,10,100

bin: 6, 2, 3, 5, 2, 4, 2, 3 在第rt[i]棵树加入的时候(第i时刻),第k个位置的数的桶+1

主席数的变换运用

给定一个长度为n的数组a,有q次询问,每次询问区间[l,r][l,r]中不同的数字的个数。: 开一个last数组存上次出现的位置, [l,r]区间的不同数的个数f(l,r)=∑(l,r) ( last[ i ] ]<l )

a: 1 2 1 2 2

lst: 0 0 1 2 4

int a[N], rt[N], lst[N], idx, n ;
struct Node{
    int ls, rs, val;
}t[N << 5];

vector<int> v;
int bin(int x){return lower_bound(all(v), x) - v.begin() + 1;}
void insert(int &o, int pre, int val, int s = 0, int e = n){
    o = ++idx;
    t[o]= t[pre];
    t[o].val++;
    if(s == e)return;
    int mid = (s + e) >> 1;
    if(val <= mid)insert(t[o].ls, t[pre].ls, val, s, mid);
    else insert(t[o].rs, t[pre].rs, val, mid + 1, e);
}
int query(int lo, int ro, int l, int r, int s = 0, int e = n){
    if(l <= s && e <= r)return t[ro].val - t[lo].val;
    int mid = (s + e) >> 1, res = 0;//设置返回加和的个数
    if(mid >= l)res += query(t[lo].ls, t[ro].ls, l, r, s, mid);
    if(mid + 1 <= r)res += query(t[lo].rs, t[ro].rs, 1, r, mid + 1, e);
    return res;
}
void solve(){
    int q;cin >> n >> q;
    rep(i,1,n)cin >> a[i];
    rep(i,1,n)v.push_back(a[i]);
    sort(all(v));
    v.erase(unique(all(v)), v.end());//因为是描述的这第某个数上次出现的位置,所以去重
    rep(i,1,n){
        insert(rt[i], rt[i - 1], lst[bin(a[i])]);//只是多了一层映射,就把对所有桶的维护转变为lst数组的维护
        lst[bin(a[i])] = i;//a[i]这个数,第几个桶,上次出现的位置(也就是现在),记录在lst[bin(a[i])]
    }
    while(q --){
        int l, r;cin >> l >> r;
        cout << query(rt[l - 1], rt[r], 0, l - 1) << '\n';
        // 0 ~ l-1 是控制线段范围,相当于两个东西描述一个要找的性质,之前的性质是k罢了
    }
}

自己定义结构体线段树解决 矩形面积并问题 (结合扫描线)

const int N=1e5+5;
struct event{
    int l,r,y,type;
    bool operator<(const event& tem)const{
        return y<tem.y;
    }
};
//以下是线段树部分,因为要动态维护整个区间所覆盖的长度(区间之和),所以想到线段树
struct node{
    int len,cnt;
}t[N<<2+4];//之前这里没有+4就RE了,以后开树都加上一点
vector<event> ev;
vector<int> v;
void update(int s,int e,int p,int l,int r,int typ){
    if(r<=s||e<=l)return ;
    if(l<=s&&e<=r){
        t[p].cnt+=typ;
    }else{
        int mid=e+s>>1;//注意不是l+r>>1
        update(s,mid,p<<1,l,r,typ);
        update(mid,e,p<<1|1,l,r,typ);//注意这里不能mid+1
        //如果+1,[mid,mid+1]这一段长度就直接被忽略了,现在维护的是相邻的线段而不是离散的点
    }
    if(t[p].cnt>0){
        t[p].len=v[e-1]-v[s-1];
    }
    // }else if(s==e){t[p].len=0;} //可以不写,反正递归到最后也是0
    else{
        t[p].len=t[p<<1].len+t[p<<1|1].len;
    }
}
void solve(){
    int n;cin>>n;
    rep(i,1,n){
        int x1,x2,y1,y2;cin>>x1>>y1>>x2>>y2;
        ev.push_back({x1,x2,y1,1});
        ev.push_back({x1,x2,y2,-1});
        v.push_back(x1);
        v.push_back(x2);
    }
    sort(all(v));
    v.erase(unique(all(v)),v.end());
    for(auto &e:ev){
        e.l=lower_bound(all(v),e.l)-v.begin()+1;//因为树维护的本来就是第几个x之间的关系,至于具体的值再在v映射一步即可
        e.r=lower_bound(all(v),e.r)-v.begin()+1;
    }
    sort(all(ev));

    int ans=0;
    int pre_y=ev[0].y;
    for(auto &e:ev){
        int dy=e.y-pre_y;
        ans+=dy*t[1].len;
        update(1,v.size(),1,e.l,e.r,e.type);//注意这里v.size()即可,不需要再+1
        pre_y=e.y;
    }
    cout<<ans<<endl;
}

错误总结: ①线段树结点数目开小了 ② int mid=e+s>>1 ③ v.size()不用+1否则访问越界

④ update(mid,e,...),不用mid+1,这题比较特殊, 因为维护的每个点, 代表的是 连续的线段

posted @ 2025-04-21 21:28  byxxx  阅读(58)  评论(0)    收藏  举报