树状数组(未填完的坑)

Flag:八月底开学前写完!!!

介绍

树状数组(Binary Indexed Tree)其实是一种简单的数据结构,因为简单易懂经常代替线段树来求数列的前缀和、区间和等

原理

很久很久以前,有一个聪明绝顶的人,想到每一个十进制数都可以二进制表示(相当于由几个次数不同的2的幂相加得到),那我们前缀和是否也可以按照相似的方法划分成几个子序列的和?——于是,树状数组就这样诞生!!!
从一个a[1]~a[8]的前缀和入手分析:
l例图

黑色矩形为A[i] (即原先的数列)
红色矩形为C[i](即我们维护的树状数组)
下面到了找规律的时间了~ 也许你会说:我只看到的只是起起落落(/doge)
下方括号里的两个数分别为十进制和二进制下的表示
C[1(0001)]=A[1]
C[2(0010)]=A[1]+A[2]
C[3(0011)]=A[3]
C[4(0100)]=A[1]+A[2]+A[3]+A[4]
C[5(0101)]=A[5]
C[6(0110]=A[5]+A[6]
C[7(0111)]=A[7]
C[8(1000)]=A[1]+A[2]+……+A[7]+A[8]
规律结论

  • C[i]=A[j]+……+A[i](树状数组是一段连续的累加且末尾为A[i])
  • C[i]数组中累加A[]的个数为 2 k 2^k 2k 个,我们发现k的数值与C[i]在二进制下末尾的0的个数相等,而 2 k 2^k 2k等于末尾的第一个1的权位大小
  • C[i]数组 由k个C[j]数组贡献得到
  • C[i]=C[i- 2 k 1 2^{k1} 2k1]+C[(i- 2 k 1 2^{k1} 2k1)- 2 k 2 2^{k2} 2k2]+……+C[(i- 2 k 1 2^{k1} 2k1)- 2 k 2 2^{k2} 2k2)-…… 2 k n 2^{kn} 2kn](n为2、3条的k,ki表示从后往前数第i个0的位置ki)
  • s u m i sum_i sumi(1到i的前缀和)也等于k 个C[]相加:C[i]+C[i- 2 k 1 2^{k1} 2k1]+C[(i- 2 k 1 2^{k1} 2k1)- 2 k 2 2^{k2} 2k2]+……+C[(i- 2 k 1 2^{k1} 2k1)- 2 k 2 2^{k2} 2k2)-…… 2 k n 2^{kn} 2kn](ki表示此时从后往前数的0个数)
既然我们发现k在树状数组中有着非同寻常的作用:那我们怎么快速推算出一个数的 2 k 2^k 2k是多少?

引入算法的关键:lowbit=i&(-i)
前置芝士:负数的补码是原码取反加一 ,而整数的补码与原码相同
i&(-i)变成原来第一个1的权位大小= 2 k 2^k 2k(不太清楚的话可以手捏几个二进制数)

算法实现

Part one;我们如何利用A[]建树状数组C[]

由图得:A[i]必然对C[i]有贡献,而C[i]对C[i+ 2 k 2^k 2k]有贡献,

void add(int x,int k)//x为当前下标,y为数值
{
    while(x<=n)
    {
        tree[x]+=k;
        x+=lowbit(x);
    }
    return;
}
Part two:如何前缀和或区间和查询?

查找前缀和的方法,我们可以从前面的结论得出

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

而区间和怎么求?
a n s i , j ans_{i,j} ansi,j= s u m j sum_j sumj- s u m i − 1 sum_{i-1} sumi1 学过前缀和的同学基本都会的

算法进阶

树状数组主要的操作或用法:

单点修改+区间查询

这不是上面的板子题???
luogu板子

#include <iostream>
#include <stdio.h>
using namespace std;
int n,m,tree[2000010],ans,a,b,c;
int lowbit(int x){return x&(-x);}
void add(int x,int k)
{
    while(x<=n)
    {
        tree[x]+=k;
        x+=lowbit(x);
    }
    return;
}
int sum(int x)
{
        ans=0;
        while(x!=0)
        {
            ans+=tree[x];
            x-=lowbit(x);
        }
        return ans;
}
int main()
{
        cin>>n>>m;
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&a);
            add(i,a);
        }
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d%d",&a,&b,&c);
            if(a==1)
                add(b,c);
            if(a==2)
                cout<<sum(c)-sum(b-1)<<endl;
        }
}
区间修改+单点查询

这里要利用差分的知识
举个栗子:n=5:1 3 5 3 2
当[ a 3 a_3 a3, a 4 a_4 a4]+3时怎么维护呢?1、一个一个点地维护,但是时间复杂度太大了 。 2、利用前缀和的性质在树状数组上操作,从询问入手:单点查询,而差分数组的前缀和等于的第i个点的大小
差分数组b[]=1,2,2,-2,-1
s u m i sum_i sumi= a i a_i ai
如何修改呢?当我们发现 b i b_i bi+x时, a j a_{j} aj+x(j>=i)
[ a i a_i ai, a j a_j aj]+x等价于 b i + x b_i+x bi+x b j + 1 b_{j+1} bj+1-x
板子题

#include <iostream>
#include <cstdio>
using namespace std;
long long tree[500005];
int n, m;
long long lowbit(long long x)
{
	return  x & -x ;
}
void add(int x, long long num) {
    while (x <= n) {
        tree[x] += num;
        x += lowbit(x);
    }
}
long long query(int x) 
{
    long long ans = 0;
    while (x) {
        ans += tree[x];
        x -= lowbit(x);
    }
    return ans;
}
int main()
{
    scanf("%d%d", &n, &m);
    long long last = 0, now;
    for (int i = 1; i <= n; i++) {
        scanf("%lld", &now);
        add(i, now - last);
        last = now;
    }
    int flg;
    while (m--) {
        scanf("%d", &flg);
        if (flg == 1) {
            int x, y;
            long long k;
            scanf("%d%d%lld", &x, &y, &k);
            add(x, k);
            add(y + 1, -k);
        } else if (flg == 2) {
            int x;
            scanf("%d", &x);
            printf("%lld\n", query(x));
        }
    }
    return 0;
}
区间修改+区间查询

因为在洛谷找不到对应的树状数组的例题所以只好,拿线段树的板子题
对于区间查询,可以像前面例题那样,由两个前缀和相减得到
转换思路:求某个前缀: s u m i sum_i sumi=a[1]+……+a[i],如果我们代换成差分数组的话:b[1]+(b[1]+b[2])+……+(b[1]+……b[i])=b[1]* i + b[2]*(i-2) + …… + b[i]*1
差分数组的每一项贡献次数i-j+1(i为前缀和一共的项数,j为当前项的项数)
用式子表示就是
s u m n sum_{n} sumn = ∑ i = 1 n \sum\limits_{i=1}^n i=1n ∑ j = 1 i \sum\limits_{j=1}^i j=1i b j b_j bj = ∑ i = 1 n \sum\limits_{i=1}^n i=1n * (n-i+1) * b j b_j bj
有两种方法:
1、我们维护一个数组C=(i-1) * b j b_j bj
ans= ∑ i = 1 n \sum\limits_{i=1}^n i=1n n * b i b_i bi - ∑ i = 1 n \sum\limits_{i=1}^n i=1n c j c_j cj
2、我们维护一个数组C=i * b j b_j bj
ans= ∑ i = 1 n \sum\limits_{i=1}^n i=1n (n+1) * b i b_i bi - ∑ i = 1 n \sum\limits_{i=1}^n i=1n c j c_j cj
下面展示第二种的代码:

#include<iostream>
#include<cstdio>
#define N 100010
using namespace std;
int b[N],c[N],a[N],n,m,opk,x,y,k;
int lowbit(int x){ return x&(-x);}
void update(int x,int y){
	for (int i=x;i<=n;i+=lowbit(i)){
		b[i]+=y;
		c[i]+=y*x;//不要把x写成i(这里x才是式子中的i)
	}
}
int query(int x){
	int ans=0;
	for (int i=x;i>=1;i-=lowbit(i)){
		ans+=(x+1)*b[i]-c[i];//不要把x写成i(这里x才是式子中的i)
	}
	return ans;
}
int main()
{
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		update(i,a[i]-a[i-1]);
	}
//	for (int i=1;i<=n;i++)printf("%d:%d %d\n",i,b[i],c[i]);
	for (int i=1;i<=m;i++){
		scanf("%d",&opk);
		if (opk==1){
			scanf("%d%d%d",&x,&y,&k);
			update(x,k);
			update(y+1,-k);
		}else{
			scanf("%d%d",&x,&y);
			printf("%d\n",query(y)-query(x-1));
		}
	}
	return 0;
}
区间最值

这里只讨论单点修改+区间查询最值(这里讲最大值)
建树:既然求区间最值,那树状数组的建立操作从求和变成取最值

查询:等等,这时我们发现前缀和差分在这里失去了原有的作用,那从树状数组控制的范围入手(前面的结论1、2条得知:C[i]表示:A[i- 2 k 2^k 2k+1],A[i- 2 k 2^k 2k+2],……,A[i]中最值)所以我们要判边界!!

而修改的话不能直接加lowbit来处理,因为直接取max的话,可能原来位置上的值为最大值,把当前值变小,无法判断,我们把C[i- 2 k 1 2^{k1} 2k1]+C[(i- 2 k 1 2^{k1} 2k1)- 2 k 2 2^{k2} 2k2]+……+C[(i- 2 k 1 2^{k1} 2k1)- 2 k 2 2^{k2} 2k2)-…… 2 k n 2^{kn} 2kn]取max

int query(int x,int y){
	int ans=0;
	while (x<=y){
		ans=max(ans,a[y]);
		for (--y;y-x>=lowbit(x);y-=lowbit(y)){
			ans=max(ans,b[y]);
		}
	}
}
int update(int x,int y){
	a[x]+=y;
	while (x<=y){
		b[x]=a[x];
		for (int i=1;i<lowbit(x);i<<=1){
			b[x]=max(b[x],b[x-i]);
		}
		x+=lowbit(x);
	}
}
逆序对

题目:逆序对
定义当 a i a_i ai> a j a_j aj 且 i<j 就称( a i a_i ai, a j a_j aj)为一组逆序对
普及芝士:离散化
利用类似下标计数的方式,出现 a i a_i ai时,把jsq[ a i a_i ai]+=1,用树状数组c来维护数组jsq
前缀和 j s q i jsq_i jsqi表示小于等于i的数的个数,我们反向思路:大于 a i a_i ai的数等于一共的数减去小于等于i的数
我们按输入顺序来维护就可以了

#include<iostream>
#include<cstdio>
#include<algorithm>
#define N 500010
#define int long long
using namespace std;
int n,a[N],ansh,b[N],c[N],num;
int lowbit(int x){return x&(-x);}
void update(int x,int y){
	for (int i=x;i<=num;i+=lowbit(i)){
		c[i]+=y;
	}
}
int query(int x){
	int ans=0;
	for (int i=x;i>=1;i-=lowbit(i)){
		ans+=c[i];
	}
	return ans;
}
signed main()
{
	scanf("%lld",&n);
	for (int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		b[i]=a[i];
	}
	sort(b+1,b+1+n);
	num=unique(b+1,b+1+n)-b-1;
	for (int i=1;i<=n;i++){
		a[i]=lower_bound(b+1,b+1+num,a[i])-b;
		ansh+=i-1-query(a[i]);
//		printf("%lld %lld\n",a[i],ansh);
		update(a[i],1);
	}
	printf("%lld",ansh);
	return 0;
}
二维:

由于找不到单点修改+区间修改区间修改+单点查询的板子
推荐大佬博客一同食用!!!
所以我直接代入一个区间修改+区间查询的板子题来口胡
我们把树状数组从一维扩展成二维,类似于一行数,变成一个矩阵数(变成二维差分)
前置芝士:二维前缀和
sum[ i ] [ j ]= sum[ i-1 ][ j ]+sum[ i ][ j-1 ]-sum[ i-1 ][ j-1 ]+a[ i ][ j ]
tu
结合图来了解:sum[i][j]=(黄+蓝)+(橙+蓝)- 蓝 + 绿。

  • 差分数组的前缀和等于这个数的值的性质可以轻易单点查询
    我们如何建二维树状数组呢
    结合板子题上帝造题的七分钟来讲
    题目

题单:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值