引子
我们之前学习过前缀和,可以查询静态区间和。
前缀和是预处理出每一个前缀,对于任意一个区间,我们都可以用两个前缀和之差来表示这个区间内元素之和。
如果区间查询的是某种没有可减性的值,例如区间最值、区间 g c d gcd gcd等,这时我们就无法使用前缀和算法来求解了。
如果在查询的过程中加入修改操作,那么每次修改过后,都会有 O ( n ) O(n) O(n)个前缀和的值发生变化,重新维护的代价是 O ( n ) O(n) O(n),总体复杂度就退化为 O ( n 2 ) O(n^2) O(n2)了。
所以,对于没有可减性的问题,以及动态(带修改)问题,我们不能使用前缀和算法求解,分别需要用到 s t st st表 和 树状数组。
倍增
倍增是
s
t
st
st表的前置知识,用一句话概括倍增就是:
1
+
1
=
2
,
2
+
2
=
4
,
.
.
.
(
2
0
+
2
0
=
2
1
,
2
1
+
2
1
=
2
2
,
.
.
.
)
1+1=2, 2+2=4, ... (2^0+2^0=2^1,2^1+2^1= 2^2,...)
1+1=2,2+2=4,...(20+20=21,21+21=22,...)倍增数组就是对于序列的每个位置,分别计算出以这个位置作为左端点,长度为
1
、
2
、
4
、
8
、
.
.
.
(
1、2、4、8、...(
1、2、4、8、...(所有
2
i
<
=
n
)
2i <= n)
2i<=n)的区间运算结果。
每个位置有
O
(
O(
O(log2
n
)
n)
n)个区间,每个区间的结果可以
O
(
1
)
O(1)
O(1) 递推得到
(
1
+
1
=
2
)
(1+1=2)
(1+1=2),所以预处理出倍增数组的复杂度是
O
(
n
O(n
O(nlog2
n
)
n)
n)
[
1
,
1
]
[1, 1]
[1,1]
[
1
,
2
]
=
[
1
,
1
]
+
[
2
,
2
]
[1, 2] = [1, 1] + [2, 2]
[1,2]=[1,1]+[2,2]
[
1
,
4
]
=
[
1
,
2
]
+
[
3
,
4
]
[1, 4] = [1, 2] + [3, 4]
[1,4]=[1,2]+[3,4]
倍增数组有什么用?对于任意一个区间,可以把这个区间拆成最多log2n个倍增数组中计算好的区间。为什么?
二进制分解,每次取剩余长度
x
x
x的二进制分解中最大的一位,分解后的
x
x
x小于原来的
x
x
x的一半。如
7
(
111
)
7 (111)
7(111)2 取长度为
4
4
4的区间后变成
3
(
11
)
3 (11)
3(11)2
利用倍增做“二分”
倍增也可以用来求解有单调性的区间问题。
实现
代码: —————出处
#include<bits/stdc++.h>;
using namespace std;
int a[1005];
int main(){
int t;
cin>>t;
for(int i=1;i<=t;i++){
long long l,p,c,x,cs;
cin>>l>>p>>c;
cout<<"Case #"<<i<<": ";
if(l*c==p){
cout<<0<<endl;
}else{
x=l,cs=0;
while(x*c<p){
x*=c;
a[++cs]=x;
}
int l1=1,r1=cs,x=0;
while(l1<=r1){
x++;
int mid=(l1+r1)/2;
if(a[mid]-a[l1]<a[r1]-a[mid]){
l1=mid+1;
}else{
r1=mid-1;
}
}
cout<<x<<endl;
}
}
return 0;
}
拓展
倍增不止能用在序列上,也可以用在树上,想一想,树上倍增有什么用途?
A:求出节点
x
x
x的高度为
y
y
y的祖先节点,
L
C
A
LCA
LCA倍增法。
#include<bits/stdc++.h>
using namespace std;
struct node{
int w[25],d;//x次方父亲,深度
vector<int> E;//附近结点
}a[500005];
int n,m,s;
void fad(int x,int fa){ //求出 fa and d
a[x].d=a[fa].d+1;
a[x].w[0]=fa;
for(int i=0;i<a[x].E.size();i++){
int v=a[x].E[i];
if(v==fa)continue;
fad(v,x);
}
}
void cff(){ // 次方 fa
for(int i=0;i<=19;i++){
for(int j=1;j<=n;j++){
a[j].w[i+1]=a[a[j].w[i]].w[i];
}
}
}
int LCA(int x,int y){ // 倍增法最近公共祖先
if(a[x].d>a[y].d){
swap(x,y);
}
for(int i=19;i>=0;i--){
if(a[x].d<=a[y].d-(1<<i)){
y=a[y].w[i];
}
}
for(int i=19;i>=0;i--){
if(a[x].w[i]!=a[y].w[i]){
x=a[x].w[i];
y=a[y].w[i];
}
}
if(x!=y){
return a[x].w[0];
}
return x;
}
int main(){
cin>>n>>m>>s;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
a[u].E.push_back(v);
a[v].E.push_back(u);
}
fad(s,0);
cff();
while(m--){
int x,y;
cin>>x>>y;
cout<<LCA(x,y)<<endl;
}
return 0;
}
ST表
像区间最值这样的问题,我们把他们称为可重复贡献问题,可重复贡献问题就是指 x x x和 x x x做运算得到的结果还是 x x x,如 m i n ( x , x ) = x min(x, x) = x min(x,x)=x
对于静态可重复贡献问题,我们可以使用 s t st st表来快速查询结果,常见的可重复贡献问题有:区间最值,区间 g c d / l c m gcd/lcm gcd/lcm,区间按位与 / / /或
其实这些问题都可以归结为最值,按位与是每一个二进制位上取最小值;按位或是每个二进制位上取最大值; g c d gcd gcd是每个质因子的指数取最小值; l c m lcm lcm是每个质因子的指数取最大值。
s
t
st
st表的核心是,任意一个区间都可以表示成至多两个长度为
2
k
2k
2k的区间的并集。
因为交集的部分多次运算还等于本身,所以凑出的两个区间即使有交集,也不会影响最终的答案。
处理出倍增数组之后,对于任意一个区间查询,我们都可以找到两个已经预处理好结果的区间的并集等于原区间,所以对这两个区间的结果做 1 1 1次运算即可求出原区间的结果。
实现
代码: —————出处
#include<bits/stdc++.h>
using namespace std;
int t[20][100005];
int main(){
int m,n;
cin>>m>>n;
for(int i=1;i<=m;i++){
cin>>t[0][i];
}
for(int i=1;i<=log2(m);i++){
for(int j=1;j<=m-(1<<i)+1;j++){
t[i][j]=min(t[i-1][j],t[i-1][j+(1<<(i-1))]);
}
}
while(n--){
int l,r;
cin>>l>>r;
int z=log2(r-l+1);
cout<<min(t[z][l],t[z][r-(1<<z)+1])<<" ";
}
return 0;
}
复杂度
s
t
st
st表的预处理复杂度是
O
(
n
O(n
O(nlog2
n
)
n)
n),单次查询复杂度是
O
(
1
)
O(1)
O(1)
如果是维护
g
c
d
gcd
gcd的
s
t
st
st表,要在复杂度上乘一个
l
o
g
log
log (
g
c
d
gcd
gcd 常数更小 )
树状数组
树状数组是一种支持单点修改和可减区间查询的,码量低常数小的数据结构。
我们知道,任意一个数都可以表示为至多
l
o
g
log
log个
2
2
2的幂的和,如
7
=
4
+
2
+
1
7 = 4 + 2 + 1
7=4+2+1
这种思想是树状数组的核心。
如果可以找到一种方法,使得任意一个前缀和可以被log个区间和表示出来,并且任意一个位置都被最多 l o g log log个区间包括,那我们就可以实现 O ( l o g ) O(log) O(log)的单点修改,区间查询。
在树状数组中,我们利用
l
o
w
b
i
t
lowbit
lowbit(二进制表示中最小的1)来实现这种操作。
6
6
6的
l
o
w
b
i
t
lowbit
lowbit是
2
2
2,
8
8
8的
l
o
w
b
i
t
lowbit
lowbit是
8
8
8,可以用
x
x
x&-
x
x
x来快速计算。
00000101
0000 0101
00000101
11111010
1111 1010
11111010 ~ 取反
11111011
1111 1011
11111011 ~ 补码
树状数组中,
t
[
i
]
t[i]
t[i]表示以
i
i
i作为右端点,长度为
l
o
w
b
i
t
(
i
)
lowbit(i)
lowbit(i) 的区间和,
所以长度为
7
7
7的前缀和
=
t
[
7
]
+
t
[
6
]
+
t
[
4
]
= t[7] + t[6] + t[4]
=t[7]+t[6]+t[4],其中
6
=
7
−
l
o
w
b
i
t
(
7
)
6 = 7 - lowbit(7)
6=7−lowbit(7),
4
=
6
−
l
o
w
b
i
t
(
6
)
4 = 6 - lowbit(6)
4=6−lowbit(6)
在修改第 i i i个位置时,因为 t [ i ] t[i] t[i] 储存的区间的右端点是 i i i,所以只需要向更大的位置寻找包括第i个位置的区间,对每个区间做出修改即可,这一步也可以使用 l o w b i t lowbit lowbit快速计算。
实现
代码—————出处
#include<bits/stdc++.h>
using namespace std;
int s[500005],n,m;
int lowbit(int x){
return x&-x;
}
void add(int i,int x){
for(;i<=n;i+=lowbit(i))s[i]+=x;
}
int sum(int i){
int sm=0;
for(;i>=1;i-=lowbit(i))sm+=s[i];
return sm;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
add(i,x);
}
for(int i=1;i<=m;i++){
int f,x,k;
cin>>f>>x>>k;
if(f==1){
add(x,k);
}
if(f==2){
cout<<sum(k)-sum(x-1)<<endl;
}
}
return 0;
}