20250804 倍增&ST表 树状数组

引子

我们之前学习过前缀和,可以查询静态区间和。

前缀和是预处理出每一个前缀,对于任意一个区间,我们都可以用两个前缀和之差来表示这个区间内元素之和。

如果区间查询的是某种没有可减性的值,例如区间最值、区间 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、...( 1248...(所有 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=7lowbit(7) 4 = 6 − l o w b i t ( 6 ) 4 = 6 - lowbit(6) 4=6lowbit(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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值