定义
我们已经了解了线段树的许多操作与结构
但是有一些缺点:
1.它是递归操作的
所以空间可能会很大
2.叶子节点深度都不同
这样操作会很麻烦
3.代码量太大...
所以现在可以引进zkw线段树
和普通的线段树相比,zkw线段树主要有这样几个不同点:
1.所有的叶子结点都在同一深度
2.在左右两端各增加了一个哨兵
3.每个点可以根据自己的编号计算父节点编号
以上三点保证了zkw线段树可以自底向上非递归
做法:
怎么做呢?
我们现在要在最底层设置n个点,同时还要加上2个卫兵(作用后面会有),所以最底层至少n+2个点
根据完全二叉树的性质(第i层会有个点),我们可以把最底层的第一个标号求出来
注意:这里最底层不一定刚好要存下,但是最底层至少要有这么多的点
接着,就是预处理了
void pre(){
m = 1;
while( m <= n + 1 ) m *= 2;//找最底层的第一个点
for( int i = n + m + 1 ; i >= 1; i -- ){
if( i >= m )
tre[i] = a[i-m];//如果这个点是叶子,那么它存的就是初始值
else
tre[i] = tre[i*2] + tre[i*2+1];//否则是它的左儿子+右儿子
}
}
单点修改
很简单,把这个叶子修改后,依次 / 2,一个个更新他的祖先 O(logn)
void add( int x , int delta ){
x += m;
while( x ){
tre[x] += delta;
x /= 2;
}
区间查询
这个时候我们就需要用到卫兵了
我们已知区间左右端点L , R,那我们得到它们卫兵的两个位置是L-1,R+1
现在把卫兵往上移
如果我们发现当前卫兵是它父亲的左端点,那么它父亲的右端点就会是在这个区间范围内(自己推)
那么把它标兵的兄弟就加进我们的ans中来
void query( int x , int y ){
int s = x + m - 1 , l = y + m + 1 , ans = -1;//ans存的是区间最大值
while( s || l ){
if( s / 2 != l / 2 ){//如果两个点两个标兵已经跳到统一个点了,那么整个区间就找完了
if( s % 2 == 0 )
ans = max( tre[s+1] , ans );
if( l % 2 == 1 )
ans = max( tre[l-1] , ans );
}
s /= 2 , l /= 2;
}
printf( "%d\n" , ans );
}
现在有一道题:LCIS
这道题就是用的了区间修改和单点查询
但是不同的是,如果我们直接找肯定很难做到
所以我们在每一个节点(tre[]数组)中多加几个情况
每一个tre[i]里多保存:
1.以它为子树的叶子节点个数
2.从它左端点开始为起点得到的LCIS
2.从它右端点开始为起点得到的LCIS
4.整个区间的最大LCIS
由于自己只会用笨方法,所以我把每一个LCIS起点下标,终点下标,长度都用了另一个结构体存了起来
那么区间合并就可以这样子做了:
左端点的LCIS就是两种情况:
1.如果它的左儿子的左端点LCIS包含整个区间
则就是左端点区间的大小加上右儿子的左端点LCIS
否则
就是左儿子的左端点的LCIS
更新右端点的LCIS同理
那么整个区间最大的LCIS就三个情况
左儿子的区间最大LCIS
右儿子的区间最大LCIS
如果左儿子的右端点的值比右儿子的左端点的值还小
那么就还有一个情况:左儿子右端点开始的LCIS + 右儿子左端点开始的LCIS
而这些所有的东西,都可以用重载运算符做:
struct edge{
int l , r;
int cd;
};
struct node{
edge sl , sr , sz;//sl是左端点的LCIS,sz是整个区间的LCIS,sr是右端点的LCIS
int len;
void qk(){
len = sl.cd = sl.l = sl.r = 0;
sr = sz = sl;
}
friend node operator + ( node i , node j ){
node ans;
ans.qk();
//memset( ans , 0 , sizeof( ans ) );
ans.len = i.len + j.len;
if( i.sl.cd == i.len && a[i.sl.r] < a[j.sl.l] ){
ans.sl.cd = i.len + j.sl.cd;
ans.sl.l = i.sl.l;
ans.sl.r = j.sl.r;
}
else
ans.sl = i.sl;
if( j.sr.cd == j.len && a[i.sr.r] < a[j.sr.l] ){
ans.sr.cd = j.len + i.sr.cd;
ans.sr.l = i.sr.l;
ans.sr.r = j.sr.r;
}
else
ans.sr = j.sr;
if( i.sz.cd >= j.sz.cd )
ans.sz = i.sz;
else
ans.sz = j.sz;
if( a[i.sr.r] < a[j.sl.l] && i.sr.r + 1 == j.sl.l && ans.sz.cd < i.sr.cd + j.sl.cd ){
ans.sz.cd = i.sr.cd + j.sl.cd;
ans.sz.l = i.sr.l , ans.sz.r = j.sl.l;
}
return ans;
}
}tre[MAXN*4];
区间修改&&区间查询
同样用类似懒标记的方法
只不过这样是永久标记化简单的说就是以和区间查询相同的方式,将区间表示为多个子树相加,在每个受到影响的子树上打上标记。查询的时候如果访问到某个有标记的子树,就将这个标记对所查询区间的影响反映到答案中
void add( int x , int y , ll delta ){
int lsum = 0 , rsum = 0;
x = x + m - 1;
y = y + m + 1;
while( x || y ){
tre[x] += lsum * delta;
tre[y] += rsum * delta;
if( x / 2 != y / 2 ){
if( x % 2 == 0 ){
tre[x+1] += delta * len[x+1];
flag[x+1] += delta;
lsum += len[x+1];
}
if( y % 2 == 1 ){
tre[y-1] += delta * len[y-1];
flag[y-1] += delta;
rsum += len[y-1];
}
}
x /= 2;y /= 2;
}
}
void query( int x , int y ){
int lsum = 0 , rsum = 0;
x = x + m - 1;
y = y + m + 1;
ll ans = 0;
while( x || y ){
ans += lsum * flag[x];
ans += rsum * flag[y];
if( x / 2 != y / 2 ){
if( x % 2 == 0 ){
ans += tre[x+1];
lsum += len[x+1];
}
if( y % 2 == 1 ){
ans += tre[y-1];
rsum += len[y-1];
}
}
x /= 2;y /= 2;
}
printf( "%lld\n" , ans );
}