线段树与树状数组学习总结——树状数组(一维&二维树状数组的单点&区间的查询&更新&区间最大值维护)

树状数组

1.基础内容

说一下树状数组,和线段树一样,线段树和树状数组都是为了加快素组的操作效率的,那么,为什么要弄两个数据结构来达到一个目的呢?
所以先说一下线段树与树状数组的区别,线段树的功能强大,而树状数组的速度更快。不过树状数组的时间复杂度也是O(logN)但是树状数组的常数小
然后我们说一下树状数组的样子。如图:
这里写图片描述

这里写图片描述
和线段树非常不同的是是树状数组的空间小,是多大就是多大,不像线段树一样要开4倍空间。
这个图看上去比较乱,说明一下,A数组就是我们平时说的数组,那么C数组就是它对应的数组。第一次看这个图我也很乱,但是,注意,所有的树状数组都是长一个样的,都是这么乱。。。说一下C数组吧,C数组的值就是他连出的线的值的合,如图二!
如图,我们可以看到:
C1 = A1
C2 = A1+A2
C3 = A3
C4 = A1+A2+A3+A4
C5 = A5
C6 = A5+A6
C7 = A7
C8 = A1+A2+A3+A4+A5+A6+A7+A8

2.单点更新与区间查询

很明显:
设节点编号为X,区间内有2^k个元素,(k为x转为二进制后末尾的0的个数,)

c(n)=A(n-(2^k)+1)+…+A(n)

其实我觉得并不明显。。。
不需要管那个性质,我们只需要知道怎么用,用函数写:

int lowbit(int x) {
    return x & -x;
}

这个比较简单好记

那么如何求和呢
直接给代码

int getsum(int x) {//目标:求和A1~Ax
    int res = 0;
    for (; x; x -= x & (-x))
        res += t[x];
    return res;
}

然后就是修改,与求和类似,不同的就是+=和-=

int change(int x) {
     for (; x <= maxn; x += x & (-x))
         t[x]++;
}

这里给出模板:

#include <iostream>
using namespace std;
const int MAX_N=10010;
int C[MAX_N];
int n;

int lowbit(int x){
    return x&(-x);
}

int getsum(int x){
    int res=0;
    for(;x;x-=lowbit(x)){
        res+=C[x];
    }
    return res;
}

void change(int x,int c){
    for(;x<=n;x+=x&(-x)){
        C[x]+=c;
    }
}

int main() {
    cin>>n;
    for(int i=1;i<=n;i++){
    int d;
    cin>>d;
    change(i,d);
    }
    for(int i=1;i<=n;++i){
        cout<<getsum(i)<<" ";
    }
    return 0;
}

注意这份代码是单点更新与整体求和
如何求一个区间的和呢?
简单getsum(b)-getsum(a-1);

3.区间更新

然后我们说一下线段树的区间更新和单点查询:
先说区间更新,这个需要用到差分的思想,直接抄了两位洛谷大犇的原话

来介绍一下差分
设数组a[]={1,6,8,5,10},那么差分数组b[]={1,5,2,-3,5}
也就是说b[i]=a[i]-a[i-1];(a[0]=0;),那么a[i]=b[1]+….+b[i];(这个很好证的)假如区间[2,4]都加上2的话,a数组变为a[]={1,8,10,7,10},b数组变为b={1,7,2,-3,3};
发现了没有,b数组只有b[2]和b[5]变了,因为区间[2,4]是同时加上2的,所以在区间内b[i]-b[i-1]是不变的.
所以对区间[x,y]进行修改,只用修改b[x]与b[y+1]:
b[x]=b[x]+k;b[y+1]=b[y+1]-k;

还有一个

这是树状数组的一个拓展,在树状数组中可以用前 i 项的和来表示第 i 个数.
那么当对 x ~ y 的区间进行修改的时候需要在树状数组中的第 x 个位置 + k, 第 y + 1 个位置 -k
这样便维护了这个树状数组
输出时候直接输出查询即可

注意这里的数组仅仅是一个差分的数组,就像整体在一部分抬高了一截,但是不管是抬高的那一段还是没抬高的,相对左右还是一样的,分界线除外。
而如果树状数组初始化为0这和差为了正好重合,相当于求两点差分和就是求两点区间和!
上代码:

#include <iostream>
#include <cstdio>
using namespace std;
int MAX_N=10000;
int s[MAX_N];
int n;

void lowbit(x){
 return x & (-x);
 } 

void change(int x, int v) {
    for(;x<=n;x+=lowbit(x))tree[x]+=v;
}
int main() {
    scanf("%d", &n);
    int last = 0, now;      //差分用 
    for (int i = 1; i <= n; i++) {
        scanf("%lld", &now);
        add(i, now - last);
        last = now;
    }
    int x, y;               //区间[x,y]+k 
    long long k;
    scanf("%d%d%lld", &x, &y, &k);
    add(x, k);
    add(y + 1, -k);
    return 0;
}

4.单点查询

有了区间更新后单点查询与区间更新代码一起上

#include <iostream>
#include <cstdio>
using namespace std;
int MAX_N=10000;
int s[MAX_N];
int n;

void lowbit(x){
 return x & (-x);
 } 

void change(int x, int v) {
    for(;x<=n;x+=lowbit(x))tree[x]+=v;
}


int query(int x) {
    int res = 0;
    for(;x;x-=lowbit(x)){
        res+=s[x];
    }
    return res;
}
int main() {
    //区间更新 
    scanf("%d", &n);
    int last = 0, now;      //差分用 
    for (int i = 1; i <= n; i++) {
        scanf("%lld", &now);
        add(i, now - last);
        last = now;
    }
    int x, y;               //区间[x,y]+k 
    long long k;
    scanf("%d%d%lld", &x, &y, &k);
    add(x, k);
    add(y + 1, -k);

    //单点查询
    int x;
    scanf("%d", &x);
    printf("%d\n", query(x));

    return 0;
}

5.二维树状数组

一个由数字构成的大矩阵,能进行两种操作
1) 对矩阵里的某个数加上一个整数(可正可负)
2) 查询某个子矩阵里所有数字的和,要求对每次查询,输出结果。
一维树状数组很容易扩展到二维,在二维情况下:数组A[][]的树状数组定义为:
C[x][y] = ∑ a[i][j], 其中,
x-lowbit(x) + 1 <= i <= x,
y-lowbit(y) + 1 <= j <= 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[][]数组第一行+第二行+第三行+第四行后的树状数组

给出模板代码:

#include <iostream>
using namespace std;
const int MAX_N=810;
int C[MAX_N][MAX_N];
int n;

int lowbit(int x){
    return x&(-x);
}

void change(int i,int j,int delta){
    for(int x=i;x<=n;x+=lowbit(x)){
        for(int y=j;y<=n;y+=lowbit(y)){
            C[x][y]+=delta;
        }
    }
}

int getsum(int i,int j){
    int res=0;
    for(int x=i;x;x-=lowbit(x)){
        for(int y=j;y;y-=lowbit(y)){
            res+=C[x][y];
        }
    }
    return res;
}

int main() {
    cin>>n;
    for(int i=1;i<=n;++i){
        for(int j=1;j<=n;++j){
            int d;
            cin>>d;
            change(i,j,d);
        }
    }
    int x,y;
    cin>>x>>y;
    cout<<getsum(x,y)<<endl;
    return 0;
}

最后说一个最重要的,初始化的时候不要为了“完全初始化”让程序出现change(0,X),lowbit(0)==0于是就掉如死循环了!

6.区间最大值维护

直接给代码
修改

void change(int r) {
    c[r] = a[r];
    for(int i = 1; i < lowbit(r); i <<= 1)
        c[r] = max(c[r], c[r-i]);
}

查找

int getmax(int l, int r) {
    int ret = a[r];
    while(l <= r) {
        ret = max(ret, a[r]);
        for(--r; r - l >= lowbit(r); r -= lowbit(r))
            ret = max(ret, c[r]);
    }
    return ret;
}

注意:它只支持末端插入,不支持单点修改操作。


猜你喜欢

转载自blog.csdn.net/liukairui/article/details/80920057