树状数组

参考https://www.cnblogs.com/xenny/p/9739600.html

树状数组与线段树的区别

1.两者在复杂度上同级, 但是树状数组的常数明显优于线段树, 其编程复杂度也远小于线段树.

2.树状数组的作用被线段树完全涵盖, 凡是可以使用树状数组解决的问题, 使用线段树一定可以解决, 但是线段树能够解决的问题树状数组未必能够解决.

树状数组的介绍

(注意数组的下标时从1开始)

黑色数组代表原来的数组(下面用A[i]代替)

红色结构代表我们的树状数组(下面用C[i]代替)

  • C[1] = A[1];
  • C[2] = A[1] + A[2];
  • C[3] = A[3];
  • C[4] = A[1] + A[2] + A[3] + A[4];
  • C[5] = A[5];
  • C[6] = A[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[i] = A[i - 2k+1] + A[i - 2k+2] + ... + A[i];   
k为i的二进制中从最低位到高位连续零的长度

树状数组的例题+代码

http://acm.hdu.edu.cn/showproblem.php?pid=1166

Input

第一行一个整数T,表示有T组数据。
每组数据第一行一个正整数N(N<=50000),表示敌人有N个工兵营地,接下来有N个正整数,第i个正整数ai代表第i个工兵营地里开始时有ai个人(1<=ai<=50)。
接下来每行有一条命令,命令有4种形式:
(1) Add i j,i和j为正整数,表示第i个营地增加j个人(j不超过30)
(2)Sub i j ,i和j为正整数,表示第i个营地减少j个人(j不超过30);
(3)Query i j ,i和j为正整数,i<=j,表示询问第i到第j个营地的总人数;
(4)End 表示结束,这条命令在每组数据最后出现;
每组数据最多有40000条命令

Output

对第i组数据,首先输出“Case i:”和回车,
对于每个Query询问,输出一个整数并回车,表示询问的段中的总人数,这个数保持在int以内。

Sample Input

1
10
1 2 3 4 5 6 7 8 9 10
Query 1 3
Add 3 6
Query 2 7
Sub 10 2
Add 6 3
Query 3 10
End 

Sample Output

Case 1:
6
33
59

代码

#include <bits/stdc++.h>
using namespace std;

int n,m;
int a[50005],c[50005]; //对应原数组和树状数组

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

void updata(int i,int k){    //在i位置加上k
    while(i <= n){
        c[i] += k;
        i += lowbit(i);
    }
}

int getsum(int i){        //求A[1]~A[i]的和
    int res = 0;
    while(i > 0){
        res += c[i];
        i -= lowbit(i);
    }
    return res;
}

int main(){
    int t;
    cin>>t;
    for(int tot = 1; tot <= t; tot++){
        cout << "Case " << tot << ":" << endl;
        memset(a, 0, sizeof a);
        memset(c, 0, sizeof c);
        cin>>n;
        for(int i = 1; i <= n; i++){
            cin>>a[i];
            updata(i,a[i]);   //输入初值的时候,也相当于更新了值
        }

        string s;
        int x,y;
        while(cin>>s && s[0] != 'E'){
            cin>>x>>y;
            if(s[0] == 'Q'){    //求和操作
                int sum = getsum(y) - getsum(x-1);    //x-y区间和也就等于1-y区间和减去1-(x-1)区间和
                cout << sum << endl;
            }
            else if(s[0] == 'A'){
                updata(x,y);
            }
            else if(s[0] == 'S'){
                updata(x,-y);    //减去操作,即为加上相反数
            }
        }

    }
    return 0;
}

代码解析:

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

其中的x&(-x)

当一个偶数与它的负值向与时,结果是能被这个偶数整除的最大的2的n次幂

当一个奇数与它的负值向与时结果一定是1.

image-20200421090900644

用途1 单点更新 区间查询

单点更新

void updata(int i,int k){    //在i位置加上k
    while(i <= n)//注意是小于等于n,不是小于n!!!!
    {
        c[i] += k;
        i += lowbit(i);
    }
}
//*************************************
 for(int i = 1; i <= n; i++){
            cin>>a[i];
            updata(i,a[i]);   //输入初值的时候,也相当于更新了值
        }
例如i==1:c[1]=c[1]+a[1]; i=i+1=2;
		 c[2]=c[2]+a[2]; i=i+2=4;
		 c[4]=c[4]+a[4]; i=i+4=8;
		 c[8]=c[8]+a[8]; i=i+8=16结束  将a[]数组中所有需要加a[1]的全都加了

区间查询

int getsum(int i){        
    int res = 0;
    while(i > 0){
        res += c[i];
        i -= lowbit(i);
    }
    return res;
}
例如:求a[1]到a[8]的和:也就是c[8]的值:
res=res+c[8]; i=i-8=0;结束

再例如:求a[1]到a[7]的和:
res=res+c[7] i=i-1=6   a[7]
res=res+c[6] i=i-2=4   a[6] a[5]
res=res+c[4] i=i-4=0   a[4] a[3] a[2] a[1]

用途2:区间更新 单点查询

这里我们引入差分,利用差分建树。

规定A[0]=0

A[] =0 1 2 3 5 6 9//原数组
D[] =0 1 1 1 2 1 3//差分数组(d[i]=a[i]-a[i-1])

如果我们把[2,5]区间内值加上2,则变成了

A[] =0 1 4 5 7 8 9
D[] =0 1 3 1 2 1 1

当某个区间[x,y]值改变了,区间内的差值是不变的,只有D[x]和D[y+1]的值发生改变

这样就把,原来要更新一个区间的值变成了只需要更新两个点

代码:

int n,m;
int a[50005] = {0},c[50005]; //对应原数组和树状数组

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

void updata(int i,int k)
{    //在i位置加上k
    while(i <= n)
    {
        c[i] += k;
        i += lowbit(i);
    }
}

int getsum(int i)
{        //求D[1 - i]的和,即A[i]值
    int res = 0;
    while(i > 0)
    {
        res += c[i];
        i -= lowbit(i);
    }
    return res;
}

int main(){
    cin>>n;
    for(int i = 1; i <= n; i++)
    {
        cin>>a[i];
        updata(i,a[i] - a[i-1]);   //输入初值的时候,也相当于更新了值
    }
    
    //[x,y]区间内加上k
    updata(x,k);    //d[x]需要增加k,所以相应的c[]数组中需要增加的都要增加
    updata(y+1,-k); 
    
    //查询i位置的值,就是查询a[i]的值,就是求从d[0]到d[i]的和,就是借c[]数组用getsum函数求和   (单点查询)
    int sum = getsum(i);

    return 0;
}

区间更新:

  	updata(i,a[i] - a[i-1]);  
	updata(x,k);   
    updata(y+1,-k); 

单点查询:

int sum = getsum(i);

用途3:区间查询 区间更新

思路:https://blog.csdn.net/bestsort/article/details/80796531

怎么求呢?我们基于问题2的“差分”思路,考虑一下如何在问题2构建的树状数组中求前缀和:

位置p的前缀和 =\sum_{i=1}{p}a[i]=\sum_{i=1}{p}\sum_{j=1}^{i}d[j]

在等式最右侧的式子\sum_{i=1}{p}\sum_{j=1}{i}d[j]中,d[1]被用了p次,d[2]被用了p-1次……那么我们可以写出:

位置p的前缀和 =\sum_{i=1}{p}\sum_{j=1}{i}d[j]=\sum_{i=1}{p}d[i]*(p-i+1)=(p+1)*\sum_{i=1}{p}d[i]-\sum_{i=1}^{p}d[i]*i

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

int n,m;
int a[50005] = {0};
int sum1[50005];    //(D[1] + D[2] + ... + D[n])
int sum2[50005];    //(1*D[1] + 2*D[2] + ... + n*D[n])

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

void updata(int i,int k){
    int x = i;    //因为x不变,所以得先保存i值
    while(i <= n){
        sum1[i] += k;
        sum2[i] += k * (x-1);
        i += lowbit(i);
    }
}

int getsum(int i){        //求前缀和
    int res = 0, x = i;
    while(i > 0){
        res += x * sum1[i] - sum2[i];
        i -= lowbit(i);
    }
    return res;
}

int main(){
    cin>>n;
    for(int i = 1; i <= n; i++){
        cin>>a[i];
        updata(i,a[i] - a[i-1]);   //输入初值的时候,也相当于更新了值
    }

    //[x,y]区间内加上k
    updata(x,k);    //A[x] - A[x-1]增加k
    updata(y+1,-k);        //A[y+1] - A[y]减少k

    //求[x,y]区间和
    int sum = getsum(y) - getsum(x-1);

    return 0;
}

区间更新

void updata(int i,int k){
    int x = i;    //因为x不变,所以得先保存i值
    while(i <= n){
        sum1[i] += k;
        sum2[i] += k * (x-1);
        i += lowbit(i);
    }
}
 updata(x,k);    //A[x] - A[x-1]增加k
    updata(y+1,-k);        //A[y+1] - A[y]减少k

区间查询

 //求[x,y]区间和
    int sum = getsum(y) - getsum(x-1);

用途4:求逆序对

1.逆序对的定义

逆序对就是序列a中ai>aj且i<j的有序对。

方法一:未进行离散化

我们可以先开一个大小为a的最大值的数组 t,每当读入一个数时,我们可以用桶排序的思想,将t[a[i]]加上1,然后我们统计t[1]~t[a[i]]的和ans,ans - 1(除掉这个数本身)就是在这个数前面有多少个数比它小。我们只要用i-ans就可以得出前面有多少数比它大,也就是逆序对的数量。

#include <iostream>
#include<string>
#include<algorithm>
#define lowbit(x) (x)&(-x)
using namespace std;

const int maxn = 1e6 + 10;
int c[maxn], n, result;

void update(int i)
{
	while (i <= maxn)
	{
		c[i]++;
		i += lowbit(i);
	}
}

int getsum(int i)
{
	int ans = 0;
	while (i > 0)
	{
		ans += c[i];
		i -= lowbit(i);
	}
	return ans;
}

int main()
{
	int temp;
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
	{
		scanf("%d", &temp);
		update(temp);
		result += i - getsum(temp);//使用i减去前面比自己小的就是比自己大的
	}
	printf("%d\n", result);
	return 0;
}

方法二:离散化

现在这个代码可以在数的最大值比较小的时候可以正确的得出答案,如果数据很大,这回造成我们要开的空间很大。

我们是否可以适当的减少空间的需求呢?我们看看下面这些数:

1 2 3 4 5 10

这6个数我们需要使用大小10的数组来存储,我们仔细想想,可以发现中间 6 7 8 9 这4个位置是没有用到的,也就是说这4个空间被浪费了。怎样减少这样的浪费呢?

我们可以在读完数数据后对他进行从小到大排序,我们用排完序的数组的下标来进行运算。这样可以保证小的数依旧小,大的数依旧大。这一步叫做离散化

#include<iostream>
#include<string>
#include<cstring>
#include<algorithm>
using namespace std;
struct node
{
	int data;
	int index;
}list[1000];
int aa[1000], c[1000];
int n;
int lowbit(int x)
{
	return x&(-x);
}
bool cmp(struct node &a, struct node&b)
{
	return a.data < b.data;
}
void update(int i)
{
	while (i <=n)
	{
		c[i] +=1;
		i += lowbit(i);
	}
}
int getsum(int i)
{
	int ans = 0;
	while (i > 0)
	{
		ans += c[i];
		i -= lowbit(i);
	}
	return ans;
}


int main()
{
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
	{
		scanf("%d", &list[i].data);
		list[i].index = i;
	}
	sort(list+1, list + n+1, cmp);
	for (int i = 1; i <= n; i++)
		aa[list[i].index] = i;
	long long answer = 0;
	for (int i = 1; i <= n; i++)
	{
		update(aa[i]);
		answer += i - getsum(aa[i]);//用来存储原数第i个数的order下标是什么
	}
	cout << answer;
}


或者不用aa数组
    /*for (int i = 1; i <= n; i++)
		aa[list[i].index] = i;*/
	long long answer = 0;
	for (int i = 1; i <= n; i++)
	{
		update(list[i].index);
		answer += i - getsum(list[i].index);//用来存储原数第i个数的order下标是什么
	}

用途5:求区间最大值

void update(int i ,int k)
{
	while (i <= n)
	{
		c[i] = max(c[i], k);
		i += lowbit(i);
	}
}
int getsum(int i)
{
	int ans = 0;
	while (i > 0)
	{
		ans=max(ans, c[i]);
		i -= lowbit(i);
	}
	return ans;
}

改进:

https://blog.csdn.net/u010598215/article/details/48206959?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1

二维树状数组

C[x][y]记录的是右下角为(x, y),高为lowbit(x), 宽为 lowbit(y)的区间的区间和。

单点修改 区间查询

单点修改

void updata(int x,int y,int k)//将点(x, y)加上z
{    int memy=y;
    while(x <= n)
    {
        y=memy;
        while(y<=n)
        {
            c[x][y]+=k;
            y+=lowbit(y);
        }
       x+=lowbit(x);
    }
}

区间查询

int getsum(int x int y)
{        //求前缀和
    int res = 0, memy=y;
    while(x>0)
    {
        y=memy;
        while(y>0)
        {
            res += c[x][y];
        	y -= lowbit(y);
        }
        x-=lowbit(x);
    }
    return res;
}

区间修改 单点查询

二维前缀和:

sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j]

那么我们可以令差分数组d[i][j]表示a[i][j]a[i-1][j]+a[i][j-1]-a[i-1][j-1]的差。

下面是给最中间的3*3矩阵加上x时,差分数组的变化:

0  0  0  0  0
0 +x  0  0 -x
0  0  0  0  0
0  0  0  0  0
0 -x  0  0 +x

效果:

0  0  0  0  0
0  x  x  x  0
0  x  x  x  0
0  x  x  x  0
0  0  0  0  0
void add(int x, int y, int z){ 
    int memo_y = y;
    while(x <= n){
        y = memo_y;
        while(y <= n)
            tree[x][y] += z, y += y & -y;
        x += x & -x;
    }
}
//与单点修改 区间查询的add一样

void range_add(int xa, int ya, int xb, int yb, int z){
    add(xa, ya, z);
    add(xa, yb + 1, -z);
    add(xb + 1, ya, -z);
    add(xb + 1, yb + 1, z);
}//分别对四个特殊位置进行加减运算
void ask(int x, int y){
    int res = 0, memo_y = y;
    while(x){
        y = memo_y;
        while(y)
            res += tree[x][y], y -= y & -y;
        x -= x & -x;
    }
}
posted @ 2020-04-26 22:28  Jason66661010  阅读(297)  评论(0编辑  收藏  举报