数据结构的本质就是对朴素算法的优化
遇到一道题目,首先先要有朴素的解题思想,将要维护的量与操作记录下来后,再考虑对应的数据结构
文章目录
P a r t 1 Part1 Part1 树状数组
对于树状数组上的每一个数 x x x,可以发现, x x x 在 2 进制 {2进制} 2进制 下, 令 x x x 最后一个 1 1 1 出现的位置为 第 k k k 位, x x x 表示的区间长度即为 2 k − 1 2^{k-1} 2k−1 即 最后一个 1 1 1 的权值
例如:
14
=
(
01110
)
2
14=(01110)_2
14=(01110)2 其最后一个
1
1
1 出现在第
2
2
2 位,那么
14
14
14 表示区间的长度为
2
2
−
1
=
2
2^{2-1}=2
22−1=2 即
(
10
)
2
(10)_2
(10)2 的大小
例如:
8
=
(
00100
)
2
8=(00100)_2
8=(00100)2 其最后一个
1
1
1 出现在第
3
3
3 位,那么
8
8
8 表示区间的长度为
2
3
−
1
=
4
2^{3-1}=4
23−1=4 即
(
100
)
2
(100)_2
(100)2 的大小
而我们使用 l o w b i t lowbit lowbit 就可以求出该值
int lowbit(int x)
{
return x&(-x);
}
1 ∣ 1| 1∣ 单点修改,区间查询
- 单点修改
以 3 3 3 为例,修改 a [ 3 ] a[3] a[3] 会对区间包含 a [ 3 ] a[3] a[3] 的点 : t [ 3 ] , t [ 4 ] , t [ 8 ] , t [ 16 ] t[3],t[4],t[8],t[16] t[3],t[4],t[8],t[16] 造成影响
不难发现,
3
+
l
o
w
b
i
t
(
3
)
=
4
3+lowbit(3)=4
3+lowbit(3)=4
4
+
l
o
w
b
i
t
(
4
)
=
8
4+lowbit(4)=8
4+lowbit(4)=8
8
+
l
o
w
b
i
t
(
8
)
=
16
8+lowbit(8)=16
8+lowbit(8)=16
//单点修改
void add(int x,int delta)
{
for(int i=x;i<=n;i+=lowbit(i))
tree[i]+=delta;
}
- 区间求和
以
15
15
15 为例,区间
[
1
,
15
]
[1,15]
[1,15] 之和等于
t
r
e
e
[
15
]
+
t
r
e
e
[
14
]
+
t
r
e
e
[
12
]
+
t
r
e
e
[
8
]
tree[15]+tree[14]+tree[12]+tree[8]
tree[15]+tree[14]+tree[12]+tree[8] (区间
[
15
,
15
]
[15,15]
[15,15]
[
13
,
14
]
[13,14]
[13,14]
[
9
,
12
]
[9,12]
[9,12]
[
1
,
8
]
[1,8]
[1,8] 之和)
观察可得,
15
−
l
o
w
b
i
t
(
15
)
=
14
15-lowbit(15)=14
15−lowbit(15)=14
14
−
l
o
w
b
i
t
(
14
)
=
12
14-lowbit(14)=12
14−lowbit(14)=12
12
−
l
o
w
b
i
t
(
12
)
=
8
12-lowbit(12)=8
12−lowbit(12)=8
8
−
l
o
w
b
i
t
(
8
)
=
0
8-lowbit(8)=0
8−lowbit(8)=0
就此求出区间 [ 1 , x ] [1,x] [1,x] 的和
// 区间查询 ( 区间 1 到 x 的和)
int query(int x)
{
int res=0;
for(int i=x;i>0;i-=lowbit(i))
res+=tree[i];
return res;
}
- 在每次输入时, a d d ( i , a [ i ] ) add\space (\space i,a[i]\space ) add ( i,a[i] )
- 对于一个询问 [ L , R ] [L,R] [L,R] ,利用前缀和的思想,[1,R] 的和 减去 [1,L-1] 的和 即可
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i], add(i,a[i]);
while(m--)
{
cin>>opt;
if(opt==1)
{
cin>>x>>delta;
add(x,delta);
}
if(opt==2)
{
cin>>l>>r;
cout<<query(r)-query(l-1)<<endl;
}
}
2 ∣ 2| 2∣ 区间修改,单点查询
区间修改,单点查询 其实可以转换成 单点修改,区间查询
我们利用差分的思想,维护差分数组,区间 [ 1 , x ] [1,x] [1,x] 的数之和即为第 x x x 的数的值
对于区间修改 [ L , R ] [L,R] [L,R] 我们在 L L L 处加上 + d e l t a +delta +delta,在 R + 1 R+1 R+1 处加上 − d e l t a -delta −delta
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i], add(i,a[i]-a[i-1]);
while(m--)
{
cin>>opt;
if(opt==1)
{
cin>>l>>r>>delta;
add(l,delta);
add(r+1,-delta);
}
if(opt==2)
{
cin>>x;
cout<<query(x)<<endl;
}
}
3 ∣ 3| 3∣ 区间修改,区间查询
例题:树状数组 区间修改,区间查询
t
r
e
e
1
tree1
tree1 维护
d
[
i
]
d[i]
d[i]
t
r
e
e
2
tree2
tree2 维护
d
[
i
]
∗
i
d[i]*i
d[i]∗i
对于区间 a d d add add 操作,
t r e e 1 tree1 tree1 在 L L L 处 + d e l t a +delta +delta , 在 R + 1 R+1 R+1 处 − d e l t a -delta −delta (只影响差分数组的 L L L 与 R + 1 R+1 R+1 处)
t
r
e
e
2
tree2
tree2 在
L
L
L 处
+
d
e
l
t
a
∗
L
+delta*L
+delta∗L , 在
R
+
1
R+1
R+1 处
−
d
e
l
t
a
∗
(
R
+
1
)
-delta*(R+1)
−delta∗(R+1)
(由于
d
[
i
]
d[i]
d[i] 只有
L
,
R
+
1
L,R+1
L,R+1 处改变,因此
d
[
i
]
∗
i
d[i]*i
d[i]∗i 亦然)
对于
q
u
e
r
y
query
query 操作,
前缀和思想,
q
u
e
r
y
(
R
)
−
q
u
e
r
y
(
L
−
1
)
query(R)-query(L-1)
query(R)−query(L−1)
int lowbit(int x)
{
return x&(-x);
}
void add1(int x,int delta)
{
for(int i=x;i<=n;i+=lowbit(i))
tree1[i]+=delta;
}
void add2(int x,int delta)
{
for(int i=x;i<=n;i+=lowbit(i))
tree2[i]+=delta*x;
}
int query(int x)
{
int res=0;
for(int i=x;i>0;i-=lowbit(i))
res+=tree1[i]*(x+1)-tree2[i];
return res;
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i], add1(i,a[i]-a[i-1]), add2(i,a[i]-a[i-1]);
while(m--)
{
cin>>opt;
if(opt==1)
{
cin>>l>>r>>delta;
add1(l,delta); add1(r+1,-delta);
add2(l,delta); add2(r+1,-delta);
}
if(opt==2)
{
cin>>l>>r;
cout<<query(r)-query(l-1)<<endl;
}
}
return 0;
}
4 ∣ 4| 4∣ 权值树状数组
例如该排列,在其之前的个数为
67
67
67 ,其顺序为
68
68
68
则对于第
i
i
i 位,
令
f
(
i
)
f(i)
f(i) 表示在
i
i
i 之前的未出现过的数字个数
答案
=
=
=
(
n
−
i
)
!
∗
f
(
i
)
(n-i)!*f(i)
(n−i)!∗f(i) 之和 (
i
i
i 从
1
1
1 到
n
n
n )
可以用 权值树状数组 维护 f ( i ) f(i) f(i)
即化为 区间查询,单点修改
void init()
{
calc[1]=1;
for(int i=2;i<=n;i++)
calc[i]=calc[i-1]*i,calc[i]%=mod;
}
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i],add(i,1);
init();
int ans=0;
for(int i=1;i<=n;i++)
{
int cnt=query(a[i]-1);
ans+=cnt*calc[n-i], ans%=mod;
add(a[i],-1);
}
cout<<ans+1;
}
5 ∣ 5| 5∣ 树状数组求第 k k k 大
由于树状数组中 t r e e [ x ] tree[x] tree[x] 管辖的区域是 l o w b i t ( x ) lowbit(x) lowbit(x) , 那么从 0 0 0 开始 , 每次加 2 i 2^i 2i , 累计的和一定表示从 1 1 1 开始的连续区间 , 根据这一特性 , 我们可以通过倍增的方式查询第一个前缀和大于等于 k k k 的某数
若我们用权值树状数组 , 那么一个数的前缀和就表示有多少数比它小
思路就是不断倍增逼近最后一个小于 k k k 的数
int select(int k)
{
int pos = 0, cur = 0;
for(int i=20;i>=0;i--)
{
pos += (1<<i);
if(pos>n or cur+tree[pos]>=k) pos -= (1<<i);
else cur += tree[pos];
}
return pos+1;
}
6 ∣ 6| 6∣ 例题
6.1 6.1 6.1 树状数组求逆序对
求逆序对及求 i < j i<j i<j 且 a i < a j a_i<a_j ai<aj 的个数
用树状数组维护以 a [ i ] a[i] a[i] 为下标的权值数组
i i i 从小到大枚举,则在 i i i 之前已经枚举的大于 a [ i ] a[i] a[i] 的个数即为 i i i 的逆序对的个数,在枚举 i i i 之前有 i − 1 i-1 i−1 个数,因此个数 = i − 1 − q u e r y ( a [ i ] ) =i-1-query(a[i]) =i−1−query(a[i])
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e5+5;
int n,a[MAXN],A[MAXN],tree[MAXN];
int lowbit(int x)
{
return x&(-x);
}
void add(int x,int delta)
{
for(int i=x;i<=n;i+=lowbit(i))
tree[i]+=delta;
}
int query(int x)
{
int res=0;
for(int i=x;i>0;i-=lowbit(i))
res+=tree[i];
return res;
}
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i], A[i]=a[i];
sort(A+1,A+n+1);
int cnt=unique(A+1,A+n+1)-A-1;
for(int i=1;i<=n;i++)
a[i]=lower_bound(A+1,A+cnt+1,a[i])-A;
int ans=0;
for(int i=1;i<=n;i++)
{
ans+=i-1-query(a[i]);
add(a[i],1);
}
cout<<ans;
return 0;
}
6.2 6.2 6.2 树状数组去重求和
对于重复的数字,我们只关注其最后一个数字对答案的贡献
如 :
1
2
3
2
1
1 \space 2\space 3\space 2\space 1
1 2 3 2 1
询问区间
[
1
,
5
]
[1,5]
[1,5] 不重复数字的和,等价于
0
0
3
2
1
0 \space 0\space 3\space 2\space 1
0 0 3 2 1,结果为
6
6
6
如果在该询问之后又有询问区间 [ 1 , 2 ] [1,2] [1,2] 的和,而数列已变为 0 0 3 2 1 0 \space 0\space 3\space 2\space 1 0 0 3 2 1,还需重复修改
因此我们可以建立一根扫描线,从左向右进行扫描,当某个询问的右端点与扫描线重合时,解决这次询问
至于保留最后一个数字而删除之前重复数字的操作,对于 a [ i ] a[i] a[i],我们可以记录上一个 a [ i ] a[i] a[i] 出现的位置 l s t [ a [ i ] ] lst[a[i]] lst[a[i]],将 l s t [ a [ i ] ] lst[a[i]] lst[a[i]] 处减去 a [ i ] a[i] a[i]
所以,此题要求单点修改,区间求和,使用树状数组
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e4+5;
const int MAXM=1e5+5;
int n,m,a[MAXN],ans[MAXM],tree[MAXN];
unordered_map <int,int> lst;
struct Question {
int l,r,id;
}q[MAXM];
bool cmp(Question x,Question y)
{
if(x.r==y.r) return x.l<y.l;
return x.r<y.r;
}
int lowbit(int x) {return x&(-x);}
void add(int x,int delta)
{
for(int i=x;i<=n;i+=lowbit(i))
tree[i]+=delta;
}
int query(int x)
{
int res=0;
for(int i=x;i>0;i-=lowbit(i))
res+=tree[i];
return res;
}
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
cin>>m;
for(int i=1;i<=m;i++)
cin>>q[i].l>>q[i].r, q[i].id=i;
sort(q+1,q+m+1,cmp);
int i=1;
for(int t=1;t<=m;t++)
{
for(;i<=q[t].r;i++)
{
if(lst[a[i]]!=0) add(lst[a[i]],-a[i]);
lst[a[i]]=i;
add(i,+a[i]);
}
ans[q[t].id]=query(q[t].r)-query(q[t].l-1);
}
for(int i=1;i<=m;i++) cout<<ans[i]<<endl;
return 0;
}
6.3 6.3 6.3 权值树状数组与偏序问题
如果存在一个运动员 j j j ,使得 x j > x i x_j>x_i xj>xi、 y j > y i y_j>y_i yj>yi、 y j > y i y_j>y_i yj>yi ,则 i i i 不可用
我们先将 x x x 从大到小排序,需保证对于 j > i j>i j>i ,有 x j > x i x_j >x_i xj>xi 这样只要存在 j j j 的 y j > y i , z j > z i y_j>y_i,z_j>z_i yj>yi,zj>zi,也就是说只要之前出现过 y j > y i , z j > z i y_j>y_i,z_j>z_i yj>yi,zj>zi , i i i 就不可用
那么在所有满足 y j > y i y_j>y_i yj>yi 的 j j j 中 ,只要 m a x max max{ z j z_j zj} > z i >z_i >zi , i i i 就不可用
用权值树状数组维护一个以 y y y 的值为下标,以 z m a x z_{max} zmax 为值的数组
树状数组可维护前缀最大值,我们现在要求的是 y j > y i y_j>y_i yj>yi 的, 因此我们需要通过处理将 y y y 按倒序存储
例:
- 注意的细节
- 1 ∣ 1| 1∣ 记 y i ′ = n − y i + 1 y_i'=n-y_i+1 yi′=n−yi+1 ,即可实现将 y y y 倒序处理,加 1 1 1 是为了防止 y ′ = 0 y'=0 y′=0 导致树状数组卡死
- 2 ∣ 2| 2∣ 对于每个 i i i ,需要询问 < y i <y_i <yi 的 最大 z z z ,因此应该先访问 y i y_i yi 比较小的,再访问 y i y_i yi 较大的
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
struct Node{
int x,y,z;
}a[MAXN];
int n,tree[MAXN];
bool cmp(Node t1,Node t2)
{
if(t1.x==t2.x) return t1.y<t2.y;// 细节2
return t1.x>t2.x;
}
bool bad[MAXN];
int lowbit(int x)
{
return x&(-x);
}
void add(int x,int val)
{
for(int i=x;i<=n;i+=lowbit(i))
tree[i]=max(tree[i],val);
}
int query(int x)
{
int res=0;
for(int i=x;i>0;i-=lowbit(i))
res=max(tree[i],res);
return res;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i].x>>a[i].y>>a[i].z;
sort(a+1,a+n+1,cmp);
int cnt=n;
for(int i=1;i<=n;i++)
{
if(query(n-a[i].y)>a[i].z)//细节1
cnt--;
add(n-a[i].y+1,a[i].z);//细节1
}
cout<<cnt;
return 0;
}
6.4 6.4 6.4 权值树状数组优化dp
d
p
dp
dp 转移方程:
s
u
m
sum
sum 表示前缀和
f[0]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<i;j++)
{
if(sum[i]<0 || sum[j]<0) continue;
if(sum[i]-sum[j]>=0)
{
f[i]+=f[j], f[i]%=mod;
}
}
看到这题就觉得和 usaco 的 Bookshelf 和 TJOI 的书架 很像,只不过那两题是求 f [ j ] f[j] f[j] 的最大值,详见 线段树优化dp,此题变为了求 f [ j ] f[j] f[j] 的和,可以考虑使用树状数组
- 具体实现:
i i i 从小到大开始枚举,则在 i i i 之前已枚举的数 并且 满足 s u m ≤ s u m [ i ] sum \leq sum[i] sum≤sum[i] 的 f f f 之和 ,即图中小于等于 s u m [ i ] sum[i] sum[i] 的 f f f 总和,即为 f [ i ] f[i] f[i] 的值
以前缀和为下标,用权值树状数组维护
此题由于可能为负数,我们需要离散化处理
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+5;
const int mod=1e9+9;
int n,a[MAXN],x[MAXN],sum[MAXN],pos[MAXN],f[MAXN],tree[MAXN];
int lowbit(int x)
{
return x&(-x);
}
void add(int x,int delta)
{
for(int i=x;i<=n;i+=lowbit(i))
tree[i]+=delta, tree[i]%=mod;
}
int query(int x)
{
int res=0;
for(int i=x;i>0;i-=lowbit(i))
res+=tree[i], tree[i]%=mod;
return res;
}
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i], sum[i]=sum[i-1]+a[i], x[i]=sum[i], x[i]=sum[i]%=mod;
sort(x+1,x+n+1);
int cnt=unique(x+1,x+n+1)-x-1;
for(int i=1;i<=n;i++)
pos[i]=lower_bound(x+1,x+cnt+1,sum[i])-x;
pos[0]=lower_bound(x+1,x+cnt+1,0)-x;
add(pos[0],1);
for(int i=1;i<=n;i++)
{
f[i]=query(pos[i]), f[i]%=mod;
add(pos[i],f[i]);
}
cout<<f[n];
return 0;
}
6.5 6.5 6.5 树状数组求顺序数对
直接从132型数对不好求,而我们可以很容易将 123 型和 132 型一起求出
记所有
j
<
i
j<i
j<i 且
a
[
j
]
<
a
[
i
]
a[j]<a[i]
a[j]<a[i] 的
j
j
j 的个数为
L
[
i
]
L[i]
L[i]
记所有
i
<
j
i<j
i<j 且
a
[
i
]
<
a
[
j
]
a[i]<a[j]
a[i]<a[j] 的
j
j
j 的个数为
R
[
i
]
R[i]
R[i]
对于求 132+123 型数对 ,枚举 i i i 作为左端点, R [ i ] ∗ ( R [ i ] − 1 ) / 2 R[i]*(R[i]-1)/2 R[i]∗(R[i]−1)/2 即为以 i i i 为左端点的该数对个数
对于 123 型个数,枚举 i i i 作为中间点, L [ i ] ∗ R [ i ] L[i]*R[i] L[i]∗R[i] 即为以 i i i 为中间点的该数对个数
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e4+5;
int n,a[MAXN],A[MAXN],tree1[MAXN],tree2[MAXN],L[MAXN],R[MAXN],tot1,tot2;
int lowbit(int x)
{
return x&(-x);
}
void add(int *tree,int x,int delta)
{
for(int i=x;i<=n;i+=lowbit(i))
tree[i]+=delta;
}
int query(int *tree,int x)
{
int res=0;
for(int i=x;i>0;i-=lowbit(i))
res+=tree[i];
return res;
}
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i], A[i]=a[i];
sort(A+1,A+1+n);
int cnt=unique(A+1,A+1+n)-A-1;
for(int i=1;i<=n;i++)
a[i]=lower_bound(A+1,A+cnt+1,a[i])-A;
for(int i=1;i<=n;i++)
{
L[i]=query(tree1,a[i]-1);
add(tree1,a[i],+1);
}
for(int i=n;i>=1;i--)
{
R[i]=query(tree2,n-a[i]);
add(tree2,n-a[i]+1,+1);;
}
for(int i=1;i<=n;i++)
{
tot1+=R[i]*(R[i]-1)/2;
tot2+=L[i]*R[i];
}
cout<<tot1-tot2;
return 0;
}