树状数组(BIT)简单分析、应用

树状数组(BIT)简单分析、应用

前言

文章仅作参考、学习

本篇文章作为作者日常分享,相比别的大佬 可能质量有点不尽人意,有什么错误请大家指出。

Now

为了解决上次在讲康托展开时没解决的问题——“如何用优化康托展开?”(本来说是用线段树,但线段树过于复杂了,这里再插个锚点🚩,以后再学);今天我们学习树状数组。(上次关于康托展开(仅展开)的问题(8月6日)已解决,链接可见本文最后)


前置知识

既然学树状数组,那么前缀和、差分应该都学过了吧,这里只简单提一下。先来回顾一些位运算,以及说一下lowbit函数以便后面的树状数组进行节点更新。下面给出一些位运算示例、有关前缀和与差分的知识、lowbit函数的功能和实现。

  • “&” 与 两数都为1时,结果才为1
  • “~” 取反 0变1,1变0
  • “^” 异或 两个位相同为0,相异为1

F_T1
F_T2

(其中diff表示差分、prefix表示前缀和、arr表示原数组、x表示加的数)

lowbit函数可以用来取一个二进制数 最低位的1 以此为最高位 往后全为0的数。

比如: 001 1 2 = 3 10 0011_2=3_{10} 00112=310,使用lowbit函数后可得到 000 1 2 = 1 10 0001_2=1_{10} 00012=110

0000110 0 2 = 1 2 10 00001100_2=12_{10} 000011002=1210,使用lowbit函数后可得到 0000010 0 2 = 4 10 00000100_2=4_{10} 000001002=410

lowbit函数实现方式:

  1. return x & (x ^ (x - 1));

  2. return x & -x;//简洁明了,建议使用这个

  3. return x & (~x + 1);//实际上与第二种一样

    自己推一下就好了;顺便提醒一下,计算机中数是按补码的形式储存的

树状数组

知识点(规律)

T1
如图所示,这是树状数组的基本结构,tree分成不同的层(由下往上,从1开始)。

观察可得,第i层各数的lowbit都是 2 i − 1 2^{i-1} 2i1(图示的右边就是各层的lowbit),且对于每个t[j],它都有i个子节点,子节点由下标从小到大依次是t[j-lowbit(i-1)],t[j-lowbit(i-2)],…,t[j-lowbit(i)]。反过来(属规律,从图上推)也成立,既对于t[i],它的父节点是t[j+lowbit(j)]。

同时,t[j-lowbit(j)]是t[j]上一层比t[j]下标小的最近的集合,再继续这样往前推,即可得到从a[1]到a[j]的总和,求区间和也一样,再减去区间前面的总和就行了;区间修改直接在原数组的差分数组做BIT上就行了。

感觉上面讲的有点难懂,一大段,没有举例还是太晦涩了,所以下面是主要的概况与举例;

  • t[i]的父节点是t[i+lowbit(i)]:比如t[7]的父节点是t[7+lowbit(7)]=t[7 + 1]=t[8];t[2]的父节点是t[2+lowbit(2)]=t[2 + 2]=4[4]
  • 区间a[1, r]的大小为t[r]+t[r-lowbit®]+t[t[r-lowbit[r]-lowbit(r-lowbit[r])]+…(直到当前位减lowbit为0):比如区间a[1, 5]大小为t[5]+t[5-lowbit(5) = 4];(4 - lowbit(4) = 0)

C++代码

不多说了,直接上代码。

#include <bits/stdc++.h>

using namespace std;

int n, inl, inr, inx;
vector<int> arr;//原数组
vector<int> bit;//树状树状(二元数组)
vector<int> diff;//差分

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

//单点修改
void add(int x,int z){
	int size = bit.size();
	for(int i = z;i <= size;i += lowbit(i)) bit[i] += x;
}

//区间和计算
int summing(int l, int r){
	int sum = 0;
	for(int i = l - 1;i != 0;i -= lowbit(i)) sum -= bit[i];
	for(int i = r;i != 0;i -= lowbit(i)) sum += bit[i];
	return sum;
}

int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	
	cin >> n;//原数组长度
	arr.resize(n + 1, 0);
	bit.resize(n + 1, 0);
	diff.resize(n + 1, 0);
	
	for(int i = 1;i <= n;i++) cin >> arr[i];//原数组
	for(int i = 1;i <= n;i++) add(arr[i], i);//计算前缀和
	for(auto ele : bit) cout << ele << " ";cout << endl;
	
	cin >> inl >> inr;//区间
	cout << summing(inl, inr) << endl;
	bit.clear();
	bit.resize(n + 1, 0);
	
	//差分
	for(int i = 1;i <=n;i++) diff[i] = arr[i] - arr[i - 1];
	for(auto ele : diff) cout << ele << " ";cout << endl;
	for(int i = 1;i <= n;i++) add(diff[i], i);//用树状数组维护差分数组
	cin >> inl >> inr >> inx;//区间、值
	add(inx, inl);
	add(-inx, inr + 1);
	
	//单点查询
	cin >> inr;
	cout << summing(1, inr) << endl;
	
	return 0;
}

此前遗留的问题(用树状数组解决):

康托展开与逆展开

感兴趣的可以去看一下(本人强烈建议阅读全篇,收归与本人“算法、数据结构与技巧”专栏,保证质量!!!)
用树状数组的位置在“康托展开->C++代码->代码优化”这部分。下面是链接:
康托(Cantor)展开与逆展开理解与运用——YDelete

(本人坚持原创、探寻极限、追求卓越。你的一个赞与关注,是对我莫大的支持!!!)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值