本篇只用于我对树状数组和线段树的初步思考记录

树状数组:

image

  对于tr[i],存储的是[i-(i&-i)+1,i]的数的信息。比如4的二进制为100,存储的就是[01,100]2的信息,6的二进制为110,存储的是[101,110]的信息。

  对于[1,r]范围的区间查询,用-=i&-i表示对r的二进制位全部分解,得到结果,如r=7时,二进制表示为111,则这段区间信息就是111的信息+110的信息+100的信息+000的信息,再如r=1012时,区间信息应为101+100+000,当r=1011时,区间信息为1011+1010+1000+0000

  对于[i]的单点修改,即让最低位1的位置不断提高以覆盖全部包含该数字的区间

  对于[l,r]的区间修改,由于树状数组的结构问题,必须将其转化为俩个单点修改,然后用前缀和求区间修改,这时就不便使用树状数组

所以树状数组用于简单的前缀和查询是比线段树要好很多的,但这里要注意的是,前缀和由于可以进行逆运算(sum[r]-sum[l-1]=sum[l,r])可以维护任意区间,但是最大值或者gcd是不支持右区间减左区间去维护任意[l,r]的范围,只能维护[1,r]的范围。因此,树状数组只能维护类似加法,异或,乘法(无0)的任意区间,对于最大值,最小值,gcd,lcm,之类的只能维护前缀信息。

  总结:树状数组本质上是维护前缀信息的结构。要查询任意区间 [ l , r ] 的信息,通常需要该信息满足可逆性(即可以通过两个前缀信息组合得到区间信息)。

单点修改代码如下:

  

查看代码
struct BIT {
    int n;
    vector<int> c;
    BIT(int n_) : n(n_ + 1), c(n_ + 1, 0) {}//初始化
    int sum(int r) {
        int s = 0;
        while (r > 0) s += c[r], r -= r & -r;
        return s;
    }//左区间查询
    int sum(int l, int r) {
        if (l > r) return 0;
        return sum(r) - sum(l - 1);
    }//区间查询
    void add(int idx, int delta) {
        while (idx < n) c[idx] += delta, idx += idx & -idx;
    }//点修
};

下标线段树:

  线段树是基于二分思想的一种数据结构,相对与树状数组,线段树可以通过懒标记来维护区间修改,以及区间信息,在功能上比树状数组要更胜一筹。

  线段树每个节点对应一个区间的汇总信息,因此要求区间之间满结合律。难点在于维护懒标记和区间合并的数学关系推导

  先看建树过程:

    可以用id<<1和id<<1|1这样朴素的建树方式,每个节点的结构体内变量l,r分别维护该节点表示的左右范围;

    也可以用动态开点的方式(n>=1e8),在函数的传递过程中传递区间l,r;

    以下是俩种建树方式的代码,可以自行比较

    

查看代码
void build(int id,int l,int r){
    tr[id]={l,r,a[l],1,0};
    if(l>=r)return;
    int md=(l+r)>>1;
    build(lc,l,md);
    build(rc,md+1,r);
    pushup(id);//这个地方如果不需要维护类似最值的条件就可以直接注释掉
}
/*
动态开点一般不会有建树函数,只有在用到的时候才会建节点,洛谷p13825模版题
*/
void add(int &id,int l,int r,int x,int y,int k){//
    if(!id)id=++idx;
    if(x<=l && r<=y){
        tr[id].sum+=k*(r-l+1);
        tr[id].add+=k;
        return;
    }
    pushdown(id,l,r);
    int md=(l+r)>>1;
    if(x<=md)add(lc,l,md,x,y,k);
    if(y>md)add(rc,md+1,r,x,y,k);
    pushup(id);
}

之后看线段树的一个难点:查询+上沿

上沿操作其实等价于你让俩个区间的信息合并,比较友好的是,这个操作不用去管懒标记,比较不友好的是,你很难事先确定你的上沿函数应该怎么写。

因为你在做查询操作时可能会遇到一种比较棘手的情况:区间被拆分成了好几个

image

如果你需要维护的信息是可以通过查询根节点直接得出的,那只需要跟新上浮一直到根节点即可。但是如果你需要维护的信息如果只能通过这三个区间拼凑,不能涉及其他区间。对于简单维护,只是一个二元运算,如区间和(相加),区间最大值(取max)即可;但是对于复杂维护,如区间最大连续子段和,区间最大公因数等就需要维护多个变量,此时只能通过创造临时节点T来做上沿的操作,因此对上沿函数也要有较大改变。

以下给出简单维护和复杂维护俩种上沿和查询函数,自行比较体会:

查看代码
/*
动态开点建树方式下,维护区间和的简单上沿
*/
void pushup(int id){
    tr[id].sum=tr[lc].sum+tr[rc].sum;
}
int query(int &id,int l,int r,int x,int y){
    if(!id)return 0;
    if(x<=l && r<=y){
        return tr[id].sum;
    }
    pushdown(id,l,r);
    int md=(l+r)>>1;
    int ans=0;
    if(x<=md)ans+=query(lc,l,md,x,y);
    if(y>md)ans+=query(rc,md+1,r,x,y);
    return ans;
}
/*
id<<1 和 id<<1|1 建树方式下,区间最大连续字段和的复杂上沿
*/
void pushup(pi &id,pi l,pi r){
    id.sum=l.sum+r.sum;
    id.lmx=max(l.lmx,l.sum+r.lmx);
    id.rmx=max(r.rmx,r.sum+l.rmx);
    id.mx=max(l.mx,max(r.mx,l.rmx+r.lmx));
}
pi query(int id,int x,int y){
    int l=tr[id].l,r=tr[id].r;
    if(x<=l && r<=y)return tr[id];
    int md=(l+r)>>1;
    if(y<=md)return query(lc,x,y);
    if(x>md)return query(rc,x,y);
    pi T;
    pushup(T,query(lc,x,y),query(rc,x,y));
    return T;
}

最后再来看线段树的第二个难点:懒标记下沿

懒标记的作用是不做多余的下沿操作,只要必要下沿的时候下沿,进而大幅减少时间复杂度。

懒标记在简单模版下也相当简单,但在复杂情况下也相当难,以下为区间加的懒标记下沿,可以代表代码整体的框架:

void pushdown(int id, int l, int r) {
    int mid = (l + r) >> 1;
    // 假设标记是加法 add
    if (tag[id] != 0) {
        int v = tag[id];
        // 更新左孩子
        tree[lc] += v * (mid - l + 1);
        tag[lc] += v;
        // 更新右孩子
        tree[rc] += v * (r - mid);
        tag[rc] += v;
        // 清除当前节点标记
        tag[id] = 0;
    }
}

这是在维护id<<1 和id<<1|1情况下的简单下沿,在动态开点时,我们还要注意如果左右子树的节点没有创建,应当创建,即:

if (!lc) lc = ++idx;
if (!rc) rc = ++idx;

在复杂下沿中,我们需要考虑多个懒标记的组合,如又有区间乘,又有区间加操作时就要考虑操作的先后,经过推理,得出先乘后加是更优的(具体不多赘述)

下面给出维护区间加和区间乘的下沿代码,请自行体会:

查看代码
void pushdown(int id){
    int add=tr[id].add,mul=tr[id].mul;
    tr[lc].sum=(tr[lc].sum*mul+add*(tr[lc].r-tr[lc].l+1))%m;
    tr[lc].mul=tr[lc].mul*mul%m;
    tr[lc].add=(tr[lc].add*mul+add)%m;
    tr[rc].sum=(tr[rc].sum*mul+add*(tr[rc].r-tr[rc].l+1))%m;
    tr[rc].mul=tr[rc].mul*mul%m;
    tr[rc].add=(tr[rc].add*mul+add)%m;
    tr[id].add=0;
    tr[id].mul=1;
}

 

posted on 2026-05-01 16:13  LeoCodex  阅读(11)  评论(0)    收藏  举报