前言
我研究了一晚上的ZKW线段树,总要有点收获吧,打算就现在这里啦。
先说一下我为什么要学这个ZKW线段树,因为好写,对于我来说,就是为了好写。不过后来发现它还有更多的好处,比如非递归啊,常数小啊,是不是还可以可持久化!?当然我觉得这不是很科学,因为堆储存父亲是确定的, 可能只是运用到了一些技巧罢。
但是它有什么局限性呢,其实应该是有的,ZKW在他的PPT《统计的力量》里面曾经自称他写的这个是树状数组,是有道理的,这是一个加强版的树状数组,可以实现一部分的线段树功能。我再研究研究吧。。。。
之后将会以RMQ为主,因为。。。区间求和用树状数组去啊!
非差分版本(不支持区间修改)
特点
利用了堆式储存,利用了二叉堆中结点本身的数值关系。
开的点数一定要是2的倍数
原理
我不想写原理了,网上到处都是,原版的可以参见ZKW的PPT《统计的力量》,能看到我这篇博文的应该都是自学ZKW线段树学得实在没有办法开始乱找资料的那种,原理大概是了解了吧,我就写个版子,然后把我学习过程中的疑问提一下。
我先来解释一下不容易理解的地方,可以先跳过,看不懂代码的地方再回来看这里。
代码基本上和网上保持一致。
问:为什么要开区间维护和查询/为什么从M+1的结点开始编号?(M是叶子结点的个数也是最后一排第一个元素的下标)
答:我们把[l,r]变成(l-1,r+1),在我们的代码中初始结点并不会被统计到,所以实际上统计的是[l,r],由于这个性质,我们从M+1开始编号并且把叶子结点最后一个位置空出来(因为我们无法统计这两个位置的元素)。也说明我们最终只能维护M-2个结点(n是叶子结点的个数)。还有一个好处就是第i个元素的下标是M+i,多方便啊。
关于查询操作是基于这么一个思想:如果一个左边结点是父亲结点的左子树,那么他的兄弟结点就一定需要被考虑。
在具体操作中为了防止重复统计,当左右两个结点是兄弟结点的时候,就停止了。
可以结合网上其他人的图理解哦。
还有一点代码技巧,不要问为什么,可以仔细研究一下满二叉树的编号特征和位运算符
简单的结点关系:
‘x>>1’:x/2
‘x<<1’:x*2
‘x|1’:x是奇数不变,偶数则+1
一些判断子树的技巧:
‘x&1’:x%2==1
‘~x&1’:x%2==0
i和j是兄弟结点:
i^j=1
i^1=j
j^1=i
i^j^1=1
左子树:now<<1
右子树: now<<1|1
父亲结点:now>>1
i结点对应叶子结点编号:M+i
版子
#define lc now<<1
#define rc now<<1|1
const int maxn=1e5+100,inf=1e9;
int a[maxn];
struct ZKW_SegmentTree
{
static const int maxn=1<<(17+1);
int M,mi[maxn];
void pushup(int now)
{
mi[now]=min(mi[lc],mi[rc]);
}
void Build(int n)//直接和Initial合并;这里是唯一需要用到n的地方,后面都不用了,当然可以不加这个参数
{
for(M=1;M-2<n;M<<=1);//只能统计M-2个元素
memset(mi,0x3f,sizeof(mi)) ;
for(int i=1;i<=n;i++)mi[i+M]=a[i];//填满儿子结点
for(int i=M-1;i;i--)pushup(i);//填满父亲结点
}
void modify(int i,int v)//把i的值增加v
{
mi[M+i]+=v;
for(int now=(M+i)>>1;now;now>>=1)pushup(now);
}
int query(int i,int j)
{
int ret=inf;
for(i=i+M-1,j=j+M+1;i^j^1;i>>=1,j>>=1)
{
if(~i&1)ret=min(ret,mi[i^1]);
if(j&1)ret=min(ret,mi[j^1]);
}
return ret;
}
}sgt;
代码解释:
看注释
差分版本(支持区间修改)
先说说
写给那些看了原理理解不了开始乱翻博客的人看的。
至于原理就是差分嘛,代码可以看我的。
原理就懒得详细写了,但是我会写一下很细节理解,都是我学习的时候遇到的问题。
操作原理与细节理解
我来理解一下这个ZKW线段树的区间修改,以最小值为例子
定义原始值是每个结点代表区间的最小值,差分值是当前结点原始值减去父亲结点原始值,令根结点的差分值=原始值
我们可以通过计算该结点和其所有祖先的差分值,得到一个元素的原始值
这也就可以省去原始值数组,也就是ZKW说的:永久化标记就是值
通过差分的思想就很容易修改了:
对于一个区间:
如果所有的元素都被修改,那么差分之后该结点的所有子结点的差分值都不变,只需要将该结点的差分值增加v
这是基于这样的性质,我们才有可能在log级别的时间复杂度完成修改
关于修改过程的理解:
在修改的一个过程中,我们是逐步向上的,也就是说我们的当前线段树的值
原始值只有当前结点和子结点的原始值是对的,而祖先结点我们还没有修改。
我们通过上传操作来完成对父亲结点原始值的修改。
关于上传操作的理解(再次强调是对于min操作,其它的略有不同,超易推):
我们来看一看一颗完全正确的树长成什么样:
少主经过缜密的研究发现有很多结点的差分值是0!这说明有很多子结点和父亲结点的值一样!而且它的兄弟结点的值是正数(max就是负数了哈)!(惊恐.jpg)
咳咳。。。废话!父亲结点的原始值肯定来自的原始值较小的那个儿子啊。
重点来了!!!
所以修改的时候我们把较小的那个儿子的差分值弄成0,假设减小了A,然后由于原始值不变,我们把父亲结点的差分值增加A,又由于原始值不变,我们把该结点的兄弟结点(父亲结点的另一个儿子)的差分值减小A。
其中由于两个兄弟结点的祖先是一样的,我们可以直接通过差分值判断大小。
关于上传操作的对象:
这里我们上传操作是修改父亲结点的值,而不是递归线段树中修改当前结点的值
并且想一下你就会发现,其实也是可以放在父亲结点修改的。
那么为什么ZKW要写成对父亲结点的操作呢?
其实这个和他要用开区间查询的道理是一样的,为了不在最后一排讨论。
如果在子结点修改父亲,只会在根结点顺便把0修改了。
而如果在父亲结点取子结点,一旦不在叶子结点特判的话,就会访问违规下标。
不过我这种懒人并不打算这么做
因为pushup操作实在是太好写了,叶子结点也很好判断,况且我们可以在建树操作用同样的操作完成。
关于查询:
由于是差分值,因此不要忘了即使找到了最优值,还要加上祖先的值才得到原始值哦
关于单点查询:
单点查询可以用区间查询,这应该也是个用开区间的原因,感觉很方便
非要单独查就把它和祖先的差分值加起来即可。
代码
关于代码
因为支持区间修改维护的是差分值,建图修改查询和非差分版全部都不一样
还有就是少主我的代码是最大值哈,那个。。因为少主家题库是最大值嘛。。
typedef long long LL;
#define lc now<<1
#define rc now<<1|1
const LL inf=1e16;
const int maxn=1e6+10;
int a[maxn];
struct ZKW_SegmentTree
{
static const int maxn=(1<<20)+10;
int M;
LL mx[maxn<<1];
void pushup(int now)
{
if(now>M)return;
LL A=max(mx[lc],mx[rc]);
mx[lc]-=A,mx[rc]-=A,mx[now]+=A;
}
void Build(int n)
{
for(M=1;M-2<n;M<<=1);
for(int i=0;i<M;i++)mx[i+M]=-inf;
for(int i=1;i<=n;i++)mx[i+M]=a[i];
for(int i=M-1;i;i--)pushup(i);
}
void modify(int s,int t,int v)
{
for(s=s+M-1,t=t+M+1;s^t^1;s>>=1,t>>=1)
{
pushup(s);pushup(t);
if(~s&1)mx[s^1]+=v;
if(t&1)mx[t^1]+=v;
}
pushup(s);pushup(t);
for(s>>=1;s;s>>=1)pushup(s);
}
LL query(int s)//单点查询
{
LL ret=0;
for(int i=s+M;i;i>>=1)ret+=mx[i];
return ret;
}
LL query(int s,int t)//区间查询
{
if(s==t)return query(s);
LL L=0,R=0;
for(s=s+M,t=t+M;s^t^1;s>>=1,t>>=1)
{
L+=mx[s],R+=mx[t];
if(~s&1)L=max(L,mx[s^1]);
if(t&1)R=max(R,mx[t^1]);
}
L+=mx[s],R+=mx[t];
LL ret=max(L,R);
for(s>>=1;s;s>>=1)ret+=mx[s];
return ret;
}
}sgt;
依旧算是短小精干,但是总感觉考虑了很多特殊情况,出现了诸多问题,而且区间查询的时候不能用开区间,代码肯定是没有问题的,但是问题在于有没有更加简单并且一般的方法,因为这样子感觉可能会比较容易写错。