c++ 树状数组三种方法

树状数组

初识树状数组

树状数组用的是树结构的思想(也就是树型逻辑结构),而不是真正的“树形结构”,(换句话说,从某种意义上,树状数组跟树其实没有特别大的关系)

  • 树状数组支持的操作:
    区间和、区间异或和、区间乘积和RMQ(显然,支持的操作都具有交换律
    2.单点修改和区间修改。 树状数组的优点:树状数组的维护是(logn)的,并且查询、修改是O(1)。 树状数组被称为树的原因:维护以(logn),而这个log底是2;就是二进制表示法,也就是二叉树上数据之间的特殊逻辑关系。

还是不懂的人可以结合下面的图理解一下啦不要关闭换下一个QWQ

在这里插入图片描述

1: 1

2: 10

3: 11

4: 100

5: 101

6: 110

7: 111

8:1000
而对于每一个x的最低含一位,即上文中的2,可以借助一个lowbit函数实现——而这个的实现方式是很玄学的
我也看不懂
简单的说就是用这个函数作为线往根(上)走

幼体代码解释

主要最原始的树状数组是单点修改,区间查询

lowbit函数

	inline int lowbit(int x) {
		//return (x^(x-1))&x;
		return x&(-x);
	}
	// inline这个是让函数不进入在用的地方直接代替函数里的内容
	// 还是不懂就把它当做很加快运行速度就行了QWQ
  • 两种实现方法:一种是通过数学+二进制的方式,另一种则是通过计算机编码特性
  • 记住第二种就对了 应该也没有人用第一种吧QWQ

树状数组的建立维护和查询

建立维护:此处拿求区间和为样例

    inline void search(int x, int k) { // 在x点加上k
        for(; x <= n; x += lowbit(i))
        	tree[x] += k;
    }

查询:

	inline int query(int x) {// 求1~x的和
		int ans = 0;
	    for(; x; x -= lowbit(x))
        ans += tree[x];
        return ans;
    }

求和:

	inline int add(int x, int y) { // 1~y 减 1~(x-1) 可得 x~y
		return query(y) - query(x-1);
	}

幼体模板

  • lougu P3374
  • 将某一个数加上 x
  • 求出某区间每一个数的和
  • 输入
  • 1 x k 含义:将第 x 个数加上 k
  • 2 x y 含义:输出区间 [x,y] 内每个数的和
	#include<iostream>
	using namespace std;
	const int MAX = 5*1e5 + 10;
	int tree[MAX];
	int n , m;
	
	inline int lowbit(int k) {
	    return k & -k;
	}
	inline void search(int x, int k) {// 修改
	    for(; x <= n; x += lowbit(x))
	        tree[x] += k;
	}
	inline int query(int x) {// 求1~x的和
	    int ans = 0;
	    for(; x; x-=lowbit(x))
	        ans += tree[x];
	    return ans;
	}
	inline int add(int x, int y) {
		return query(y) - query(x-1);
	}
	int main() {
	    scanf("%d%d", &n, &m);
	    int a, x, y;
	    for(int i = 1; i <= n; ++i) {
	        scanf("%d", &a);
	        search(i,a); // 将值放入树状数组
	    }
	    for(int i = 1; i <= m; ++i) {
	        scanf("%d%d%d", &a, &x, &y);
	        if(a == 1) search(x, y);
	        if(a == 2) cout << add(x, y) << endl;
	    }
	    return 0;
	}

成熟体代码解释

进阶树状数组区间修改,单点查询
这个树状数组主要在于存的不是输入的值,而是输入的值的与上一个值的差分

	    scanf("%d%d", &n, &m);
	    ll a, b, x, y;
	    b = 0;
	    for(int i = 1; i <= n; ++i) {
	        scanf("%lld", &a);
	        b = a - b; // 差分
	        // D[i] = C[i] - C[i-1];
	        search(i, b); // 将差分放进数组
	        b = a;
	    }

主要差别就在这个
用差分的话, 在进行区间加的时候,就不用在区间内每个数都加一遍,而是在这个区间的头和尾加上值,这样的话就可以将整个区间都与其他数相差这个值了;
文字说明好像也很难懂qwq,那我们还是结合文字看图吧
在这里插入图片描述
很简单的吧
也因为C[i] = D[1] + D[2] + … + D[i]
所以查询一个数直接用这个就可以啦

	inline ll query(ll x) { // 用差分查点D[1~x]得C[i]
	    ll ans = 0;
	    for(; x > 0; x-=lowbit(x))
	        ans += tree[x];
	    return ans;
	}

成熟体模板

	#include<iostream>
	using namespace std;
	const int MAX = 5*1e5 + 10;
	typedef long long ll;
	ll tree[MAX];
	int n , m;
	// tree[i] = D[i]
	inline ll lowbit(ll k) {
	    return k & -k;
	}
	inline void search(ll x, ll k) {// 修改
	    for(; x <= n; x += lowbit(x))
	        tree[x] += k;
	}
	inline ll query(ll x) { // 用差分查点
	    ll ans = 0;
	    for(; x > 0; x-=lowbit(x))
	        ans += tree[x];
	    return ans;
	}
	int main() {
	    scanf("%d%d", &n, &m);
	    ll a, b, x, y;
	    b = 0;
	    for(int i = 1; i <= n; ++i) { // 这里变化一下
	        scanf("%lld", &a);
	        b = a - b;
	        search(i, b);
	        b = a;
	    }
	    for(int i = 1; i <= m; ++i) {
	        scanf("%lld", &a);
	        if(a == 1) {
	            scanf("%lld%lld%lld", &x, &y, &b);
	            search(x, b); // 这里要改两个数
	            search(y+1, -b); // 分别是头和尾
	        }
	        if(a == 2) {
	            scanf("%lld", &b);
	            cout << query(b) << endl; // 不用相减了
	        }
	    }
	    return 0;
	}

完全体代码解释

既然是完全版,那肯定是既能区间修改又能区间查询啦
首先有这一个公式证明
只要这个公式懂了 其他就会了
由成体版我们可以知道D[1]+D[2]+……D[i]=C[i]
那么,对于1到r的区间和,即为:

*- C[1]+C[2]+……+C[r-1]+C[r] //用上方公式推导得出
=D[1]+(D[1]+D[2])+……+(D[1]+……+D[r]) //根据加法交换律与结合律:
=(D[1](r ))+(D[2](r-1))+……(D[r]1) //那么:
=r
(D[1]+D[2]+……+D[r])-(D[1]0+D[2]1+……+D[r](r-1))

看到这里,是不是已经很清晰了呢?
对于C的树状数组(差分)tree,建立一个新的树状数组tree1使得:
tree1[i]=tree[i]*(i-1)
( tree[i] = D[i] tree1[i] = D[i] * (i-1) )
之后,x到y的区间和即为:
( y*query(tree,y)-(x-1)*query(tree,x-1))-(query(tree1,y)-query(tree1,x-1) )

Tips:
因为求区间和满足区间加法,所以Sum(L,R)=Sum(1,R)-Sum(1,L-1),所以有上述公式。

如果还是看不懂公式 那我们就看看图了

i123456789
C[i]1246846710
D[i]11222-4213
D[i]*(i-1)01468-2012724
修改区间2~6 加 5 后得
i123456789
C[i]179111396710
D[i]16222-4-313
D[i]*(i-1)06468-20-18724
查询区间 2~8
sum = ( 8*(D[1]+…D[8]) - 1*(D[1]) ) - ( D[1](1-1)+…+D[8](8-1) - D[1]*(1-1) )
      = ( D[1]*7 + D[2]*7 + D[3]*6+...D[8]*1 )
      = ( C[2] + C[3] + ... C[8] )
      = 62
      用( y*query(tree,y)-(x-1)*query(tree,x-1))-(query(tree1,y)-query(tree1,x-1) )也可以得

sum = ( 8*(D[1]+…D[8]) - 1*(D[1]) ) - ( D[1](1-1)+…+D[8](8-1) - D[1](1-1) )
= (8
C[8] - C[1]) - ( D[1](1-1)+…+D[8](8-1) - D[1]*(1-1) )
= (56 - 1) - (6 + 4 + 6 + 8 - 20 -18 + 7 + 24)
= 55 - (-7) = 62

完全体模板

#include<iostream>
using namespace std;
const int MAX = 5*1e5 + 10;
typedef long long ll;

ll tree[MAX];
ll tree1[MAX];
int n , m;
// tree[i] = D[i],tree1[i] = D[i]*(i-1);
inline ll lowbit(ll k) {
    return k & -k;
}
inline void search(ll *f, ll x, ll k) {// 修改
    for(; x <= n; x += lowbit(x))
        f[x] += k;
}
inline ll query(ll *f, ll x) {
    ll ans = 0;
    for(; x > 0; x-=lowbit(x))
        ans += f[x];
    return ans;
}
inline ll add(int x, int y) {
    ll ans1 = y*query(tree, y) - (x-1)*query(tree,x-1);
    ll ans2 = query(tree1, y) - query(tree1, x-1);
    return ans1 - ans2;
}
int main() {
    scanf("%d%d", &n, &m);
    ll a, b, x, y;
    b = 0;
    for(int i = 1; i <= n; ++i) {
        scanf("%lld", &a);
        b = a - b;
        search(tree , i, b);
        search(tree1, i, (i-1)*b);
        b = a;
    }
    for(int i = 1; i <= m; ++i) {
        scanf("%lld", &a);
        if(a == 1) {
            scanf("%lld%lld%lld", &x, &y, &b);
            search(tree, x, b);// 区间修改
            search(tree, y+1, -b);
            search(tree1, x, (x-1)*b);
            search(tree1, y+1, y*(-b));
        }
        if(a == 2) {
            scanf("%lld%lld", &x, &y);
            cout << add(x, y) << endl; // 区间和
        }
    }
    return 0;
}

萌新初来写博客,如有错误, 求大佬手下留情QWQ

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

光—暗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值