树状数组

作用

树状数组可以解决一些区间问题,虽然功能没有线段树高级(可以被线段树完全代替),但是思想简单,代码复杂度也很低,常数也很低,低到足以蔑视线段树

实现

给出一个长度为n(1<=n<=100000)的序列,提出m(1<=m<=100000)个操作,操作有两种形式:
Q x y:在x点上加上y(1<=x<=n;-10000<=y<=10000)
C x y:求出序列中x到y(包括x和y)的和(1<=x<=y<=n)

暴力很好想,每次Q操作的时候重新构造前缀和,但是效率很低。这时候树状数组就派上用场了。
这里写图片描述
我们建立一个c数组,表示a[i-lowbit(i)]~a[i]这一段的累加和,其中lowbit(i)为i&(-i),效果是得到2^k,k是i二进制下末尾的0的个数(根据反码和补码,-i是i按位取反之后加上1,所以i&(-i)就会得到2^k)那么:
c[1]=a[1]
c[2]=a[1]+a[2]=c[1]+a[2]
c[3]=a[3]
c[4]=a[1]+a[2]+a[3]+a[4]=c[2]+c[3]+a[4]
c[5]=a[5]
c[6]=a[5]+a[6]=c[5]+a[6]
c[7]=a[7]
c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]=c[4]+c[6]+c[7]+a[8]
……
这个就是所谓的树状数组(发明者是Fenwick,所以也叫Fenwick_Tree),观察图就会发现像树一样。那么怎么使用呢?

单点修改区间求和

1.单点修改

当a[i]改变时,哪些c会改变呢?观察上面的图,会发现:c[i]改变,c[i+lowbit(i)]会改变,c[i+lowbit(i)+lowbit(i+lowbit(i))]也会改变……那么就很容易写出一个Insert过程:

void Insert(int x,int tem) {while (x<=n) s[x]+=tem,x+=lowbit(x);}

2.区间求和

前缀和利用了容斥原理,而树状数组区间查询的目标就是求前缀和,再次观察图,会发现1~x的前缀和就是: c[x]+c[x-lowbit(x)]+c[x-lowbit(x)-lowbit(x-lowbit(x))]+……
(取了c[x]之后,x-lowbit(x)+1~x这一段就都取了,减去lowbit(x),继续求1~x-lowbit(x))
Ask过程也很容易写出:

int Ask(int x) {int sum=0;while (x) sum+=s[x],x-=lowbit(x);return sum;}

那么x~y的区间总和就是Ask(y)-Ask(x-1)。

3.模板

#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=100000;

int n,te;
struct FenwickTree
{
    int s[maxn+5];
    void clear() {memset(s,0,sizeof(s));}
    int lowbit(int x) {return x&(-x);}
    void Insert(int x,int tem) {while (x<=n) s[x]+=tem,x+=lowbit(x);}
    int Ask(int x) {int sum=0;while (x) sum+=s[x],x-=lowbit(x);return sum;}
};
FenwickTree tr;

char getrch() {char ch=getchar();while (ch!='x'&&ch!='y') ch=getchar();return ch;}
int main()
{
    freopen("FenwickTree.in","r",stdin);
    freopen("FenwickTree.out","w",stdout);
    scanf("%d%d",&n,&te);tr.clear();
    while (te--)
    {
        char td=getrch();int x,y;scanf("%d%d",&x,&y);
        if (td=='x') tr.Insert(x,y); else printf("%d\n",tr.Ask(y)-tr.Ask(x-1));
    }
    return 0;
}

区间增减区间求和

ps:之前根本不知道有这种东西QAQ,见到Lynstery大神写之后自叹不如啊。

1.区间增减

令c1[i]=a[i]-a[i-1](好像叫做差分),则增减a[i~j]的时候会发现只有c1[i]和c1[j+1]改变了,其他都没变。
然后用c1表示a[i]就是Σc1[1~i],求a[1~x]就是Σc1[1]+Σc1[1~2]+……Σc1[1~x]。这有何用?继续看:
Σc1[1]+Σc1[1~2]+……Σc1[1~x]
=(c1[1])+(c1[1]+c1[2])+……+(c1[1]+c1[2]+……+c1[x])
=x*c1[1]+(x-1)*c1[2]+……+c1[x]
=x*(c1[1]+c1[2]+……+c1[x])-(0*c1[1]+1*c1[2]+……+(x-1)*c1[x])
令c2[i]=(i-1)*c1[i]
=x*Σc1[1~x]-Σc2[1~x]
看最后一个式子,又变成前缀和了,又因为差分过后只需要修改c1的两个节点和c2的两个节点,所以问题回归到树状数组单点修改!

2.区间求和

和上面的区间求和一样,只不过要计算如下式子:
y*Σc1[1~y]-Σc2[1~y]-((x-1)*Σc1[1~x-1]-Σc2[1~x-1])。

3.模板

#include<cstdio>
#include<cstring>
using namespace std;
typedef long long LL;
const int maxn=100000;

int n,te;
struct FenwickTree
{
    LL s[2][maxn+5];
    void clear() {memset(s,0,sizeof(s));}
    int lowbit(int x) {return x&(-x);}
    void Insert(int L,int R,LL tem)
    {
        int x;R++;
        x=L;while (x<=n) s[0][x]+=tem,s[1][x]+=tem*(L-1),x+=lowbit(x);
        x=R;while (x<=n) s[0][x]-=tem,s[1][x]-=tem*(R-1),x+=lowbit(x);
    }
    LL Ask(int R)
    {
        int x=R;LL sum=0;
        while (x) sum+=R*s[0][x]-s[1][x],x-=lowbit(x);
        return sum;
    }
};
FenwickTree tr;

bool Eoln(char ch) {return ch==10||ch==13||ch==EOF;}
int readi(int &x)
{
    int tot=0,f=1;char ch=getchar(),lst='+';
    while ('9'<ch||ch<'0') {if (ch==EOF) return EOF;lst=ch;ch=getchar();}
    if (lst=='-') f=-f;
    while ('0'<=ch&&ch<='9') tot=tot*10+ch-48,ch=getchar();
    x=tot*f;
    return Eoln(ch);
}
int readl(LL &x)
{
    LL tot=0,f=1;char ch=getchar(),lst='+';
    while ('9'<ch||ch<'0') {if (ch==EOF) return EOF;lst=ch;ch=getchar();}
    if (lst=='-') f=-f;
    while ('0'<=ch&&ch<='9') tot=tot*10+ch-48,ch=getchar();
    x=tot*f;
    return Eoln(ch);
}
int main()
{
    freopen("FenwickTree.in","r",stdin);
    freopen("FenwickTree.out","w",stdout);
    readi(n);readi(te);
    for (int i=1;i<=n;i++)
    {
        LL x;readl(x);
        tr.Insert(i,i,x);
    }
    while (te--)
    {
        int td,x,y;LL z;readi(td);readi(x);readi(y);
        if (td==1) readl(z),tr.Insert(x,y,z); else
        printf("%lld\n",tr.Ask(y)-tr.Ask(x-1));
    }
    return 0;
}

效率

好像效率不容易计算,其实我们只需要从lowbit的角度来看就行了。

对于单点修改,我们需要修改至多 log2(n) 次,为什么呢?因为第一次取lowbit最小是1,加上1之后,最后一位肯定不是1,然后下一次取lowbit最小是2,加上2之后,最后两位肯定不是1。以此类推,极端情况下每次lowbit的值会是这样的:1,2,4,8,……,2^k。所以至多加 log2(n) 次。

对于区间求和,效率也是 log2(n) ,这个很容易分析,因为每次lowbit取到的是2^k(k是二进制下末尾的0的个数),一旦减去2^k,末尾就肯定少掉一个1(二进制),不停少掉1直到变成0为止。而n的二进制最多有 log2(n) 个1,所以至多减 log2(n) 次。

简单转化

给出一棵根固定且节点有权值的树,再给出若干个操作,有两种:1.将一个节点的权值改变。2.询问以一节点为根的子树权值总和。

遇到这种题目,我们肯定会想到前缀和(因为一个节点子树的权值总和是他所有儿子子树权值总和加上他自己的权值,类似前缀和),但是不知道如何实现,其实仔细想想就发现果断应该把树按照Dfs序变成链,这样才可能用前缀和。

先Dfs,然后对于i点,记录int[i]和outt[i],表示进栈顺序和出栈顺序,那么很明显以i为根的子树权值总和就是int[i]~outt[i]的区间加和。这样处理完成之后,就可以用树状数组进行优化了。

缺陷

显然树状数组是利用容斥原理的,不能用来处理区间极值(区间极值不满足容斥原理)等一系列问题。而且区间操作只能增减,不能乘除或者修改。所以遇到这些问题还是乖乖打线段树吧。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值