树状数组

树状数组

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) \]

  • 时间复杂度

\[n\ log_n \]

广义“前缀和”

  1. 前缀和不仅包括 \(\pm\) 还包括 \(\times\) . \(\bigoplus\) .\(\min\) . \(max\). \(\&\) 都可以进行区间查询

  2. 可以得到 单点\(\max\) \(\min\) 得不到区间最大值或最小值,这就需要线段树进行维护

思路

  1. 建完树以后,直接 \(\ add\) 单点修改

  2. 修改后的树进行 取点\(\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\),

\[\begin{alignedat}{3} & B_x = A_x+w-A_{x-1}=B_x+w\\ \\ & B_{y+1}=A_{y+1}-(A_y+w)=B_{y+1}-w \end{alignedat} \]

\({\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\) 次。

论证

\[\begin{alignedat}{3} \sum_{i=1}^ja[i]&= \sum_{i=1}^j\sum_{i=1}^ib[i]\\ & =\sum_{i=1}^jb[i]\times(j-i+1)\\ & =j\times\sum_{i=1}^jb[i]-\sum_{i=1}^jb[i]\times(i-1) \end{alignedat} \]

举例

  • \(A:1\ 2\ 3\ 4\ 5\)

  • \(So\ B:1\ 1\ 1\ 1\ 1\)

\(query(4)\)

\[\begin{alignedat}{3} A[4]& =A[1]+A[2]+A[3]+A[4]\\ & =B[1]+B[1]+B[2]+B[1]+B[2]+B[3]+B[1]+B[2]+B[3]+B[4]\\ & =B[1]\times4+B[2]\times3+B[3]\times+B[4]\times1;\\ & = \sum_{i=1}^4B[i]\times(4-i+1)\\ & = \sum_{i=1}^4B[i]\times\ 4\ +\ \sum_{i=1}^4B[i]\times(1-i)\\ & = 4\times\sum_{i=1}^4B[i]\ -\ \sum_{i=1}^4B[i]\times(i-1) \end{alignedat} \]

思路

  • 用两个树状数组分别来维护 \(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

posted @ 2020-10-06 07:40  zxsoul  阅读(147)  评论(6编辑  收藏  举报