秒懂算法 | 倍增与ST算法

简介: 本篇介绍了倍增的概念、例题以及ST算法。

640.jpg

01、倍增

倍增就是“成倍增长”,每次递增2倍。

倍增法的题目都利用了数的二进制表示(二进制划分):

    N=a020+a121+a222+a323+a424+…

例如35,它的二进制是100011,第5、1、0位是1,即a5=a1=a0=1,把这几位的权值相加,有35=25+21+20=32+2+1。

数的二进制划分反映了一种快速增长的特性,第i位的权值2i等于前面所有权值的和加1:

  2i=2i−1+2i−2+…+21+20+1

一个整数n,它的二进制表示只有logn位。如果要从0增长到n,可以用1、2、4、…、2k为“跳板”,快速跳到n,这些跳板只有k = logn个。

倍增法的特点是需要提前计算出第1、2、4、…、2k个跳板,这要求数据是静态不变的,不是动态变化的。如果数据发生了变化,所有跳板要重新计算,跳板就失去了意义。

倍增法的经典应用有:矩阵快速幂、后缀数组、ST算法,LCA(最近公共祖先)。本节介绍相对简单的ST算法,另外也给出了一个应用倍增的经典题。

02、ST(Sparse Table)算法

ST算法是求解RMQ问题的优秀算法,它适用于静态空间的RMQ查询。

静态空间的RMQ问题(Range Minimum Query,区间最大最小值问题):给定长度为n的静态数列,做m次询问,每次给定L, R ≤ n,查询区间[L, R]内的最值。

下面都以最小值为例。

用暴力搜区间[L, R]的最小值,即逐一比较区间内的每个数,复杂度是O(n)的;m次查询,复杂度O(mn)。暴力法的效率很低。

ST算法源于这样一个原理:一个大区间若能被两个小区间覆盖,那么大区间的最值等于两个小区间的最值。例如下图中,大区间{4, 7, 9, 6, 3, 6, 4, 8, 7, 5}被两个小区间{4, 7, 9, 6, 3, 6, 4, 8}、{4, 8, 7, 5}覆盖,大区间的最小值3,等于两个小区间的最小值,min{3, 4}=3。这个例子特意让两个小区间有部分重合,因为重合不影响结果。

640.png


▍图1.大区间被两个小区间覆盖

从以上原理得到ST算法的基本思路,包括两个步骤:

(1)把整个数列分为很多小区间,并提前计算出每个小区间的最值;

(2)对任意一个区间最值查询,找到覆盖它的两个小区间,用两个小区间的最值算出答案。

如何设计出这2个步骤的高效算法?

对于(1),简单的方法是把数列分为固定大小的小区间,即“分块”,在“分块与莫队算法”中有介绍。它把数列分为√n块,每块有√n个数,提前计算这√n个小区间的最值,复杂度是O(n√n)。然后对于(2)的最值查询,每次计算量约为O(1)。这种算法的效率比暴力法强很多,但是还不够好。

下面用“倍增”的方法来分块,它的效率非常高:(1)的复杂度是O(nlogn),(2)的复杂度是O(1)。

  1. 把数列按倍增分成小区间

对数列的每个元素,把从它开始的数列分成长度为1、2、4、8、…的小区间。

下图给出了一个分区的例子,它按小区间的长度分成了很多组。

第1组是长度为1的小区间,有n个小区间,每个小区间有1个元素;

第2组是长度为2的小区间,有n个小区间,每个小区间有2个元素;

第3组是长度为4的小区间,有n个小区间,每个小区间有4个元素;

共有logn组。

640.png


▍图2.按倍增分为小区间

可以发现,每组的小区间的最值,可以从前一组递推而来。例如第3组{4, 7, 9, 6}的最值,从第2组{4, 7}、{9, 6}的最值递推得到。

定义dps,表示左端点是s,区间长度为2k的区间最值。递推关系是:

   dps = min{dps, dps + 1<<(k-1)}

其中1<<(k-1)等于2k−1。

用dp[][]来命名,是因为递推关系是一个动态规划的过程。

计算所有小区间的最值,即计算出所有的dp[][],复杂度是多少?图中的每一组都需计算n次,共logn组,总计算量是O(nlogn)。

  1. 查询任意区间的最值

根据上面的分区方法,有以下结论:以任意元素为起点,有长度为1、2、4、…的小区;以任意元素为终点,它前面也有长度为1、2、4、…的小区。

根据这个结论,可以把需要查询的区间[L, R]分为2个小区间:以L为起点的小区间、以R为终点的小区间,让这两个小区间首尾相接覆盖[L, R],区间最值从两个小区间的最值求得。一次查询的计算复杂度是O(1)。

区间[L, R]的长度是len = R-L+1。两个小区间的长度是x,令x是比len小的最大2的倍数,有2*x ≥ len,这样保证能覆盖。另外需要计算dp[][],根据dps的定义,有2k =x。例如len = 19,x = 16,2k = 16,k = 4。

已知len如何求k?计算公式是k=log2(len)= log(len)/log(2),向下取整。以下两种代码都行:

int k=(int)(log(double(R-L+1)) / log(2.0)); //以10为底的库函数log()
int k=log2(R-L+1); //以2为底的库函数log2()

如果觉得库函数log2()比较慢,可以自己提前算出LOG2,LOG2[i]的值与向下取整的log2(i)相等:

LOG2[0] = -1;
    for(int i=1;i<=maxn;i++)
        LOG2[i] = LOG2[i>>1]+1;

最后给出区间[L, R]最小值的计算公式,等于覆盖它的两个小区间的最小值:

   min(dpL,dpR-(1<<k)+1);

用这个公式做一次最值查询,计算复杂度是O(1)。

下面用一个模板题给出代码。
题目描述:给定一个包含n个整数的数列,和q个区间询问,询问区间内最大值和最小值的差。

输入:第一行是2个整数,n和q。接下来n行,每行一个整数hi。再后面q行,每行2个整数a、b,表示一个区间询问。

输出:对每个区间询问,返回区间内最大值和最小值的差。

数据范围:1≤n≤5×104,1≤q≤1.8×105,1≤a≤b≤n。
代码:

#include<cstdio>
#include<iostream>
#include<cmath>
using namespace std;

const int maxn=50005;
int a[maxn],dp_max[maxn][22],dp_min[maxn][21],n,m;
int LOG2[maxn]; //自己计算以2为底的对数,向下取整
void st_init(){
    LOG2[0]=-1;
    for(int i = 1;i<=maxn;i++) //不用系统的log()函数,自己算
        LOG2[i] = LOG2[i>>1]+1;

    for(int i=1;i<=n;i++){ //初始化区间长度为1时的值
        dp_min[i][0]=a[i];
        dp_max[i][0]=a[i];
    }
    //int p=log2(n); //可倍增区间的最大次方: 2^p <= n
    int p= (int)(log(double(n)) / log(2.0)); //两者写法都行
  for(int k=1;k<=p;k++) //倍增计算小区间。先算小区间,再算大区间,逐步递推
        for(int s=1;s+(1<<k)<=n+1;s++){
            dp_max[s][k]=max(dp_max[s][k-1], dp_max[s+(1<<(k-1))][k-1]);
            dp_min[s][k]=min(dp_min[s][k-1], dp_min[s+(1<<(k-1))][k-1]);
  }
}
int st_query(int L,int R){
    //int k=log2(R-L+1); //3种方法求k
    int k=(int)(log(double(R-L+1)) / log(2.0));
    //int k=LOG2[R-L+1]; //用自己算的LOG2
    int x=max(dp_max[L][k],dp_max[R-(1<<k)+1][k]);//区间最大
    int y=min(dp_min[L][k],dp_min[R-(1<<k)+1][k]);//区间最小
    return x-y; //返回差值
}
int main(){
    scanf("%d%d",&n,&m);//输入
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    st_init();
  for(int i=1;i<=m;i++){
    int L,R; scanf("%d%d",&L,&R);
    printf("%d\n",st_query(L,R));
  }
  return 0;
}

03、几个讨论

1. ST算法与线段树

求解RMQ问题更常用的是线段树,它求RMQ的时间复杂度与ST差不多,但是两者有很大区别:线段树用于动态数组,ST用于静态数组。

ST算法用O(nlogn)时间来预处理数组,预处理之后每次区间查询是O(1)的。如果数组是动态改变的,改变一次就需要用O(nlogn)预处理一次,导致效率很低。线段树适用于动态维护(添加或删除)的数组,它每次维护数组和每次查询都是O(logn)的。从数组是否能动态维护这个角度来说,ST是离线算法,线段树是在线算法。ST的优势是编码简单,如果数组是静态的,就用ST。

2. ST算法的适用场合

从ST算法的原理看,它的核心思想是“大区间被两个小区间覆盖、小区间的重复覆盖不影响结果”,然后用倍增法划分小区间和计算小区间的最值。最大值和最小值符合这种场景,类似的有RGQ问题(Range GCD Query,区间最大公约数问题):查询给定区间的GCD。

04、倍增例题

下面给出另一个应用倍增的经典题目。
题目描述:边境上有m个边防站围成一圈,顺时针编号1到m。有n个战士,每个战士常驻2个站,能在2个站之间移动。局长有个国旗计划,让边防战士举着国旗环绕一圈。局长想知道至少需要多少战士才能完成国旗计划,并且他想知道,在某个战士必须参加的情况下,至少需要多少边防战士。

输入:第一行是两个正整数n,m,表示战士数量和边防站数量。后面n行,每行有两个正整数,第i行的ci、di表示i号边防战士常驻的两个边防站编号,沿顺时针从ci边防站到di是他的移动区间。数据保证整个边境线是可被覆盖的。所有战士的移动区间互相不包含。

输出:输出一行,包含n个正整数,其中第j个正整数表示j号战士必须参加的前提下至少需要多少边防战士才能顺利完成国旗计划。

数据范围:n≤2×105, m<109, 1≤ci,di≤m
题目的要求很清晰:计算能覆盖整个圆环的最少区间(战士)。

题目给定的所有区间互相不包含,那么按区间的左端点排序后,区间的右端点也是单调增加的。这样情况下能用贪心来选择区间。

解题用到的技术有:断环成链、贪心、倍增。

(1)断环成链路。把题目给的环断开变成一条链,更方便处理。注意环是首尾相接的,断开后为了保持原来的首尾的关系,需要把原来的环复制再相接。

(2)贪心。首先考虑从一个区间出发,如何选择所有的区间。选择一个区间i后,下一个区间只能从左端点小于等于i的右端点的那些区间中选,在这些区间中选右端点最大的那个区间,是最优的。例如下图选择区间i后,下一个区间可以从A、B、C中选,它们的左端点都在i内部。C是最优的,因为它的右端点最远。选定C之后,再用贪心策略找下一个区间。这样找下去,就得到了所需的最少区间。

640.png


▍图3.用贪心选下一个最优区间

以i为起点,用贪心查询一次,要遍历所有的区间,复杂度O(n)。题目要求以每个区间为起点,共做n次查询,总复杂度是O(n2),超时。

(3)倍增。为了进行高效的n次查询,可以用类似ST算法中的倍增,预计算出一些“跳板”,快速找到后面的区间。

定义gos:表示从第s个区间出发,走2i个最优区间后到达的区间。例如 go s,是从s出发到达的第2i =16个最优的区间,s和gos之间的区间也都是最优的。

预计算出从所有的区间出发的go[][],以它们为“跳板”,就能快速跳到目的地。

注意,跳的时候先用大数再用小数。以从s跳到后面第27个区间为例:

  1)从s跳16步,到达s后的第16个区间f1;

  2)从f1跳8步,到达f1后的第8个区间f2;

  3)从f2跳2步到达f3;

  4)从f3跳1步到达终点f4。

共跳了16+8+2+1=27步。这个方法利用了二进制的特征,27的二进制是11011,其中的4个“1”的权值就是16、8、2、1。把一个数转换为二进制数时,是从最高位往最低位转换的,这就是为什么要先用大数再用小数的原因。

640.png


▍图4.用倍增跳到终点

复杂度是多少?查询一次,用倍增法从s跳到终点复杂度是O(logn)的。共有n次查询,总复杂度O(nlogn)。

剩下的问题是如何快速预计算出go[][]。有以下非常巧妙的递推关系:

   gos = gogo[s][i-1]

递推式的右边这样理解:

   1)gos。从s起跳,先跳2i−1步到了区间z = gos;

   2)gogo[s][i-1] = goz。再从z跳2i−1步到了区间goz。

一共跳了2i−1+2i−1=2i步。公式右边实现了从s起跳,跳到了s的第2i个区间,这就是递推式左边的gos。

特别地,gos是x后面第20 = 1个区间(用贪心算出的下一个最优区间),gos是递推式的初始条件,从它递推出了所有的go[][]。递推的计算量有多大?从任意一个s到末尾,最多只有logn个go[s][],所以只需要递推O(logn)次。计算n个结点的go[][],共计算O(nlogn)次。

以上所有的计算,包括预计算go[][]和n次查询,总复杂度是O(nlogn) + O(nlogn)。

下面是代码。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 4e5+1;
int n, m;

struct warrior{   
    int id, L, R; //id:战士的编号;L、R,战士的左右区间
    bool operator < (const warrior b) const{return L < b.L;}
}w[maxn*2];
int n2;
int go[maxn][20];
void init(){ //贪心 + 预计算倍增
    int nxt = 1;
    for(int i=1;i<=n2;i++){ //用贪心求每个区间的下一个区间
        while(nxt<=n2 && w[nxt].L<=w[i].R) //每个区间的下一个是右端点最大的那个区间
            nxt++;
        go[i][0]=nxt-1; //区间i的下一个区间
    }
    for(int i=1;(1<<i)<=n;++i) //倍增:i=1,2,4,8,... 共log(n)次
        for(int s=1;s<=n2;s++) //每个区间后的第2^i个区间
            go[s][i] = go[go[s][i-1]][i-1];
}
int res[maxn];
void getans(int x){ //从第x个战士出发
    int len=w[x].L+m, cur=x, ans=1;
    for(int i=log2(maxn);i>=0;i--){ //从最大的i开始找:2^i = maxn
        int pos = go[cur][i];
        if(pos && w[pos].R < len){
            ans += 1<<i; //累加跳过的区
            cur = pos; //从新位置继续开始
        }
    }
    res[w[x].id] = ans+1;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        w[i].id = i; //记录战士的顺序
        scanf("%d%d",&w[i].L, &w[i].R);
        if(w[i].R < w[i].L) //把环变成链
            w[i].R += m;
    }
    sort(w+1, w+n+1); //按左端点排序
    n2 = n;
    for(int i=1;i<=n;i++){ //拆环加倍成一条链
        n2++; w[n2]=w[i]; w[n2].L=w[i].L+m; w[n2].R=w[i].R+m;
    }
    init();
    for(int i=1;i<=n;i++) getans(i); //逐个计算每个战士
    for(int i=1;i<=n;i++) printf("%d ",res[i]);
    return 0;
}
目录
相关文章
|
算法 Python
person correlation,spearman correlation and kendall's t的算法(python,算法来自FRM-market risk )
person correlation,spearman correlation and kendall's t的算法(python,算法来自FRM-market risk )
87 0
|
人工智能 算法 BI
RMQ问题(线段树算法,ST算法优化)
RMQ (Range Minimum/Maximum Query)问题是指: 对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j
1559 0
|
1天前
|
机器学习/深度学习 算法
基于改进遗传优化的BP神经网络金融序列预测算法matlab仿真
本项目基于改进遗传优化的BP神经网络进行金融序列预测,使用MATLAB2022A实现。通过对比BP神经网络、遗传优化BP神经网络及改进遗传优化BP神经网络,展示了三者的误差和预测曲线差异。核心程序结合遗传算法(GA)与BP神经网络,利用GA优化BP网络的初始权重和阈值,提高预测精度。GA通过选择、交叉、变异操作迭代优化,防止局部收敛,增强模型对金融市场复杂性和不确定性的适应能力。
102 80
|
20天前
|
算法
基于WOA算法的SVDD参数寻优matlab仿真
该程序利用鲸鱼优化算法(WOA)对支持向量数据描述(SVDD)模型的参数进行优化,以提高数据分类的准确性。通过MATLAB2022A实现,展示了不同信噪比(SNR)下模型的分类误差。WOA通过模拟鲸鱼捕食行为,动态调整SVDD参数,如惩罚因子C和核函数参数γ,以寻找最优参数组合,增强模型的鲁棒性和泛化能力。
|
26天前
|
机器学习/深度学习 算法 Serverless
基于WOA-SVM的乳腺癌数据分类识别算法matlab仿真,对比BP神经网络和SVM
本项目利用鲸鱼优化算法(WOA)优化支持向量机(SVM)参数,针对乳腺癌早期诊断问题,通过MATLAB 2022a实现。核心代码包括参数初始化、目标函数计算、位置更新等步骤,并附有详细中文注释及操作视频。实验结果显示,WOA-SVM在提高分类精度和泛化能力方面表现出色,为乳腺癌的早期诊断提供了有效的技术支持。
|
6天前
|
供应链 算法 调度
排队算法的matlab仿真,带GUI界面
该程序使用MATLAB 2022A版本实现排队算法的仿真,并带有GUI界面。程序支持单队列单服务台、单队列多服务台和多队列多服务台三种排队方式。核心函数`func_mms2`通过模拟到达时间和服务时间,计算阻塞率和利用率。排队论研究系统中顾客和服务台的交互行为,广泛应用于通信网络、生产调度和服务行业等领域,旨在优化系统性能,减少等待时间,提高资源利用率。
|
14天前
|
存储 算法
基于HMM隐马尔可夫模型的金融数据预测算法matlab仿真
本项目基于HMM模型实现金融数据预测,包括模型训练与预测两部分。在MATLAB2022A上运行,通过计算状态转移和观测概率预测未来值,并绘制了预测值、真实值及预测误差的对比图。HMM模型适用于金融市场的时间序列分析,能够有效捕捉隐藏状态及其转换规律,为金融预测提供有力工具。
|
22天前
|
算法
基于GA遗传算法的PID控制器参数优化matlab建模与仿真
本项目基于遗传算法(GA)优化PID控制器参数,通过空间状态方程构建控制对象,自定义GA的选择、交叉、变异过程,以提高PID控制性能。与使用通用GA工具箱相比,此方法更灵活、针对性强。MATLAB2022A环境下测试,展示了GA优化前后PID控制效果的显著差异。核心代码实现了遗传算法的迭代优化过程,最终通过适应度函数评估并选择了最优PID参数,显著提升了系统响应速度和稳定性。
|
14天前
|
机器学习/深度学习 算法 信息无障碍
基于GoogleNet深度学习网络的手语识别算法matlab仿真
本项目展示了基于GoogleNet的深度学习手语识别算法,使用Matlab2022a实现。通过卷积神经网络(CNN)识别手语手势,如&quot;How are you&quot;、&quot;I am fine&quot;、&quot;I love you&quot;等。核心在于Inception模块,通过多尺度处理和1x1卷积减少计算量,提高效率。项目附带完整代码及操作视频。