关于树状数组一些有意思的东西

嘛~最近刚刚学会树状数组,写个blog记录一下心得。

树状数组呢,核心是一个叫lowbit的东西,lowbit(x)=x&-x=x的最后一位1的大小。
一、一个经典问题

一个初始值为0的k位计数器,要求支持n次+1操作。时间复杂度?

经典解法:

法I:考虑第i位的改变次数,可得 O(k1i=0n2i)O(i=0n2i)=O(n)
法II:考虑计数器中1的数量,显然每次只会增加1个(减少若干个),所以时间复杂度 O(n)
法III:考虑势能函数f(T)=计数器中1的数量,则显然单次操作均摊 O(1)

奇怪向做法:

对于x,考虑大于x的第一个y使得y&lowbit(x)==0(lowbit(x)第一次被进位),显然y=x+lowbit(x)。所以由x向x+lowbit(x)连一条边。这样的话显然会形成一棵树,计数器操作的代价就是点数+边数等于 O(n)

这棵树就是树状数组啦!
二、树状数组中的一些基本关系
x的父亲是x+lowbit(x)。
x的子树是(x-lowbit(x),x],(即所有能通过若干次+lowbit到达x的节点集合)。
考虑x一直沿着它父亲走,那么lowbit一定是严格单增的,所以树高是 O(log2n) 的。
考虑x的儿子,就是能通过一次+lowbit操作到达x的元素数量,它显然等于 log2lowbit(x) ,就等于 {x2i|2i<lowbit(x)} ,所以一个节点的儿子数量也是 O(log2n) 的。更直观的说法是,x的儿子数量其实就等于从x-1 +1(上文中的例题)时被进位的(消失的)1的数量。
三、基本的树状数组怎么写?
我们先来考虑一个简单的问题,就是求区间和。要求支持单点修改,区间询问。

那么我们对于每个节点保存它的子树的和。
修改每个节点的时候直接沿着父亲一路找上去就可以了。

void add(int x,int delta){
    for(;x<=n;x+=x&-x)bit[x]+=delta;
}

查询的时候我们可以把区间和改为两个前缀和的差。而一个前缀和可以拆分成 O(log2n) 棵子树。

int query(int x){
    int ans=0;
    for(;x;x-=x&-x)ans+=bit[x];
    return ans;
}

四、如何初始化树状数组?
比如说我们有一个数组a,我们要建出它的bit,我们该怎么做呢?
我以前的做法是把n个数插入进去。

但显然这是不必要的。

我们可以从1~n递推,假设推到i时bit[i]已经推出来了,那么显然它只需贡献给bit[i+lowbit(i)]即可。

void build(){
    for(int i=1,x;i<=n;++i){
        bit[i]+=a[i];
        if((x=i+(i&-i))<=n)bit[x]+=bit[i];
    }
}

五、维护最值?
显然,树状数组维护最值的话,只能支持两种操作:增大一个位置的数,查询前缀最值。
这看起来非常苛刻,但是其实在很多情况下,都是满足的。最常见的是bit+扫描线/dp这种的。

但是,如果时间复杂度允许是 O(log22n) ,树状数组也是可以做到维护最值的。

初始化当然不必说。

void build(){
    for(int i=1,x;i<=n;++i){
        bit[i]=max(bit[i],a[i]);
        if((x=i+(i&-i))<=n)bit[x]=max(bit[x],bit[i]);
    }
}

修改的时候,我们只需要修改x的 O(log2n) 个祖先,而每个祖先又有 O(log2n) 个儿子。

void update(int x,int A){
    a[x]=A;
    for(int y,i;x<=n;x+=x&-x){
        bit[x]=a[x];
        for(y=x-1,i=1;y&i;y-=i,i<<=1)bit[x]=max(bit[x],bit[y]);
    }
}

查询[l,r]的时候我们把它分成[l,r]路径上的点和被完全覆盖的子树两部分,因为[l,r]路径上只有 O(log2n) 个点,所以被完全覆盖的子树显然只有 O(log22n) 个。

int query(int l,int r){
    int ans=-0x7fffffff;
    --l;
    for(int x;r>l;--r){
        for(;r&&r-(r&-r)>=l;r-=r&-r)ans=max(ans,bit[r]);
        if(r>l)ans=max(ans,a[r]);
    }
    return ans;
}

当然。。这 O(log22n) 的玩意儿显然是没什么卵用的东西。仅供娱乐~
六、维护后缀?
Q:如何支持单点修改,后缀和查询?
A:= =这不跟区间查询一样么。
Q:查两边前缀和?常数太大!不开心。
A:那就把原数组反过来不就行了么。
Q:坐标什么的反来反去,很麻烦的。好烦人,不开心!
A:。。。

其实。。我们只需要把修改和询问改一下下就好了!
先上代码:

void build(){
    for(int i=n;i;--i){
        bit[i]+=a[i];
        bit[i-(i&-i)]+=bit[i];
    }
}
void add(int x,int delta){
    for(;x;x-=x&-x)bit[x]+=delta;
}
int query(int x){
    int ans=0;
    for(;x<=n;x+=x&-x)ans+=bit[x];
    return ans;
}

←_←看起来就像是写残了的树状数组。。
但为什么可以这样搞?!

我们可以将修改看成是在树上打永久化的标记,查询就是在收集标记。

但是,我们需要更高逼格的解释方法。
注意到树状数组中x的父亲是x+lowbit(x),而如果x+lowbit(x)>n,那么其实它的父亲是不存在的,就是说其实它是一棵森林。我们在build的时候为了防止数组越界,还要特判一下,好烦人!
所以我们不妨把x的父亲改为x-lowbit(x),这样就是一棵以0为根的树啦!这样的话,x的子树就是[x,min(n,x+lowbit(x)-1)]。上述代码就变得显而易见了。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值