树状数组
树状数组
By : Zxsure
注:树状数组基础概念,三种操作。
板子
lowbit
int lowbit(int x){//二进制规律
return x & -x;
}
树状后传
void add(int i ,int x){
while(i <= n){//单点修改(加值)
c[i] += x;
i += lowbit(i);
}
}
求和
int query(int k){//单点求和
int ans = 0;
while (k > 0){
ans += c[k];
k -= lowbit(k);
}
return ans;
}
一 单点修改,区间查询
复杂度
-
空间复杂度
\[O(n) \] -
时间复杂度
广义“前缀和”
-
前缀和不仅包括 \(\pm\) 还包括 \(\times\) . \(\bigoplus\) .\(\min\) . \(max\). \(\&\) 都可以进行区间查询
-
可以得到 单点 的 \(\max\) \(\min\) 得不到区间最大值或最小值,这就需要线段树进行维护
思路
-
建完树以后,直接 \(\ add\) 单点修改
-
修改后的树进行 取点\(\rightarrow\) 求前缀和 \(\rightarrow\)相减的区间和
Code
/*
Name:P3374 【模板】树状数组 1(单点修改,区间查询)
Author:BZQ
Date: 2020/10/5
*/
#include <iostream>// i ——是树状数组的编号,单点更改就是在编号上进行加值或减值。并不是在单个数值上进行修改(并不全是)
#include <cstdio>//树状数组存的数是部分或一个数的和,嗯 OK
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 2000010;
int c[N],n,m;
int lowbit(int x){//二进制规律
return x & -x;
}
void add(int i ,int x){
while(i <= n){//单点修改(加值)
c[i] += x;
i += lowbit(i);
}
}
int query(int k){//单点求和
int ans = 0;
while (k > 0){
ans += c[k];
k -= lowbit(k);
}
return ans;
}
int main(){
cin >> n >> m;
for (int i = 1;i <= n; i++){
int x;
cin >> x;
add(i,x); //初步构建树状数组
}
for (int i = 1;i <= m; i++){
int a,b,f;
cin >> a >> b >> f;
if(a == 1){
add(b,f);//单点修改
}
if(a == 2)
{
int n1 = query(b-1);
int m1 = query(f);
cout << m1 - n1 << endl;//区间求和 = 单点前缀和(x-1)——(y)之差、
}
}
return 0;
}
二 区间修改 , 单点查询(弱化版)
核心思路
-
区间修改一般都需要利用差分的思想进行维护,当我们做出某一数列的差分数组\({B_i}\)时,不难发现\({a_i\ = \lbrace \ b_1\ +\ b_2\ +b_3\ +\ \cdots\ +b_i\rbrace\ }\),因此可以用树状数组进行维护 。
\[\sum_{j=1}^{i}b[j] \] -
\(\ A\) 数组唯一对应一个 \(\ B\) 数组,\(\ B\) 数组唯一对应一个 \(\ A\) 数组。也就是说他们是同一个数列的两种表现形式——原数列、差分数列。
-
区间修改\(\ A\) 数组 \({x\ —\ y}\) 就等同于单点修改 \(\ B\) 数组 \({B_x\ +=\ w}\) 和 \({B_{y+1}\ -= w}\)
-
单点查询即求出 \(\sum_{i=1}^{x}B[i]\) 的和。
推论:
\({\because B_i=A_i-A_{i-1}}\)
- 考虑将\({A_{x..y}}\)全部加上\(w\),
\({\therefore B_x+=w\ , B_{y+1}-=w\ }\)
\({\therefore B_{x+1...y}}\)不变
Code
/*
Name:P3374 【模板】树状数组 2(区间修改,单点查询)
Author:BZQ
Date: 2020/10/5
*/
#include <iostream>// i ——是树状数组的编号,单点更改就是在编号上进行加值或减值。并不是在单个数值上进行修改(并不全是)
#include <cstdio>//树状数组存的数是部分或一个数的和,嗯 OK
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 2000010;
int c[N],a[N],n,m;
int lowbit(int x){
return x & -x;
}
void add(int i ,int x){
while(i <= n){
c[i] += x;
i += lowbit(i);
}
}
int query(int k){
int ans = 0;
while (k > 0){
ans += c[k];
k -= lowbit(k);
}
return ans;
}
int main(){
cin >> n >> m;
for (int i = 1;i <= n; i++){
cin >> a[i];
add(i,a[i]-a[i-1]);//差分思想,差分数组进行单点修改即可达成原数组的区间修改
}
for (int i = 1;i <= m; i++){
int a,b,f,d;
cin >> a;
if(a == 1){
cin >> b >> f >> d;
add(b, d);//原数组区间修改
add(f + 1, -d);
}
if(a == 2)
{
int x;
cin >> x; // 原数组单点查询。
cout << query(x) << endl;
}
}
return 0;
}
三 区间修改,区间查询(高级进阶版)
吐槽
作者\(\ Zxsure\)花费近两个\(\ hour\)肝出代码,方博客支援\(Blog\)
我们继续......
结论
-
仍然沿用B数组,考虑A数组[1,x]区间和的计算。
-
\(B[1]\) 被累加了 \(x\) 次,\(B[2]\) 被累加了 \(x-1\) 次,...,\(B[x]\) 被累加了 \(1\) 次。
论证
举例
-
\(A:1\ 2\ 3\ 4\ 5\)
-
\(So\ B:1\ 1\ 1\ 1\ 1\)
求 \(query(4)\)
思路
-
用两个树状数组分别来维护 \(b[i]\) 和 \(b[i] * i - 1\)
-
区间更改不变,查询相减即可,
-
我用四个函数,也就是两个树状数组写总是写炸,无奈借鉴了结构体函数
Code
# include<iostream>
# include<cstdio>
# include<cmath>
# include<algorithm>
const int mn = 100005;
int n,m;
struct Binary_tree{
long long tr[mn];
void add(int i,long long x)
{
while(i<=n)
{
tr[i]+=x;
i+=i&-i;
}
}
long long qsum(int i)
{
long long ret=0;
while(i>0)
{
ret+=tr[i];
i-=i&-i;
}
return ret;
}
}A,B;
long long a[mn];
int main()
{
int opt,x,y,z;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
}
for(int i=1;i<=n;i++)
{
A.add(i,a[i]-a[i-1]);
B.add(i,(a[i]-a[i-1])*(i-1));
}
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&opt,&x,&y);
if(opt==1)
{
scanf("%d",&z);
A.add(x,z);
A.add(y+1,-z);
B.add(x,z*(x-1));
B.add(y+1,-z*y);
}
else {
printf("%lld\n",(A.qsum(y)*y-(x-1)*A.qsum(x-1))-B.qsum(y)+B.qsum(x-1));
}
}
return 0;
}
\(The \ End\)
备注
- 区间求和无论是在时间复杂度还是空间复杂度都远超于线段树
- 局限是不可进行随意修改,例如将数改成颜色,不如线段树
- 小数据推荐树状数组,线段树凭能力吧
更新 : 2020/11/3