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

对于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);
}
之后看线段树的一个难点:查询+上沿
上沿操作其实等价于你让俩个区间的信息合并,比较友好的是,这个操作不用去管懒标记,比较不友好的是,你很难事先确定你的上沿函数应该怎么写。
因为你在做查询操作时可能会遇到一种比较棘手的情况:区间被拆分成了好几个

如果你需要维护的信息是可以通过查询根节点直接得出的,那只需要跟新上浮一直到根节点即可。但是如果你需要维护的信息如果只能通过这三个区间拼凑,不能涉及其他区间。对于简单维护,只是一个二元运算,如区间和(相加),区间最大值(取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;
}
浙公网安备 33010602011771号