算法学习笔记(3): 倍增与ST算法

算法,学习,笔记,倍增,st · 浏览次数 : 81

小编点评

**算法学习笔记(3.1) - ST 表倍增更多的用法优化矩形查询在静态的序列上查找一个区间 extremely naive 了,所以考虑在静态的矩形上查找一个矩形区间的信息。令 f[x][y][i][j] 表示左下角为 \\((x, y)\\),右上角为 \\((x + 2^i - 1, y + 2^j - 1)\\) 的矩形的信息。如果满足 op(a, a) = a,那么一个矩形可以被 \\(4\\) 个矩形表示。如果不满足,那么横向可以被划分为 \\(O(\\log n)\\) 个区间,纵向也是,所以一共可以被划分为 \\(O(log^2 n)\\)个子矩形,加起来即可。优化建图这里以 NOIP 2018 保卫王国 为例。倍增优化 DP,或者说叫做优化不带修的 DDP(瞬间高大上),也就是考虑链上状态影响的叠加。改变 DP 的初始值,然后 \\(O(\\log n)\\) 次叠加状态,然后得到最终的 DP 值。这不仅可以用在序列上,也可以用在树上,只是在树上可能需要考虑的更多。不过很好的是,可以将转移写成(广义)矩阵的形式,这样状态的叠加就会十分 naive,直接维护一个矩阵即可。作者有话说感谢 Larray76 指出文中的一个问题。

正文

倍增

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

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

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


查找 洛谷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 的变换时正确的呢?

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


但是这种写法真的很好吗?在实际的测试中,这种写法的常数可能在 \(1.2 \sim 1.5\) 倍的样子。

所以我们需要优化常数

考虑如下代码:

int find(int v) {
    int idx = 0;
    for (int w = 1 << (int)log2(n); w; w >>= 1) {
        if (idx + w <= n && a[idx + w] < v)
            idx += w;
    }

    return a[idx + 1] == v ? idx + 1 : -1;
}

先不说简单很多的问题……又短又快。

考虑在二进制中,\(2^k\) 的影响一定是大于 \(2^{k - 1}\) 的。

这启示我们可以逐步减少步长,尝试向前走。于是有了此写法。常数十分优秀。


但是第二种写法能够完全代替第一种写法吗?

答案是不可以的,例如 109. 天才ACM - AcWing题库

这道题只能使用第一种写法才能保证复杂度为 \(O(n \log n)\) 或者 \(O(n \log^2 n)\) 的。

为什么?我们可以从更严谨的复杂度写法上看出端倪。

不妨设每一次判断的代价为 \(f(x)\),那么对于第一种写法,如果答案为 \(a\),那么其实际复杂度是 \(O(f(a) \log a)\) 的,而对于第二种写法,实际的复杂度是 \(O(f(n) \log n)\) 的,与答案的关系没有关系。

其实后者更可以看作是二分的小常数版本,所以复杂度都是 \(O(f(n) \log n)\) 的。


重点

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


变式练习

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

其实非常类似的!

int 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;
    }

    return a[i] == k ? i : -1;
}

或者是:

int find(int v) {
    int idx = 0;
    for (int w = 1 << (int)log2(n); w; w >>= 1) {
        if (idx + w <= n && a[idx + w] <= v)
            idx += w;
    }

    return a[idx] == v ? idx : -1;
}

快速幂

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

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

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

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

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

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

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

// (a**x) % p
int quickPow(int a, int x, int p) {
    int r = 1;
    for (; x; x >>= 1, a = (a * a) % p)
        if (x & 1) r = (r * a) % p;
    return r;
}

ST表

见文章:算法学习笔记(3.1) - ST 表


倍增更多的用法

优化矩形查询

在静态的序列上查找一个区间非常的 naive 了,所以考虑在静态的矩形上查找一个矩形区间的信息。

f[x][y][i][j] 表示左下角为 \((x, y)\),右上角为 \((x + 2^i - 1, y + 2^j - 1)\) 的矩形的信息。

如果满足 op(a, a) = a,那么一个矩形可以被 \(4\) 个矩形表示。

如果不满足,那么横向可以被划分为 \(O(\log n)\) 个区间,纵向也是,所以一共可以被划分为 \(O(log^2 n)\)
个子矩形,加起来即可。

优化建图

这里以 【XR-1】逛森林 为例。

一个区间向一个区间连边(有向边),可以将一个点拆成入点和出点两个点,然后类似于ST表,每 \(2^k\) 合并为一个点。

当然,暴力合并一定是不可以的,所以类似于倍增求 LCA 的时候,将 in/out[x][k]in/out[fa[x][k][k] 合并成 in/out[x][k + 1] 即可,这样边数和点数都为 \(O(n \log n)\) 的了。

如果满足可重性,也就是重复连边对答案没有影响,那么类似于 ST表 的正常查询,拆成 \([x, x + 2^k)\)\((y - 2^k, y]\) 两个区间,连边即可。

如果不可以,那么同样的,两侧都拆分为 \(O(\log n)\) 个区间,如果可以,新建一个虚拟点,然后分别连边,这样边数新增 \(O(\log n)\),否则只能一一连边,共 \(O(\log^2 n)\) 条边,可能空间复杂度会炸,所以要慎重考虑。

还有一道 SCOI 2016 萌萌哒属于是倍增优化建图的另类了,优化区间一一合并。

优化 DP

这里以 NOIP 2018 保卫王国 为例。

倍增优化 DP,或者说叫做优化不带修的 DDP(瞬间高大上),也就是考虑链上状态影响的叠加。

改变 DP 的初始值,然后 \(O(\log n)\) 次叠加状态,然后得到最终的 DP 值。

这不仅可以用在序列上,也可以用在树上,只是在树上可能需要考虑的更多。

不过很好的是,可以将转移写成(广义)矩阵的形式,这样状态的叠加就会十分的 naive,直接维护一个矩阵即可。


作者有话说

感谢 Larray76 指出文中的一个问题。线段树的建树时间确实是 \(O(n)\) 而非 \(O(n \log n)\)

在实际的应用种,\(ST\) 表一般很少会用到,因为静态的要求太强了。

但是倍增不一样,不仅仅是可以代替二分的存在,还是很多算法的基础思想(这是二分无法做到的),例如多项式全家桶,并且其优美的性质总能解决很多问题。

倍增是十分重要的基础算法,在各大赛场上都有其身影,可见其为优化代码的重要手段,不得不重视!

完结撒花~

与算法学习笔记(3): 倍增与ST算法相似的内容:

算法学习笔记(3): 倍增与ST算法

倍增 目录倍增查找 洛谷P2249重点变式练习快速幂ST表扩展 - 运算扩展 - 区间变式答案倍增更多的用法优化矩形查询优化建图优化 DP作者有话说 倍增,字面意思即”成倍增长“ 他与二分十分类似,都是基于”2“的划分思想 那么具体是怎么样,我们以一个例子来看 查找 洛谷P2249 依据题面,我们知

算法学习笔记(3.1): ST算法

ST表 在RMQ(区间最值)问题中,著名的ST算法就是倍增的产物。ST算法可以在 \(O(n \log n)\) 的时间复杂度能预处理后,以 \(O(1)\) 的复杂度在线回答区间 [l, r] 内的最值。 当然,ST表不支持动态修改,如果需要动态修改,线段树是一种良好的解决方案,是 \(O(n)\

KD-Tree 学习笔记

学习资料: 1.B站 - 一只叫小花的猫 2.语雀 - 双愚:kdtree 3.B站视频:学习kdtree的前置知识:KNN算法 KD树简介与背景 k-d树,是一种分割k维数据空间的数据结构。主要应用于多维空间关键数据的搜索。关于kd树的背景,它主要是一种解决特征点匹配问题的算法,kd树就是一种高维

再谈23种设计模式(3):行为型模式(学习笔记)

行为型模式的关注点在于对象之间的通信和职责分配(描述结构模型中对象的动态特征)。行为型模式关注的是对象之间的交云和协作,即它们是如何相互作用的,以及如何分配职责和算法来完成任务。

VisionPro学习笔记(3)——BeadInspectTool

如果需要了解其他图像处理的文章,请移步小编的GitHub地址 传送门:请点击我 如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice VisionPro有很多的示例和算子,这里再展示一个最新出的算子Bead Inspect Tool

谱图论:Laplacian二次型和Markov转移算子

以下部分是我学习CMU 15-751: TCS Toolkit的课堂笔记。接下来将要介绍的是谱图论(spectral graph theory)的关键,也就是Laplacian二次型(Laplacian quadratic form)。直观地理解,Laplacian二次型刻画了图的“能量”(ener...

算法学习笔记(6): 树链剖分

树链剖分 树链剖分是一个很神奇,但是在树上可以完成一些区间操作问题 简单来说,就是把一棵树分成一条条的链,通过维护链上的信息来维护整棵树的信息 基础知识可以参考我的另外一篇博客:算法学习笔记(5): 最近公共祖先(LCA) 这里假设你已经掌握了上述博客中的所有相关知识,并清晰了其背后的原理 性质?发

算法学习笔记(11): 原根

原根 此文相对困难,请读者酌情食用 在定义原根之前,我们先定义其他的一点东西 阶 通俗一点来说,对于 $a$ 在模 $p$ 意义下的阶就是 $a^x \equiv 1 \pmod p$ 的最小正整数解 $x$ 或者说,$a$ 在模 $p$ 意义下生成子群的阶(群的大小) 再或者说,是 $a$ 在模

算法学习笔记(30):Kruskal 重构树

Kruskal 重构树 这是一种用于处理与最大/最小边权相关的一个数据结构。 其与 kruskal 做最小生成树的过程是类似的,我们考虑其过程: 按边权排序,利用并查集维护连通性,进行合并。 如果我们在合并时,新建一个节点,其权值为当前处理的边的权值,并将合并的两个节点都连向新建的节点,那么就可以得

C++算法之旅、09 力扣篇 | 常见面试笔试题(上)算法小白专用

算法学习笔记,记录容易忘记的知识点和难题。详解时空复杂度、50道常见面试笔试题,包括数组、单链表、栈、队列、字符串、哈希表、二叉树、递归、迭代、分治类型题目,均带思路与C++题解