树状数组(我要把它更新完了!!!)

树状数组

对不起各位
1e9*1e9的矩阵区间求和,区间修改
我还想不出来如何安全地用树状数组优化

用例子来使树状数组这个结构清晰:

有一个数组a,下标从0到n-1,现在给你w次修改,q次查询,修改的话是修改数组中某一个元素的值;查询的话是查询数组中任意一个区间的和,w + q < 500000。

这个题普通地去单点修改以及单点查询无疑是会超时的
所以可以用到树状数组,线段树,分块等多种优化,那么现在就可以用树状数组来做这道题,

可以引入这样一张图
这里写图片描述

入门

c[i]表示从下标i-lowbit(i)+1到下标i的所有数的和
如果你是新学的,我知道你会有疑问,我一一解答
1.lowbit是什么,
lowbit(i)=x&-x
是i的二进制从最右端的一到最右端的这一段二进制所反映的十进制
比如说:
6=0110
lowbit(6)=10
上面那个公式的意思是:
-x表示x的补码,补码是反码+1
比如说
6=0110
-x=1010
相同为0不同为1
lowbit(x)=10;
那么,lowbit的用处我后面再说。
2.c数组的构造
c[i]表示a[i-lowbit(i)+1]到a[i]的和
由看图可知
很神奇的地方在于,i-lowbit[i]和i+lowbit[i]算出来的位置,非常神奇,就会是断点
所以引入函数
1.我第一次理解的树状数组的构造函数

void chuangzaozhekeshu(int x)
    for (int i=x; i>=i-lowbit(x)+1; i--)
      c[x]+=a[i];

2 但其实并不用这么麻烦而且我这个时间比较复杂

void add(int x,int k)
{
    while(x<=n)
      {
        c[x]+=k;
        x+=lowbit(x);
      }
}

很轻松,就是每一个往上走的断点都把当前这个数加上去
按照我的写法,单点修改要另外写一个add
但是这个不用很轻松
3.区间求和

sum(x)表示1到x的所有数的和
比如说
sum(6)=sum(6)+sum(6-lowbit(6)){等同于sum(4)}(自己推一遍理解一下)+sum(…)知道x-lowbit(x)=0
譬如要求(x,y)的的和
即是sum(y)-sum(x-1).

int sum(int x)
{
    int ans=0;
    while(x!=0)
      {
        ans+=c[x];
        x-=lowbit(x);
      }
    return ans;
}

到此树状数组只是入门,明天我介绍区间修改等深入理解。
我先去学习一些字符串的应用

#明天到了!
即用差分数组来区间修改即区间求和
区间修改
设原数组为a[i], 设数组d[i]=a[i]−ai−1,则 a[i]=∑ij=1d[j],可以通过 求d[i]的前缀和查询
当给区间[l,r]加上x的时候,a[l] 与前一个元素 a[l−1] 的差增加了x,a[r+1] 与 a[r] 的差减少了x。根据d[i]数组的定义,只需给d[l] 加上 x, 给d[r+1] 减去 x 即可。
上代码

void add(int p, int x){ //这个函数用来在树状数组中直接修改
    while(p <= n) sum[p] += x, p += p & -p;
}
void range_add(int l, int r, int x){ //给区间[l, r]加上x
    add(l, x), add(r + 1, -x);
}

区间查询
位置p的前缀和 =
∑i=1pa[i]=∑i=1p∑j=1id[j]

在等式最右侧的式子∑pi=1∑ij=1d[j]中,d[1] 被用了p次,d[2]被用了p−1次……那么我们可以写出:

位置p的前缀和 =
∑i=1p∑j=1id[j]=∑i=1pd[i]∗(p−i+1)=(p+1)∗∑i=1pd[i]−∑i=1pd[i]∗i

那么我们可以维护两个数组的前缀和:
一个数组是 sum1[i]=d[i],
另一个数组是 sum2[i]=d[i]∗i。

void add(ll p, ll x){
    for(int i = p; i <= n; i += i & -i)
        sum1[i] += x, sum2[i] += x * p;
}
void range_add(ll l, ll r, ll x){
    add(l, x), add(r + 1, -x);
}
ll ask(ll p){
    ll res = 0;
    for(int i = p; i; i -= i & -i)
        res += (p + 1) * sum1[i] - sum2[i];
    return res;
}
ll range_ask(ll l, ll r){
    return ask(r) - ask(l - 1);//查询
}

#后天到了!
二维树状数组
举个例子
矩阵的求和和修改
设 C[x][y] = ∑ a[i][j], 其中,
x-lowbit(x) + 1 <= i <= x,
y-lowbit(y) + 1 <= j <= y.//
c[x][y] 右下角为(x, y),高为lowbit(x), 宽为 lowbit(y)

例:举个例子来看看C[][]的组成。
设原始二维数组为:
 A[][]={{a11,a12,a13,a14,a15,a16,a17,a18,a19},
{a21,a22,a23,a24,a25,a26,a27,a28,a29},
{a31,a32,a33,a34,a35,a36,a37,a38,a39},
{a41,a42,a43,a44,a45,a46,a47,a48,a49}};
那么它对应的二维树状数组C[][]呢?

记:
B[1]={a11,a11+a12,a13,a11+a12+a13+a14,a15,a15+a16,…} 这是第一行的一维树状数组
B[2]={a21,a21+a22,a23,a21+a22+a23+a24,a25,a25+a26,…} 这是第二行的一维树状数组
B[3]={a31,a31+a32,a33,a31+a32+a33+a34,a35,a35+a36,…} 这是第三行的一维树状数组
B[4]={a41,a41+a42,a43,a41+a42+a43+a44,a45,a45+a46,…} 这是第四行的一维树状数组
那么:
C[1][1]=a11,C[1][2]=a11+a12,C[1][3]=a13,C[1][4]=a11+a12+a13+a14,c[1][5]=a15,C[1][6]=a15+a16,…
这是A[][]第一行的一维树状数组

C[2][1]=a11+a21,C[2][2]=a11+a12+a21+a22,C[2][3]=a13+a23,C[2][4]=a11+a12+a13+a14+a21+a22+a23+a24,
C[2][5]=a15+a25,C[2][6]=a15+a16+a25+a26,…
这是A[][]数组第一行与第二行相加后的树状数组

C[3][1]=a31,C[3][2]=a31+a32,C[3][3]=a33,C[3][4]=a31+a32+a33+a34,C[3][5]=a35,C[3][6]=a35+a36,…
这是A[][]第三行的一维树状数组

C[4][1]=a11+a21+a31+a41,C[4][2]=a11+a12+a21+a22+a31+a32+a41+a42,C[4][3]=a13+a23+a33+a43,…
这是A[][]数组第一行+第二行+第三行+第四行后的树状数组

这个我觉得不好描述,直接上代码

单点修改

void add(int x,int y,int p){  
    while(x<=n){
        for(int i=y;i<=m;i+=lowbit(i)){
            C[x][i]+=p;
        } 
        x+=lowbit(x);
    }  
} 

外围循环枚举每一行,内循环在行内进行一维树状数组的单点修改,从而实现二维树状数组的单点修改。把跟它有关的所有都进行修改。

以原点为一个端点的子矩阵和

int sum(int x,int y){  
    int result = 0;  
    while(x>0){
        for(int i=y;i>0;i-=lowbit(i)){
            result+=C[x][i];
        }
        x-=lowbit(x);
    }  
    return result;  
}  

以任意两点为左上和右下两个端点的子矩阵和
这个呢,我们可以根据矩阵前缀和来理解
sum[i][j]=sum[i][j-1]+sum[i-1][j]-sum[i-1][j-1]+a[i][j]
由此可得

int ask(int x1,int y1,int x2,int y2){
    return sum(x2,y2)+sum(x1-1,y1-1)-sum(x2-1,y1)-sum(x1,y2-1);
}

其实二维树状数组也不是很难,
有下面一道例题:

区间修改

scanf("%d %d %d %d", &x1, &y1, &x2, &y2);
Add(x1, y1, 1);
Add(x2+1, y1, 1);
Add(x1, y2+1, 1);
Add(x2+1, y2+1, 1);

这里用到的,是差分数组的思想
倘若我们要把一维的树状数组惊醒区间修改,可以先得出差分数组,只对我们当前要修改的这一段(l,r)进行d[l]+=x,d[r+1]-=x;

上帝造题的七分钟

输入数据的第一行为X n m,代表矩阵大小为 n×m 。
从输入数据的第二行开始到文件尾的每一行会出现以下两种操作:

L a b c d delta —— 代表将 (a,b),(c,d)(a,b),(c,d) 为顶点的矩形区域内的所有数字加上delta。
k a b c d —— 代表求 (a,b),(c,d)(a,b),(c,d) 为顶点的矩形区域内所有数字的和。
请注意, k为小写。

输出格式:
针对每个k操作,在单独的一行输出答案。

该题难度是NOI/NOI+/CTSC
但是,在学习了二维树状数组,是不是觉得很简单?

上代码:

#include<bits/stdc++.h>
using namespace std;
long long n,tree[101000],tree1[100000],m,c,l,r,k;
void add(int x,int y)
{
    //修改tree
    for (int i=x;i<=n;i+=(i&(-i)))
        tree[i]+=y;
}
void add1(int x,int y)
{
    //修改tree1
    for (int i=x;i<=n;i+=(i&(-i)))
        tree1[i]+=y;
}
long long ask(long long x)
{
    long long ans=0,ans1=0;
    for (long long i=x;i;i-=(i&(-i)))
      ans+=tree[i];
    ans*=x;
    for (long long i=x;i;i-=(i&(-i)))
      ans1+=tree1[i];
    //分别求出ans*x与ans1(应该可以合起来写)
    return ans-ans1;
}
int main()
{
    scanf("%lld%lld",&n,&m);
    long long c=0,last=0,a=0;
    for (long long i=1;i<=n;i++)
    {
        scanf("%lld",&a);//long long读入要用lld
        c=a-last;//求差分
        add(i,c);add1(i,c*(i-1));
        last=a;
    }
    for (long long i=1;i<=m;i++)
    {
        scanf("%lld",&c);
        if (c==1)
        {
          //区间修改
          scanf("%lld%lld%lld",&l,&r,&k);
            add(l,k);add(r+1,-k);//在l处+x
            add1(l,k*(l-1));add1(r+1,(-k)*(r));//在r+1处-x,相当于抵消前面的+x
        }
        if (c==2)
        {
           //区间查询 
           scanf("%d%d",&l,&r);
            cout<<ask(r)-ask(l-1)<<endl;
        }
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值