树状数组 - 区间可加可拼凑的信息

I.综述&解释

树状数组,一听就知道是长得像树的数组。emm

其内部节点的长度都是2的整数次幂,用一个数组存储,这样做利用了“任意一个正整数都能被唯一分解为不同的若干二的次幂的和的形式”这个定理,从而可用若干节点之和拼凑成为一个整体,表示一个前缀区间的信息。

II.起源

树状数组最基础的用法是用于维护前缀和的。

不过如果只需要维护前缀信息,那么任意一个满足区间可加性的二元运算符均可。

它本质上还是一种分治、倍增的思想,其实只是借助2的性质进行信息分割,不过就因为以上定理(从0开始加),使其只能维护前缀信息。


无关话题:

至于网上有人搞出的那些可维护区间最大值的树状数组,我认为实际上只是借了树状数组的名头在做分块罢了,其实读来有“有则用之,无则作罢”的感觉。现贴出博客链接Link

就比如它的修改操作:

void update(int position){
	int len_x;
	while(position<=n){
		len_x=lowbit(position);
		h[position]=a[position];
		
		for(int i=1;i<len_x;i<<=1){
			h[position]=max(h[position],h[position-i]);
		}
		
		position+=len_x;
	}
}

你仔细读,它实际上是在普通的修改过程中加了一次特殊的修改,这就意味着进行了以下操作:

看见了吗,它把每个受影响区间重构了!

这点,在OI-wiki上有解释:

事实上,对于不可差分信息,不存在通过 p 直接修改 c[y] 的方式。这是因为修改本身就相当于是把旧数从原区间「移除」,然后加入一个新数。「移除」时对区间信息的影响,相当于做「逆运算」,而不可差分信息不存在「逆运算」,所以无法直接修改 c[y]。

那么查询呢?

int query(int x, int y) {
    int ans=0;
    while(y>=x) {
        ans=max(ans,a[y]), --y;
        while(y-lowbit(y)>=x) {
            ans=max(ans,b[y]);
            y-=lowbit(y);
        }
    }
    return ans;
}

可以看到,这个查询就是这样的过程:

我们还是基于之前的思路,从 r 沿着 lowbit\operatorname{lowbit} 一直向前跳,但是我们不能跳到 l 的左边。

因此,如果我们跳到了 c[x]c[x],先判断下一次要跳到的xlowbit(x) x - \operatorname{lowbit}(x) 是否小于 l:

如果小于 l,我们直接把 a[x]\boldsymbol{a[x]} 单点 合并到总信息里,然后跳到c[x1] c[x - 1]。 如果大于等于 l,说明没越界,正常合并c[x] c[x],然后跳到 c[xlowbit(x)]c[x - \operatorname{lowbit}(x)] 即可。

所以我才说它是为了迎合条件有则用之,无则作罢

算法时间复杂度O(nlog2n)\operatorname O(n \log^2 n)劣于线段树

当然,这是因为其存储与索引方式的无奈之举,在我们讨论了线段树后就知道,线段树能更有效的对区间执行二进制划分


对于有更多性质的运算,例如满足区间可减性的运算(e.g. +)还可以求区间。

它还能便捷地将区间修改转化为单点操作差分数组。

III.基本操作

  • 单点修改(modify)

对修改的起点“向上”对所有包括它的父节点进行修改,这里利用了一个技巧,快速地找出它的父节点,即lowbit运算。因为实际上二进制表示下每一个1都是代表2的次幂,即一个区间的长度。

  • 单点查询

对查询的节点“向下”(其实不准确,因为这些点不是包含关系的)累计答案。也借助lowbit运算。

posted @ 2023-07-08 18:07  haozexu  阅读(4)  评论(0编辑  收藏  举报  来源