更多算法详解见:gitee
定义
树状数组是利用数的二进制特征进行检索的一种树状的结构。它用于维护前缀和的数据结构,支持单点修改、区间查询;区间修改、区间查询等一系列操作。
树状数组维护的元素要满足结合律和可差分的性质。
树状数组
给定一个数组
{
a
1
,
a
2
…
,
a
8
}
\{a_1,a_2\dots ,a_8 \}
{a1,a2…,a8},我们如何快速的求其前缀和呢?
一般我们求前缀和就是累加,时间复杂度是
O
(
n
)
O(n)
O(n),一种容易想到的分治的优化策略是,对数组中的元素两两求和并且存到新的数组中,一直这样计算下去直到新数组中只有一个元素,如下:
这样就优化成线段树了,求前缀和、修改元素值的复杂度就都是
O
(
log
n
)
O(\log n)
O(logn)了,但是对于求前缀和有很多元素是多余的,如:我们要求
a
1
a_1
a1到
a
4
a_4
a4的前缀和,只需要用到
a
4
a_4
a4的父节点,并不需要
a
4
a_4
a4其本身。我们将类似的节点都删掉,就得到树状数组:
图中黑色节点都是被删除的节点,这样空间复杂度就优化为
O
(
n
)
O(n)
O(n)了,我们可以直接将节点值存到一个数组中。但是问题来了,我们该怎么在数组中正确快速的求前缀和呢?这就要用到大名鼎鼎的
l
o
w
b
i
t
lowbit
lowbit函数了
l o w b i t lowbit lowbit函数
我们将每个节点下标对应的二进制形式写出来,就能看出一些规律:
- 每个节点覆盖的长度是其二进制表示下的最低位 1 1 1及其后面的 0 0 0构成的数值
- 每个节点的父节点的下标就是在其二进制的最低位 1 1 1加上 1 1 1。
那么实现树状数组的就归结到一个关键问题:如何快速找到一个数二进制表示下最低位
1
1
1及其后面的
0
0
0构成的数值。这也就是
l
o
w
b
i
t
lowbit
lowbit函数的功能。
我们举个例子:求
l
o
w
b
i
t
(
10
)
lowbit(10)
lowbit(10)
我们先将
10
10
10的二进制位写出来
(
1010
)
2
(1010)_2
(1010)2,在对其按位取反得到
(
0101
)
2
(0101)_2
(0101)2,再加上
1
1
1,就得到
(
0110
)
2
(0110)_2
(0110)2,我们对比两个二进制数,发现除了最低位
1
1
1及其后面的
0
0
0,两个数字其他位上的数完全不同,我将两个数进行按位与(&)运算,就得到
l
o
w
b
i
t
lowbit
lowbit值了。
我们再思考,按位取反再加 1 1 1,这不就是负数补码的运算过程吗,所以我们直接将该数取负再按位与即可得到 l o w b i t lowbit lowbit的值,下面是代码:
inline int lowbit(int x) {
return x & -x;
}
所以节点 x x x覆盖的长度就是其 l o w b i t ( x ) lowbit(x) lowbit(x)的值,其父节点下标就是 x + l o w b i t ( x ) x+lowbit(x) x+lowbit(x)
实现
单点修改,区间查询
【模板】树状数组 1
最简单的树状数组可以在
O
(
log
n
)
O(\log n)
O(logn)的复杂度实现这两种操作
单点修改
我们修改某一位置的值,就将覆盖其的父节点的值都进行修改,将节点 x x x加上 d d d的实现如下:
inline void update(int x, int d) {
for (; x <= n; x += lowbit(x))
f[x] += d;
}
区间查询
我们要求区间 [ l , r ] [l,r] [l,r]的和,其实就是 s u m ( r ) − s u m ( l − 1 ) sum(r)-sum(l-1) sum(r)−sum(l−1), s u m ( x ) sum(x) sum(x)代表下标从 1 1 1到 x x x的前缀和,查询某个点的前缀和就是树状数组擅长的,就是从这个节点开始,向坐上找到上一个节点,并加上其节点的值,可以发现向左上找上一个节点,只需要将下标减去当前下标的 l o w b i t lowbit lowbit值,下面是实现代码:
inline int ask(int x) {
int sum = 0;
for (; x >= 1; x -= lowbit(x))
sum += f[x];
return sum;
}
//求区间[l,r]的和
//ask(r)-ask(l-1);
区间修改,单点查询
区间修改
我们只需一个简单而巧妙的操作,就能利用树状数组高效的实现区间修改,这个操作就是差分数组,我们用树状数组维护原数组的差分数组,这样我当我们要修改某个区间的值时,只需要修改两个端点即可。
void update(int x,int d){
for(int i=x;i<=n;i+=lowbit(i))
f[i]+=d;
}
void change(int x,int y,int k){
update(x,k);
update(y+1,-k);
}
单点查询
众所周知,差分的逆运算是求前缀和,树状数组计算的就是前缀和,用树状数组维护差分数组,那么树状数组计算得到就是单点的元素值,即 a s k ( x ) = a [ x ] ask(x)=a[x] ask(x)=a[x],所以单点查询和上面的查询实现相同:
int ask(int x){
int sum=0;
for(int i=x;i>=1;i-=lowbit(i))
sum+=f[i];
return sum;
}
区间修改,区间查询
区间查询
我们要利用树状数组求区间和,首先要求前缀和
s
u
m
(
x
)
sum(x)
sum(x),这里我们定义差分数组
d
d
d,它和原数组的关系是
a
[
k
]
=
d
[
1
]
+
d
[
2
]
+
⋯
+
d
[
k
]
a[k]=d[1]+d[2]+\cdots+d[k]
a[k]=d[1]+d[2]+⋯+d[k],
d
[
k
]
=
a
[
k
]
−
a
[
k
−
1
]
d[k]=a[k]-a[k-1]
d[k]=a[k]−a[k−1],下面推导前缀和与差分数组的关系:
a
1
+
a
2
+
⋯
+
a
k
=
d
1
+
(
d
1
+
d
2
)
+
⋯
+
(
d
1
+
d
2
+
⋯
+
d
k
)
=
k
d
1
+
(
k
−
1
)
d
2
+
⋯
+
(
k
−
(
k
−
1
)
)
d
k
=
k
(
d
1
+
d
2
+
⋯
+
d
l
)
−
(
d
2
+
2
d
3
+
⋯
+
(
k
−
1
)
d
k
)
=
k
∑
i
=
1
k
d
i
−
∑
i
=
1
k
(
i
−
1
)
d
i
\begin{align*} &a_1+a_2+\cdots + a_k\\ ~\\ =&d_1+(d_1+d_2)+\cdots +(d_1+d_2+\cdots +d_k)\\ ~\\ =&kd_1+(k-1)d_2+\cdots +(k-(k-1))d_k\\ ~\\ =&k(d_1+d_2+\cdots +d_l)-(d_2+2d_3+\cdots +(k-1)d_k)\\ ~\\ =&k\sum_{i=1}^{k} d_i-\sum_{i=1}^{k}(i-1)d_i \end{align*}
= = = =a1+a2+⋯+akd1+(d1+d2)+⋯+(d1+d2+⋯+dk)kd1+(k−1)d2+⋯+(k−(k−1))dkk(d1+d2+⋯+dl)−(d2+2d3+⋯+(k−1)dk)ki=1∑kdi−i=1∑k(i−1)di
公式最后一行是求两个前缀和,可以用两个树状数组分别来维护,这样就可计算出前缀和
s
u
m
(
x
)
sum(x)
sum(x),区间和也就很好计算了,下面是实现:
int ask1(int x) { int sum = 0; for (; x > 0; x -= lowbit(x))sum += f1[x]; return sum; }
int ask2(int x) { int sum = 0; for (; x > 0; x -= lowbit(x))sum += f2[x]; return sum; }
int ask(int l, int r) {
return r * ask1(r) - ask2(r) - (l - 1) * ask1(l - 1) + ask2(l - 1);
}
区间修改
区间修改时,两个数组要同时修改,实现方式和上面相同
void update1(int x, int d) { for (; x <= n; x += lowbit(x))f1[x] += d; }
void update2(int x, int d) { for (; x <= n; x += lowbit(x))f2[x] += d; }
void change(int l, int r, int d) {
update1(l, d), update1(r + 1, -d);
update2(l, (l - 1) * d), update2(r + 1, -r * d);
}
树状数组扩展
二维树状数组
二维树状数组,也被称作树状数组套树状数组,用来维护二维数组上的单点修改和前缀信息问题。其实就是一维树状数组上的每个节点变成了一个一维树状数组:
基本的二维树状数组可以实现单点修改,子矩阵查询
单点修改
修改一个矩阵的值后,我们要将其父节点都修改,例如我们要修改上图的 f [ 2 ] [ 2 ] f[2][2] f[2][2]元素,那么我们同时也要修改 f [ 2 ] [ 5 ] 、 f [ 5 ] [ 2 ] 、 f [ 5 ] [ 5 ] f[2][5]、f[5][2]、f[5][5] f[2][5]、f[5][2]、f[5][5],这些节点,实现如下:
void update(int x,int y,int d){
for(int i=x;i<=n;i+=lowbit(i))
for(int j=y;j<=m;j+=lowbit(j))
f[i][j]+=d;
}
时间复杂度为 O ( log n log m ) O(\log n\log m) O(lognlogm)
子矩阵查询
现在我们可以求
∑
i
=
1
n
∑
j
=
1
m
a
[
i
]
[
j
]
\sum_{i=1}^{n}\sum_{j=1}^{m}a[i][j]
∑i=1n∑j=1ma[i][j]的值,要我们计算下图中黑色矩阵的值
很明显,计算黑色矩阵的面积有下面的公式:
∑
i
=
x
2
x
1
∑
j
=
y
2
y
1
a
[
i
]
[
j
]
=
∑
i
=
1
x
1
∑
j
=
1
y
1
a
[
i
]
[
j
]
−
∑
i
=
1
x
2
−
1
∑
j
=
1
y
1
a
[
i
]
[
j
]
−
∑
i
=
1
x
1
∑
j
=
1
y
2
−
1
a
[
i
]
[
j
]
+
∑
i
=
1
x
2
∑
j
=
1
y
2
a
[
i
]
[
j
]
\sum_{i=x_2}^{x_1}\sum_{j=y_2}^{y_1}a[i][j]=\sum_{i=1}^{x_1}\sum_{j=1}^{y_1}a[i][j]-\sum_{i=1}^{x_2-1}\sum_{j=1}^{y_1}a[i][j]-\sum_{i=1}^{x_1}\sum_{j=1}^{y_2-1}a[i][j]+\sum_{i=1}^{x_2}\sum_{j=1}^{y_2}a[i][j]
i=x2∑x1j=y2∑y1a[i][j]=i=1∑x1j=1∑y1a[i][j]−i=1∑x2−1j=1∑y1a[i][j]−i=1∑x1j=1∑y2−1a[i][j]+i=1∑x2j=1∑y2a[i][j]
有了公式代码也就很容易写了:
int ask(int x,int y){
int sum=0;
for(int i=x;i>0;i-=lowbit(i))
for(int j=y;j>0;j-=lowbit(j))
sum+=f[i][j];
return sum;
}
int ask(int x1,int y1,int x2,int y2){
return ask(x1,y1)-ask(x1,y2-1)-ask(x2-1,y1)+ask(x2,y2);
}
时间复杂度为 O ( log n log m ) O(\log n\log m) O(lognlogm)
二维树状数组进阶
上帝造题的七分钟
该题要我们实现二维树状数组的子矩阵修改和子矩阵查询
子矩阵修改
和一维类似的,这里也要用到差分的思想,使用二维差分,我们定义一个二维差分数组
d
[
i
]
[
j
]
d[i][j]
d[i][j],它与原矩阵元素
a
[
i
]
[
j
]
a[i][j]
a[i][j]的关系如下:
d
[
i
]
[
j
]
=
a
[
i
]
[
j
]
−
a
[
i
]
[
j
−
1
]
−
a
[
i
−
1
]
[
j
]
+
a
[
i
−
1
]
[
j
−
1
]
a
[
x
]
[
y
]
=
∑
i
=
1
x
∑
j
=
1
y
d
[
i
]
[
j
]
d[i][j]=a[i][j]-a[i][j-1]-a[i-1][j]+a[i-1][j-1]\\ ~\\ a[x][y]=\sum_{i=1}^{x}\sum_{j=1}^{y}d[i][j]
d[i][j]=a[i][j]−a[i][j−1]−a[i−1][j]+a[i−1][j−1] a[x][y]=i=1∑xj=1∑yd[i][j]
现在,假设我们要在顶点为
(
a
,
b
)
、
(
c
,
d
)
(a,b)、(c,d)
(a,b)、(c,d)的矩阵内的每个元素加上
k
k
k,如下图
因为我们维护的是差分数组,所以我分别修改四个顶点即可完成对区间的修改,实现如下:
void update(int x,int y,int d){
for(int i=x;i<=n;i+=lowbit(i))
for(int j=y;j<=m;j+=lowbit(j))
f[i][j]+=d;
}
void update(int x1,int y1,int x2,int y2,int k){
update(x1,y1,k),update(x1,y2+1,-k),update(x2+1,y1,-k),update(x2+1,y2+1,k);
}
子矩阵查询
我们已经直到求一个子矩阵的和有下面的式子:
∑
i
=
x
2
x
1
∑
j
=
y
2
y
1
a
[
i
]
[
j
]
=
∑
i
=
1
x
1
∑
j
=
1
y
1
a
[
i
]
[
j
]
−
∑
i
=
1
x
2
−
1
∑
j
=
1
y
1
a
[
i
]
[
j
]
−
∑
i
=
1
x
1
∑
j
=
1
y
2
−
1
a
[
i
]
[
j
]
+
∑
i
=
1
x
2
∑
j
=
1
y
2
a
[
i
]
[
j
]
\sum_{i=x_2}^{x_1}\sum_{j=y_2}^{y_1}a[i][j]=\sum_{i=1}^{x_1}\sum_{j=1}^{y_1}a[i][j]-\sum_{i=1}^{x_2-1}\sum_{j=1}^{y_1}a[i][j]-\sum_{i=1}^{x_1}\sum_{j=1}^{y_2-1}a[i][j]+\sum_{i=1}^{x_2}\sum_{j=1}^{y_2}a[i][j]
i=x2∑x1j=y2∑y1a[i][j]=i=1∑x1j=1∑y1a[i][j]−i=1∑x2−1j=1∑y1a[i][j]−i=1∑x1j=1∑y2−1a[i][j]+i=1∑x2j=1∑y2a[i][j]
问题就转换为计算
∑
i
=
1
n
∑
j
=
1
m
a
[
i
]
[
j
]
\sum_{i=1}^{n}\sum_{j=1}^{m}a[i][j]
∑i=1n∑j=1ma[i][j],与推一维数组区间查询类似,我们利用原数组的与差分数组之间的关系进行变换,下面是推导过程:
∑
i
=
1
n
∑
j
=
1
m
a
[
i
]
[
j
]
=
∑
i
=
1
n
∑
j
=
1
m
∑
k
=
1
i
∑
l
=
1
j
d
[
k
]
[
l
]
=
∑
i
=
1
n
∑
j
=
1
m
d
[
i
]
[
j
]
×
(
n
−
i
+
1
)
×
(
m
−
j
+
1
)
=
(
n
+
1
)
(
m
+
1
)
∑
i
=
1
n
∑
j
=
1
m
d
[
i
]
[
j
]
−
(
m
+
1
)
∑
i
=
1
n
∑
j
=
1
m
d
[
i
]
[
j
]
×
i
(
n
+
1
)
∑
i
=
1
n
∑
j
=
1
m
d
[
i
]
[
j
]
×
j
+
∑
i
=
1
n
∑
j
=
1
m
d
[
i
]
[
j
]
×
i
×
j
\begin{align*} &\sum_{i=1}^{n}\sum_{j=1}^{m}a[i][j]\\ = &\sum_{i=1}^{n}\sum_{j=1}^{m}\sum_{k=1}^{i}\sum_{l=1}^{j}d[k][l]\\ =&\sum_{i=1}^{n}\sum_{j=1}^{m}d[i][j]\times (n-i+1)\times (m-j+1)\\ =&(n+1)(m+1)\sum_{i=1}^{n}\sum_{j=1}^{m}d[i][j]-(m+1)\sum_{i=1}^{n}\sum_{j=1}^{m}d[i][j]\times i\\ &(n+1)\sum_{i=1}^{n}\sum_{j=1}^{m}d[i][j]\times j+\sum_{i=1}^{n}\sum_{j=1}^{m}d[i][j]\times i\times j \end{align*}
===i=1∑nj=1∑ma[i][j]i=1∑nj=1∑mk=1∑il=1∑jd[k][l]i=1∑nj=1∑md[i][j]×(n−i+1)×(m−j+1)(n+1)(m+1)i=1∑nj=1∑md[i][j]−(m+1)i=1∑nj=1∑md[i][j]×i(n+1)i=1∑nj=1∑md[i][j]×j+i=1∑nj=1∑md[i][j]×i×j
我们可以用四个二维树状数组来分别维护上面四个二维前缀和,下面是实现:
int ask(int x,int y){
int sum=0;
for(int i=x;i>0;i-=lowbit(i))
for(int j=y;j>0;j-=lowbit(j))
sum+=(x+1)*(y+1)*f[0][i][j]-(y+1)*f[1][i][j]-(x+1)*f[2][i][j]+f[3][i][j];
return sum;
}
int ask(int x1,int y1,int x2,int y2){
return ask(x2,y2)-ask(x1-1,y2)-ask(x2,y1-1)+ask(x1-1,y1-1);
}
下面是完整代码:
char op;
int f[4][MAX][MAX], n, m,a,b,c,d,delta;
inline int lowbit(int x){
return x&-x;
}
//维护四个树状数组
void update(int x,int y,int d){
for(int i=x;i<=n;i+=lowbit(i))
for(int j=y;j<=m;j+=lowbit(j))
f[0][i][j]+=d,f[1][i][j]+=d*x,f[2][i][j]+=y*d,f[3][i][j]+=x*y*d;
}
//维护子矩阵和
void update(int x1,int y1,int x2,int y2,int k){
update(x1,y1,k),update(x1,y2+1,-k),update(x2+1,y1,-k),update(x2+1,y2+1,k);
}
//前缀和查询
int ask(int x,int y){
int sum=0;
for(int i=x;i>0;i-=lowbit(i))
for(int j=y;j>0;j-=lowbit(j))
sum+=(x+1)*(y+1)*f[0][i][j]-(y+1)*f[1][i][j]-(x+1)*f[2][i][j]+f[3][i][j];
return sum;
}
//查询子矩阵和
int ask(int x1,int y1,int x2,int y2){
return ask(x2,y2)-ask(x1-1,y2)-ask(x2,y1-1)+ask(x1-1,y1-1);
}
void solve() {
cin>>op>>n>>m;
while(cin>>op){
cin>>a>>b>>c>>d;
if(op=='L'){
cin>>delta;
update(a,b,c,d,delta);
}
else cout<<ask(a,b,c,d)<<'\n';
}
}
偏序问题
关于偏序问题,以一维、二维、三维偏序问题为例,介绍如下:
- 一维偏序(逆序对)。给定数列 a a a,求满足 i < j i<j i<j且 a i > a j a_i>a_j ai>aj,的二元组 ( i , j ) (i,j) (i,j)的个数
- 二维偏序。给定 n n n个点的坐标,求满足 x i < x j 、 y i < y j x_i<x_j、y_i<y_j xi<xj、yi<yj的二元组 ( i , j ) (i,j) (i,j)的个数
- 三维偏序。给定 n n n个点的坐标,求满足 x i < x j 、 y i < y j 、 z i < z j x_i<x_j、y_i<y_j、z_i<z_j xi<xj、yi<yj、zi<zj的二元组 ( i , j ) (i,j) (i,j)的个数
逆序对
利用离散化+树状数组求逆序对是一种简单且效率极高的方法。具体思路读者可以思考一下,这里直接给出代码:
ll f[MAX], n, a[MAX], newa[MAX];
inline ll lowbit(ll x) {
return x & -x;
}
void update(int x, ll d) {
for (int i = x; i <= n; i += lowbit(i))
f[i] += d;
}
ll ask(int x) {
ll sum = 0;
for (int i = x; i >= 1; i -= lowbit(i))
sum += f[i];
return sum;
}
void solve() {
cin >> n;
memset(f, 0, sizeof f);
ll ans = 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
newa[i] = a[i];
}
sort(newa, newa + n + 1);
//数据离散化
for (int i = 1; i <= n; ++i)
a[i] = lower_bound(newa + 1, newa + 1 + n, a[i]) - newa ;
for (int i = 1; i <= n; ++i) {
update(a[i], 1);
ans += (i - ask(a[i]));
}
cout << ans << '\n';
}
求逆序对还有一种解法是用归并排序,有兴趣的可以看洛谷的题解。
处理多维偏序问题的一般解法是
C
D
Q
CDQ
CDQ分治,这里就不展开说明了(因为我不会QAQ)。
区间最值
求区间最值一般是用线段树或
S
T
ST
ST表。这里给出树状数组的解法,加深理解。
例题:I Hate It
以前树状数组节点值是
[
x
−
l
o
w
b
i
t
(
x
)
+
1
,
x
]
[x-lowbit(x)+1,x]
[x−lowbit(x)+1,x]区间的元素和,这里树状数组节点值就改为存放
[
x
−
l
o
w
b
i
t
(
x
)
+
1
,
x
]
[x-lowbit(x)+1,x]
[x−lowbit(x)+1,x]区间的最大值。
在修改区间最值时,要更新树状数组上所有被其影响的节点,即所有与其直接相连的节点,其父节点和子节点。
在查询时分两种情况,如果当前节点覆盖的范围,超过了我们的查询范围,就用原数组对应下标的值去更新答案,并且向前递推;如果没有超过我们的查询范围,就直接用当前节点的值去更新答案。
下面是具体代码:
int f[MAX],a[MAX], n, m;
inline int lowbit(int x) {return x & -x;}
void update(int x, int d) {
while (x <= n) {
f[x] = d;
//通过子节点更新自身,看看有没有比自己大的子节点
for (int i = 1; i < lowbit(x); i <<= 1)
f[x] = max(f[x], f[x - i]);
//到下一个父节点
x += lowbit(x);
}
}
int ask(int l,int r) {
int res = 0;
while (l <= r) {
if (lowbit(r) > r - l + 1) {
res = max(res, a[r]);
--r;
}
else {
res = max(res, f[r]);
r -= lowbit(r);
}
}
return res;
}
void solve() {
while (cin >> n >> m) {
for (int i = 1; i <= n; ++i) {
cin >> a[i];
update(i, a[i]);
}
for (int i = 0; i < m; ++i) {
char op;
int x, y;
cin >> op >> x >> y;
if (op == 'Q')cout << ask(x, y) << '\n';
else a[x] = y, update(x, y);
}
}
}
离线操作
离线处理,即先读取所有查询,然后统一处理,计算结束后一起输出。
下面是离线操作的典型例题,留给读者思考了:
No Pain No Game