倍增

倍增,字面意思即”成倍增长“

他与二分十分类似,都是基于”2“的划分思想

那么具体是怎么样,我们以一个例子来看

ST表才是文章的重点 QwQ


查找 洛谷P2249

依据题面,我们知道这是一个单调序列,当然可以通过二分的方式来寻找答案,但是既然我们这里讲倍增,那么就用倍增来写吧!

首先,我们先贴上核心代码

void find(int k) {
int i = 0, p = 1;
while (p) {
if (i + p < n && a[i + p] < k) i += p, p <<= 1;
else p >>= 1;
} if (a[i + 1] == k)
printf("%d ", i + 1);
else
printf("-1 ");
}

其中i表示所寻找的下标,p表示步长。

算法步骤如下:

  1. 保证i + p没有超过上界并比较a[i + p]k的大小关系,如果小于k,证明最终答案必定在i之后,所以将i设为i + p,并将步长p乘以2;否则,将步长p除以2

  2. 重复上一步,直到步长p == 0,此时,a[i]为严格小于k的最后一个数。

  3. 如果a[i + 1]不为k,则k不存在于数组中,输出-1;否则,输出i + 1

其实不难发现,其实这种代码比而二分的代码简洁了很多,所以我很喜欢用倍增

了解了上述步骤,我们可以发现,倍增的思想体现在步长之上,那为什么步长关于2的变换时正确的呢?

其实我们很容易知道,每一个数都可以以二进制数表示,而这里的步长从某种意义上来说相当于对于数的每一个二进制位的修改。即是用了“二进制划分”的思想。


重点

像上面代码写的倍增最终i的位置是最后一个满足if后的条件的位置


变式练习

如果我们把问题改为寻找最后一次出现的位置呢?这时算法该如何书写?

参考代码见文末


快速幂

其实,从上面的例子中我们已经对于倍增的思想有了一些体会。

实际上,“倍增”与“二进制划分”两个思想相互结合,才碰撞出了不一样的烟火。如这里的快速幂。

快速幂可以参考这篇文章:算法学习笔记(4):快速幂 - 知乎 (zhihu.com)

但是,在这篇文章的讲述中,快速幂的递归形式实际上时使用了二分的思想。而只有递推的形式才属于倍增的思想。

其实这里我们可以看出倍增与二分的联系:倍增类似于二分的逆过程,当然,这并不准确。

上面链接所给文章中快速幂讲述的十分清楚,甚至有额外的拓展,所以就不再详细展开。

这里给出一个快速幂的参考代码

// (a**x) % p
int quickPow(int a, int x, int p) {
int r = 1;
while (x) {
// no need to use quickMul when p*p can be smaller than int64.max !!!
if (x & 1) r = (r * a) % p;
a = (a * a) % p, x >>= 1;
}
return r;
}

ST表

在RMQ(区间最值)问题中,著名的ST算法就是倍增的产物。ST算法可以在\(O(N\,log\,N)\)的时间复杂度能预处理后,以\(O(1)\)的复杂度在线回答区间[l, r]内的最值。

当然,ST表不支持动态修改,如果需要动态修改,线段树是一种良好的解决方案,也是\(O(N\,log\,N)\)的时间复杂度,但是查询需要\(O(logN)\)的时间复杂度

那么ST表中倍增的思想是如何体现的呢?

一个序列的子区间明显有\(N^2\)个,根据倍增的思想,我们在这么多个子区间中选择一些长度为\(2\)的整数次幂的区间作为代表值。

设\(st[i][j]\)表示子区间\([i, i+2^j)\)里最大的数

也可以表示为\([i, i + 2^j -1 ]\),无论如何,其中有\(2^j\)个元素

下文中的\(a\)表示原序列

递推边界明显是\(st[i][0] = a[i]\)。

于是,根据成倍增长的长度,有了递推公式

\[st[i][j] = max(st[i][j-1],\;st[i+2^{j-1}][j-1])
\]

当询问任意区间\([l, r]\)的最值时,我们先计算出一个最大的\(k\)满足:\(2^k \le r - l + 1\),即需要不大于区间长度。那么,由于二进制划分我们可以知道,这个最大的k一定满足\(2^{k+1}\ge r-l+1\),即我们只需要将两个长度为\(2^k\)的区间合并即可。

又根据max(a, a) = a可以知道,重复计算区间是没有任何问题的。

所以,在寻找最值的时候就有了以下公式:

\[max(a[l, r]) = max(st[l][k], st[r-2^k + 1][k])
\]

那么这里给出一种参考代码

// 啊,写这种预处理以2位底的对数的整数值的方式
// 我主要是为了将代码模块化,做到低耦合度
// 完全是可以分开来写的
class Log2Factory {
private:
int lg2[N];
public:
void init(int n) {
for (int i = 2; i <= n; ++i) lg2[i] = lg2[i >> 1] + 1;
} // 重载()运算符
int operator() (const int &i) {
return lg2[i];
}
}; template<typename T>
class STable {
private: typedef T(*OP_FUNC)(T, T);
Log2Factory Log2;
T f[N][17]; // maybe most of the times k=17 is ok, make sure 2^k greater than N;
OP_FUNC op;
public:
void setOp(OP_FUNC fc) {
op = fc;
} void init(T *a, int n) {
for (int i = 1; i <= n; ++i)
f[i][0] = *(++a); int t = Log2(n);
// f[i][k] is the interval of [i, i + 2^k - 1]
// so f[i][k] can equal to the op sum of [i, i^k - 1]
// let r = i^k - 1
// => f[r - (1^k) + 1][k] can equal to the op sum of [i][k]
for (int k = 1; k <= t; ++k) {
for (int i = 1; i + (1<<k) - 1 <= n; ++i)
f[i][k] = op(f[i][k-1], f[i + (1<<(k-1))][k-1]);
}
} const T query(int l, int r) {
int k = Log2(r - l + 1);
return op(f[l][k], f[r - (1<<k) + 1][k]);
}
};

这……写法很神奇,注意修改!

扩展 - 运算

ST算法不仅仅是可以求区间的最值的,只要时满足静态的,满足区间加法的问题大多数情况都可以通过ST表实现。

那么区间加法是什么意思呢?

定义我们需要对数列的筛选函数为op,则需要op满足以下性质

  • op(a, a) = a,即重复参与运算不改变最终影响

  • op(a, b) = op(b, a),即满足交换律

  • op(a, op(b, c)) = op(op(a, b), c),即满足结合律

举个例子,如果我们求区间是否有负数,可以将op设为如下逻辑:

bool op(bool a, bool b) {
return a | b;
}

相应的,初始化的方式也需要更改

if (a[i] < 0) st[i][0] = true;
else st[i][0] = false;

再举一个例子,如果我们需要求区间是否全为偶数时,则初始化为

if (a[i] % 2 == 0) st[i][0] = true;
else st[i][0] = false;

操作op定义为

bool op(bool a, bool b) {
return a & b;
}

由此可见,其实ST算法可以做到的不仅仅是区间最值那么普通的东西啊。

但是,由于加法不满足性质一,所以,ST表通过这种方法并不能求得区间的所有满足某种性质的元素的个数。但是,通过另外一种query方式,我们可以做到这样。

扩展 - 区间

那么这个部分我们将讨论如何利用ST表做到上文例子中求区间偶数的个数。

同样,由于我们可以通过二进制划分,所以可以将某一个区间长度转化为多个长度为2的整数幂次方的子区间,并且可以保证这些区间不相互重叠

其实这是借鉴了一点线段树的思路

那么可以写出以下代码

int query(int l, int r) {
if (l == r) return st[l][0];
int k = log2(r - l + 1);
return op(st[l][k], query(l + (1<<k), r))
}

这样就满足了区间不重叠

或许会有一个问题,为什么初始化的时候不需要修改?

其实不难发现,初始化的合并是不会有重复贡献的情况的,即是每一次合并的区间是不会重叠的


变式答案

其实非常类似的!

void find(int k) {
int i = 0, p = 1;
while (p) {
if (i + p <= n && a[i + p] <= k) i += p, p <<= 1;
else p >>= 1;
} if (a[i] == k)
printf("%d ", i);
else
printf("-1 ");
}

算法学习笔记(3): 倍增与ST算法的更多相关文章

  1. 再探快速傅里叶变换(FFT)学习笔记(其三)(循环卷积的Bluestein算法+分治FFT+FFT的优化+任意模数NTT)

    再探快速傅里叶变换(FFT)学习笔记(其三)(循环卷积的Bluestein算法+分治FFT+FFT的优化+任意模数NTT) 目录 再探快速傅里叶变换(FFT)学习笔记(其三)(循环卷积的Blueste ...

  2. C / C++算法学习笔记(8)-SHELL排序

    原始地址:C / C++算法学习笔记(8)-SHELL排序 基本思想 先取一个小于n的整数d1作为第一个增量(gap),把文件的全部记录分成d1个组.所有距离为dl的倍数的记录放在同一个组中.先在各组 ...

  3. OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波

    http://blog.csdn.net/chenyusiyuan/article/details/8710462 OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波 201 ...

  4. Manacher算法学习笔记 | LeetCode#5

    Manacher算法学习笔记 DECLARATION 引用来源:https://www.cnblogs.com/grandyang/p/4475985.html CONTENT 用途:寻找一个字符串的 ...

  5. 机器学习实战(Machine Learning in Action)学习笔记————06.k-均值聚类算法(kMeans)学习笔记

    机器学习实战(Machine Learning in Action)学习笔记————06.k-均值聚类算法(kMeans)学习笔记 关键字:k-均值.kMeans.聚类.非监督学习作者:米仓山下时间: ...

  6. Johnson算法学习笔记

    \(Johnson\)算法学习笔记. 在最短路的学习中,我们曾学习了三种最短路的算法,\(Bellman-Ford\)算法及其队列优化\(SPFA\)算法,\(Dijkstra\)算法.这些算法可以快 ...

  7. 某科学的PID算法学习笔记

    最近,在某社团的要求下,自学了PID算法.学完后,深切地感受到PID算法之强大.PID算法应用广泛,比如加热器.平衡车.无人机等等,是自动控制理论中比较容易理解但十分重要的算法. 下面是博主学习过程中 ...

  8. Johnson 全源最短路径算法学习笔记

    Johnson 全源最短路径算法学习笔记 如果你希望得到带互动的极简文字体验,请点这里 我们来学习johnson Johnson 算法是一种在边加权有向图中找到所有顶点对之间最短路径的方法.它允许一些 ...

  9. HMM的学习笔记1:前向算法

    HMM的学习笔记 HMM是关于时序的概率模型.描写叙述由一个隐藏的马尔科夫链随机生成不可观測的状态随机序列,再由各个状态生成不可观測的状态随机序列,再由各个状态生成一个观測而产生观測的随机过程. HM ...

  10. jvm学习笔记一(垃圾回收算法)

    一:垃圾回收机制的原因 java中,当没有对象引用指向原先分配给某个对象的内存时候,该内存就成为了垃圾.JVM的一个系统级线程会自动释放该内存块.垃圾回收意味着程序不再需要的对象是"无用信息 ...

随机推荐

  1. Laravel-Easy-Admin 快速搭建数据后台 web管理后台

    基于PHP + Laravel + element-admin-ui 搭建的快速数据后台,只在解决系列后台增删改查等日常操作.快速搭建,在生成业务的同时可以花更多的时间关注技术本身,提高程序员自身进阶 ...

  2. Spring 深入——IoC 容器 02

    IoC容器的实现学习--02 目录 IoC容器的实现学习--02 回顾 IoC 容器的初始化过程: BeanDefinition 的 Resource 定位 小结: 回顾 前面学习了 IoC 模式的核 ...

  3. 驱动开发:内核监视LoadImage映像回调

    在笔者上一篇文章<驱动开发:内核注册并监控对象回调>介绍了如何运用ObRegisterCallbacks注册进程与线程回调,并通过该回调实现了拦截指定进行运行的效果,本章LyShark将带 ...

  4. C#--@符号的使用(逐字字符串,跨行,声明关键字变量名)

    ---对字符串的使用 @可以定义逐字字符串 注意:@只对字符串常量有用 1)不需要用\\来转义非转义符号的\号   例如:@"\"="\\"2)可以实现多行字符 ...

  5. 最全iOS 上架指南

    一.上架基本需求资料 1.苹果开发者账号(公司已有可以不用申请,需要开通开发者功能,每年 99 美元) 2.开发好的APP 二.证书 上架版本需要使用正式的证书 1.创建证书 Apple Develo ...

  6. 全球名校AI课程库(38)| 马萨诸塞大学 · 自然语言处理进阶课程『Advanced Natural Language Processing』

    课程学习中心 | NLP课程合辑 | 课程主页 | 中英字幕视频 | 项目代码解析 课程介绍 自然语言处理 (NLP) 是一门关于如何教计算机理解人类语言的工程艺术和科学.NLP 作为一种人工智能技术 ...

  7. 云原生之旅 - 7)部署Terrform基础设施代码的自动化利器 Atlantis

    前言 前面有几篇文章讲述了如何使用Terraform创建资源 (基础设施即代码 Terraform 快速入门, 使用 Terraform 创建 Kubernetes) 以及 Kubernetes时代的 ...

  8. DNS 解析 prefeath

    本文将详细介绍DNS预解析prefetch的主要内容 概述 DNS(Domain Name System, 域名系统),是域名和IP地址相互映射的一个分布式数据库.DNS 查询就是将域名转换成 IP ...

  9. 【初赛】CSP 2020 第一轮(初赛)模拟记录

    感觉初赛不过关,洛谷上找了一套没做过的来练习. 顺便写了详细的题解. 试题用时:1h 单项选择: 第 1 题 十进制数 114 的相反数的 8 位二进制补码是: A.10001110 B.100011 ...

  10. 2022春每日一题:Day 8

    题目:[HNOI2003]激光炸弹 二维前缀和,扫大小为m*m的矩形,取最大即可. 代码: #include <cstdio> #include <cstdlib> #incl ...