进阶线段树
线段树:
这是一个线段树的例图。
我们可以发现其实线段树就是将几个连续的小区间拼凑成一个个大区间的过程,从而实现查找时的优秀的复杂度。
引入-分块基础的思想:
其实如果你知道分块的思想,那么你会更好理解线段树的思想,分块就是将一个序列分成 \(\sqrt N\) 个块每个块的块长就是 \(N / \sqrt N = \sqrt N\) 每次维护一个区间的信息就会通过零块暴力,整块直接对这个块做标记,其实这个标记和线段树的懒标记是一样的道理。只不过线段树是一个树形的结构。
线段树的懒标记:
既然我们知道了线段树的懒标记的大致的作用那么我们该如何去实现呢?
其实不难就是在查询和修改时多了一些维护的东西。
常见的修改题型有以下几种:
- 将区间 \([l,r]\) 的数加 \(k\) 。
- 将区间 \([l,r]\) 的数乘 \(k\) 。
- 将区间 \([l,r]\) 的数统一赋值为 \(k\) 。
- 将区间 \([l,r]\) 的数统一开平方。
- 将区间 \([l,r]\) 的值统一都取反(仅用于数组内所有数都为
0|1
)
常见的查询题型有以下几种:
- 输出 \(\Sigma_{i=l}^r a_i\)
- 输出 \([l,r]\) 中的最大值。
- 输出 \([l,r]\) 中的最小值。
- 输出 \([l,r]\) 中的所有数的乘积。
今天我们主要围绕这几个方面进行讲解
例题1
首先先看修改操作:
我们分析下这个对于区间 \([l,r]\) 的数加 \(k\),用 t
数组表示总和,用 lz
数组表示懒标记。
那么对于 \(t_i\) 显然表示 \(i\) 这颗子树的总和, \(lz_i\) 表示 \(i\) 这颗子树的加标记是多少。
1.修改时如果是当前的区间完全覆盖线段树某一个节点所对应的区间,那么就将这个节点的 \(t_i\) 所对应的区间的每一个数的值统一加上 \(k\) 具体的,将区间 \([l_i-r_i]\) 加上 \(k\) 因为这个区间的长度是 \(r_i-l_i+1\) 所以 \(t_i\) 的值应该加上 \((r_i-l_i+1) \times k\) 那么对于 \(lz\) 数组我们直接将他加上 \(k\) 即可表示这颗子树统一被加上了 \(k\)。
2.如果是零块,那么就将这个节点的信息传到他的左右儿子节点,即 pushdown
函数,因为这样可以方便零块去继续递归继续修改。
由于线段树为 \(\log_2 N\) 层,所有单次的时间复杂度会为 \(O(\log_2 N)\) 十分高效。
以上就是修改操作的核心。
再看查询操作:
设查询的区间为 \([l,r]\) 则如果递归到一个节点使得 \([l,r]\) 能完全覆盖这个节点所对应的区间,那么就直接加上这个节点的信息,否则递归,知道能被 \([l,r]\) 的区间完全覆盖。
查询的细节:
查询时应该查询什么值呢?
肯定是查询 \(t\) 数组的值,因为 \(t\) 数组代表这个子树的和。
需要注意的时查询时也需要将标记下传。
查询相对于修改就好理解了许多。
例题1-tag&pushdown部分的代码:
void tag(int p,int l,int r,ll k){
t[p]+=(r-l+1)*k;
lz[p]+=k;
}
void pushdown(int p,int l,int r){
int mid=(l+r)>>1;
tag(ls(p),l,mid,lz[p]);
tag(rs(p),mid+1,r,lz[p]);
lz[p]=0;
}
因为前面已经讲过如何修改了,所以这里不再复述。
例题2
修改部分:
根据数学知识我们知道 \((x+y)k = xk + yk\) 线段树中也是基于这个原理对于区间乘法,如果我们的 \(t\) 数组的值为 \(x\), \(lz\) 数组的值为 \(y\)
设区间总值为 \(sum\) 设乘标记为 \(z\) 则实际的区间总和应该为 \(sum\times z+y\) 如果将这个整体乘 \(k\) 则根据乘法分配律得 \((sum\times z+y)\times k = (sum\times k) \times (z\times k) + y \times k\) 也就是 \(sum,z,y\) 都相比于原来乘了一个 \(k\) 因为这个涉及到乘法所有乘标记清空的状态应该是 1
而不是 0
。
查询自然和线段树 1
相同,不再复述。
例题2-tag&pushdown 代码:
void tag(int p,int l,int r,ll k,bool add){
/*add表示是否为加法标记*/
if(add){
t[p].t=(t[p].t+(r-l+1)*k%m)%m;
/*总和变化*/
t[p].z1=(t[p].z1+k)%m;
/*加法标记变化*/
}else{
t[p].t=t[p].t*k%m;
/*总和变化*/
t[p].z1=t[p].z1*k%m;
t[p].z2=t[p].z2*k%m;
/*加法标记与乘法标记都乘k*/
}
}
void push_down(int p,int l,int r){
int mid=(l+r)>>1;
tag(ls(p),l,mid,t[p].z2,false);
tag(rs(p),mid+1,r,t[p].z2,false);
t[p].z2=1;
/*下传乘法标记*/
tag(ls(p),l,mid,t[p].z1,true);
tag(rs(p),mid+1,r,t[p].z1,true);
t[p].z1=0;
/*下传加法标记*/
}
例题3
这道题涉及到区间赋值的问题。
设原来的子树总和为 \(sum\) 需要赋值的数为 \(k\) 那么我们将 \(lz\) 的值清零即可因为统一赋值,再将赋值的标记变为 \(k\) 即可。其次我们将 \(sum\) 改为 \(k\times (r_i-l_i+1)\) 这里很好理解就是把 \(sum\) 设为要赋值的数乘区间的长度即可。
单本题让我们维护区间最值,这个更简单,直接将最值改为 \(k\) 即可。
例题3-tag&pushdown 代码:
void tag(int p,int l,int r,ll k,int o){
if(o==1){
/*区间赋值*/
t[p]=k;
z1[p]=0;z2[p]=k;
}else{
/*区间加法*/
t[p]+=k;
z1[p]+=k;
}
}
void push_down(int p,int l,int r){
int mid=(l+r)>>1;
if(z2[p]!=NONE){
/*有区间赋值标记*/
tag(ls(p),l,mid,z2[p],1);
tag(rs(p),mid+1,r,z2[p],1);
z2[p]=NONE;
}
if(z1[p]){
/*有区间加法标记*/
tag(ls(p),l,mid,z1[p],2);
tag(rs(p),mid+1,r,z1[p],2);
z1[p]=0;
}
}
简单提一下区间取反和区间开平方:
区间取反:
对于区间取反会有一个特点,0
变成 1
反之变成 0
所以统计答案的时候应该把 \(t_i = r_i-l_i+1-t_i\) 就是用总长度减去以前在长度,即为取反后的长度。
区间开方:
本题的操作是区间开方和区间查询,但区间开方不能直接用 Lazy-Tag 实现。本题的关键是:一个数如果被开方7次,那么它一定会变为1,后面再怎么开方也还是1。于是我们可以记录一个区间中是否有不为1的数,在修改时,如果都为1,直接跳过。
线段树在其他算法中在组合应用:
树链剖分算法:
树链剖分是一个常见的线段树与重链之间的组合的算法,他的思想是对于每一条链和一个子树,它在线段树上的标号总是连续的因此就可以用线段树的相关知识去实现快速的加减求和。
树链剖分中使用线段树修改查询部分的代码:
inline void update1(int x,int y,int k){
while(t[x].tot!=t[y].tot){
if(t[t[x].tot].d<t[t[y].tot].d)swap(x,y);
seg.upd(1,1,n,t[t[x].tot].id,t[x].id,k);
x=t[t[x].tot].fa;
}
if(t[x].d>t[y].d)swap(x,y);
seg.upd(1,1,n,t[x].id,t[y].id,k);
}
inline int query1(int x,int y){
int ans=0;
while(t[x].tot!=t[y].tot){
if(t[t[x].tot ].d<t[t[y].tot ].d )swap(x,y);
ans+=seg.que(1,1,n,t[t[x].tot].id,t[x].id)%mod;
x=t[t[x].tot].fa;
}
if(t[x].d>t[y].d)swap(x,y);
ans+=seg.que(1,1,n,t[x].id,t[y].id )%mod;
return ans%mod;
}
inline void update2(int x,int k){
seg.upd(1,1,n,t[x].id,t[x].id+t[x].sz-1,k);
}
inline int query2(int x){
return seg.que(1,1,n,t[x].id,t[x].id+t[x].sz-1)%mod;
}