一个问题
(摘自BIT-1)
时间限制:1秒 内存限制:128M
题目描述
给定数组 a 1 , a 2 . . . a n a_1,a_2...a_n a1,a2...an,进行 m m m次操作,操作有两种:
1 i x
:将 a i a_i ai加上 x x x;
2 l r
:求 a l + . . . + a r a_l + ... + a_r al+...+ar
输入描述
第一行:输入两个数 n,m,表示给定数组的长度和操作数
第二行:输入n个数,表示长度为n的给定数组
接下来q行:每行输入3个数字,表示如题的某一种操作
输出描述
对于每个2操作,输出对应结果
输入样例3 2 1 2 3 1 2 1 2 1 3
输出样例
7
数据描述
1 ≤ n , m ≤ 1 0 6 1 \leq n,m \leq 10^6 1≤n,m≤106
∣ a i ∣ ≤ 1 0 6 |a_i| \leq 10^6 ∣ai∣≤106
1 ≤ l , r , i ≤ n 1 \leq l,r,i \leq n 1≤l,r,i≤n
∣ x ∣ ≤ 1 0 6 |x| \leq 10^6 ∣x∣≤106
这道题乍一看是一个很普通的求前缀和的问题,但是,前缀和的修改操作却是
O
(
n
)
O(n)
O(n)级别,显然超时…
难道就没有其他的办法了吗?
树状数组
树状数组是一个类似树的数组,可以很快地完成数组的修改操作,以及前缀和的查找
时空复杂度
树状数组的查找前缀和的时间复杂度与修改单点的时间复杂度都为
O
(
log
2
n
)
O(\log_{2}{n})
O(log2n)
空间复杂度很明显,为
O
(
n
)
O(n)
O(n)
树状数组的结构
刚开始我们有一个输入进去的
a
1
−
a
n
a_1-a_n
a1−an,如下图:
同学们就问了,这哪里来构建树状数组,这不是树吗?
我们可以这样看:
而树状数组(
c
n
c_n
cn)在这棵树中是这样的:
c
i
c_i
ci分别记录了他自己的叶子结点中所有
a
i
a_i
ai的值之和
l o w b i t ( ) lowbit() lowbit()
l
o
w
b
i
t
(
)
lowbit()
lowbit() 是求一个数在二进制中最低位‘1’所代表的数值
举个例子
l
o
w
b
i
t
(
6
)
=
2
lowbit(6)=2
lowbit(6)=2,因为
6
6
6的二进制中的表示为
110
110
110,最低位的’1’代表的是
2
2
2
l
o
w
b
i
t
(
8
)
=
8
lowbit(8)=8
lowbit(8)=8,因为
6
6
6的二进制中的表示为
1000
1000
1000,最低位的’1’代表的是
8
8
8
代码实现:
# define lowbit(x) ((x) & (-(x)))
// define版本
int lowbit(int x) { return x & (-x); }
// 函数版本
树状数组单点修改
观察下面这些数的下标:
c
[
1
]
=
a
[
1
]
c[1]=a[1]
c[1]=a[1]
c
[
2
]
=
a
[
1
]
+
a
[
2
]
c[2]=a[1]+a[2]
c[2]=a[1]+a[2]
c
[
3
]
=
a
[
3
]
c[3]=a[3]
c[3]=a[3]
c
[
4
]
=
a
[
1
]
+
a
[
2
]
+
a
[
3
]
+
a
[
4
]
c[4]=a[1]+a[2]+a[3]+a[4]
c[4]=a[1]+a[2]+a[3]+a[4]
c
[
5
]
=
a
[
5
]
c[5]=a[5]
c[5]=a[5]
c
[
6
]
=
a
[
5
]
+
a
[
6
]
c[6]=a[5]+a[6]
c[6]=a[5]+a[6]
c
[
7
]
=
a
[
7
]
c[7]=a[7]
c[7]=a[7]
c
[
8
]
=
a
[
1
]
+
a
[
2
]
+
a
[
3
]
+
a
[
4
]
+
a
[
5
]
+
a
[
6
]
+
a
[
7
]
+
a
[
8
]
c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
假如我们修改
a
[
3
]
a[3]
a[3]这个点
我们就得去修改
c
[
3
]
,
c
[
4
]
,
c
[
8
]
c[3],c[4],c[8]
c[3],c[4],c[8]的值
而下标
3
4
8
3\ 4\ 8
3 4 8的关系如下:
3
(
0011
)
→
l
o
w
b
i
t
(
3
)
4
(
0100
)
→
l
o
w
b
i
t
(
4
)
8
(
1000
)
3(0011) \xrightarrow{ lowbit(3)} 4(0100)\xrightarrow{lowbit(4)} 8(1000)
3(0011)lowbit(3)4(0100)lowbit(4)8(1000)
我们会发现一个有趣的问题:
如果修改 a [ x ] a[x] a[x],就得修改 a [ x ] , a [ x + l o w b i t ( x ) ] , . . . , a [ i ≤ n ] a[x],a[x+lowbit(x)],...,a[i \leq n] a[x],a[x+lowbit(x)],...,a[i≤n]
是不是所有的单点修改都是这样的?
让我们再来举几个例子:
修改
a
[
7
]
a[7]
a[7],就得修改
c
[
7
]
,
c
[
8
]
c[7],c[8]
c[7],c[8]
而
7
(
111
)
→
l
o
w
b
i
t
(
7
)
8
(
1000
)
7(111)\xrightarrow{lowbit(7)}8(1000)
7(111)lowbit(7)8(1000)
修改
a
[
6
]
a[6]
a[6],就得修改
a
[
6
]
,
a
[
8
]
a[6],a[8]
a[6],a[8]
而
6
(
110
)
→
l
o
w
b
i
t
(
6
)
8
(
1000
)
6(110)\xrightarrow{lowbit(6)}8(1000)
6(110)lowbit(6)8(1000)
所以修改 a [ x ] a[x] a[x],就得修改 a [ x ] , a [ x + l o w b i t ( x ) ] , . . . , a [ i ≤ n ] a[x],a[x+lowbit(x)],...,a[i \leq n] a[x],a[x+lowbit(x)],...,a[i≤n]
既然全部符合这个规律,那么就可以根据这一点来进行单点修改操作了
单点修改代码实现
# define lowbit(x) ((x) & (-(x)))
void update(int x, int y) { // x为要修改的下标,y为要增加的值
for(int i = x; i <= n; i += lowbit(i))
c[i] += y;
}
树状数组区间查询
还是观察下面这些数的下标:
c
[
1
]
=
a
[
1
]
c[1]=a[1]
c[1]=a[1]
c
[
2
]
=
a
[
1
]
+
a
[
2
]
c[2]=a[1]+a[2]
c[2]=a[1]+a[2]
c
[
3
]
=
a
[
3
]
c[3]=a[3]
c[3]=a[3]
c
[
4
]
=
a
[
1
]
+
a
[
2
]
+
a
[
3
]
+
a
[
4
]
c[4]=a[1]+a[2]+a[3]+a[4]
c[4]=a[1]+a[2]+a[3]+a[4]
c
[
5
]
=
a
[
5
]
c[5]=a[5]
c[5]=a[5]
c
[
6
]
=
a
[
5
]
+
a
[
6
]
c[6]=a[5]+a[6]
c[6]=a[5]+a[6]
c
[
7
]
=
a
[
7
]
c[7]=a[7]
c[7]=a[7]
c
[
8
]
=
a
[
1
]
+
a
[
2
]
+
a
[
3
]
+
a
[
4
]
+
a
[
5
]
+
a
[
6
]
+
a
[
7
]
+
a
[
8
]
c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
如果我们要查询
∑
i
=
1
n
=
7
a
i
\sum_{i= 1}^{n = 7}a_i
∑i=1n=7ai
只需要求出
c
[
7
]
,
c
[
6
]
,
c
[
4
]
c[7],c[6],c[4]
c[7],c[6],c[4]的和就可以了,而他们的下标变化正好与修改相反:
7
(
111
)
→
l
o
w
b
i
t
(
7
)
6
(
110
)
→
l
o
w
b
i
i
t
(
6
)
4
(
100
)
7(111)\xrightarrow{lowbit(7)}6(110)\xrightarrow{lowbiit(6)}4(100)
7(111)lowbit(7)6(110)lowbiit(6)4(100)
区间查询代码实现
# define lowbit(x) ((x) & (-(x)))
int getsum(int x){
int ans = 0;
for(int i = x; i; i -= lowbit(x))
ans += c[i];
return ans;
}
问题的解决
让我们返回上面的问题,树状数组正好能够解决此问题
(数据很大,记得用scanf printf~,数组记得开longlong~)
C
o
d
e
Code
Code
# include <cstdio>
# include <cstring>
# include <cmath>
# include <algorithm>
# include <vector>
# include <iostream>
# include <string>
# include <set>
# include <unordered_set>
# include <map>
# include <unordered_map>
# include <stack>
# include <queue>
# include <deque>
# define pf push_front
# define pb push_back
# define ppf pop_front()
# define ppb pop_back()
# define mp make_pair
# define p_a first
# define p_b second
# define PI acos(-1, 0)
# define LF putchar('\n')
# define SP putchar(' ')
# define repi(a, b) for(int i = (a); i <= (b); i++)
# define repi_(a, b) for(int i = (a); i >= (b); i--)
# define repj(a, b) for(int j = (a); j <= (b); j++)
# define repj_(a, b) for(int j = (a); j >= (b); j--)
# define repk(a, b) for(int k = (a); k <= (b); k++)
# define repk_(a, b) for(int k = (a); k >= (b); k--)
# define WW(x) while((x)--)
# define paix(a, n) sort((a) + 1, (a) + 1 + (n));
# define BUG(z) cout << endl << (z) << endl;
# define lowbit(z) ((z) & (-z))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef unsigned int ui;
typedef pair<int, int> pii;
const int N1 = 1e6 + 5, N2 = 1e3 + 5, M1 = 1e9 + 7, M2 = 998244353, INF1 = 0x7fffffff, INF2 = 0x3f3f3f3f;
const ll INF = 9223372036854775807;
template <typename T>
void read(T& x) { x = 0; char ch = getchar(); ll f = 1; while (!isdigit(ch)) { if (ch == '-') f *= -1; ch = getchar(); } while (isdigit(ch)) { x = x * 10 + ch - 48; ch = getchar(); } x *= f; }
template <typename T, typename... Args>
void read(T& first, Args&... args) { read(first); read(args...); }
template <typename T>
void write(T arg) { T x = arg; if (x < 0) { putchar('-'); x = -x; } if (x > 9) write(x / 10); putchar(x % 10 + '0'); }
template <typename T, typename... Ts>
void write(T arg, Ts... args) { write(arg); if (sizeof...(args) != 0) { putchar(' '); write(args...); } }
void CLOSE() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); }
int MAXn(int a, int b) { return b > a ? b : a; }
int MINn(int a, int b) { return b < a ? b : a; }
// ————————————————————分割线~~~
int n, m;
ll a[N1], c[N1];
void update(int x, ll y) {
for(int i = x; i <= n; i += lowbit(i))
c[i] += y;
}
ll getsum(int x) {
ll ans = 0;
for(int i = x; i; i -= lowbit(i))
ans += c[i];
return ans;
}
signed main() {
/*
freopen("", "r", stdin);
freopen("", "w", stdout);
*/
scanf("%d%d", &n, &m);
repi(1, n) {
scanf("%lld", &a[i]);
update(i, a[i]);
} WW(m) {
int ch, x, y;
scanf("%d%d%d", &ch, &x, &y);
if (ch == 1) update(x, ll(y));
else printf("%lld\n", getsum(y) - getsum(x - 1));
}
/*
fclose(stdin);
fclose(stdout);
*/
return 0;
}
总结
树状数组是一种非常好用的解决问题的方式,也有诸多的变形,本篇博客中并未提及,希望大家能够灵活地使用树状数组,解决更多的问题。
感谢您的观看
希望这篇博客能够帮助到您!