算法基础篇:(十二)基础算法之倍增思想:从快速幂到大数据运算优化

目录

前言

[一、什么是倍增思想?------ 从 "一步步走" 到 "跳着走"](#一、什么是倍增思想?—— 从 “一步步走” 到 “跳着走”)

[1.1 倍增思想的核心本质](#1.1 倍增思想的核心本质)

[1.2 倍增思想的数学基础](#1.2 倍增思想的数学基础)

[1.3 倍增思想的适用场景](#1.3 倍增思想的适用场景)

[二、倍增思想的入门实践:快速幂(O (log b) 求 a^b mod p)](#二、倍增思想的入门实践:快速幂(O (log b) 求 a^b mod p))

[2.1 问题引入:为什么需要快速幂?](#2.1 问题引入:为什么需要快速幂?)

[2.2 快速幂的原理:二进制分解 + 倍增计算](#2.2 快速幂的原理:二进制分解 + 倍增计算)

[2.3 快速幂的 C++ 实现(递归 + 迭代版)](#2.3 快速幂的 C++ 实现(递归 + 迭代版))

[2.3.1 递归版快速幂](#2.3.1 递归版快速幂)

[2.3.2 迭代版快速幂(推荐)](#2.3.2 迭代版快速幂(推荐))

[2.4 快速幂的边界情况处理](#2.4 快速幂的边界情况处理)

[三、倍增思想的延伸:大整数乘法取模(a×b mod p)](#三、倍增思想的延伸:大整数乘法取模(a×b mod p))

[3.1 问题引入:为什么需要大整数乘法取模?](#3.1 问题引入:为什么需要大整数乘法取模?)

[3.2 大整数乘法取模的原理:倍增 + 加法取模](#3.2 大整数乘法取模的原理:倍增 + 加法取模)

[3.3 大整数乘法取模的 C++ 实现(快速乘)](#3.3 大整数乘法取模的 C++ 实现(快速乘))

[3.4 快速幂 + 快速乘:解决超大数幂运算取模](#3.4 快速幂 + 快速乘:解决超大数幂运算取模)

总结


前言

在算法世界中,有一类思想如同 "四两拨千斤" 的智慧 ------ 它通过将问题规模 "翻倍增长" 的方式,把原本复杂的运算转化为高效的分步处理,这就是倍增思想。从基础的快速幂运算,到超大数乘法取模,再到后续可能延伸的 LCA(最近公共祖先)、RMQ(区间最值查询)等高级应用,倍增思想始终以 "高效、简洁" 的核心优势,成为程序员必须掌握的核心算法思想之一。

本文将从倍增思想的本质出发,结合数学原理、具体案例和 C++ 实战代码,由浅入深地拆解倍增思想的应用场景。无论你是算法入门的新手,还是寻求进阶优化的开发者,都能通过本文彻底理解倍增思想的底层逻辑,并灵活运用到实际编程中。下面就让我们正式开始吧!


一、什么是倍增思想?------ 从 "一步步走" 到 "跳着走"

1.1 倍增思想的核心本质

倍增,顾名思义,就是 "每次增加一倍"。它的核心思想是:通过预先计算出问题的 "2^k 倍" 解,在实际求解时,将复杂问题分解为若干个 "2^k 倍" 的子问题,从而快速合并得到最终答案

举个生活中的例子:如果我们要从 1 数到 1000,最朴素的方法是逐个累加(1→2→3→...→1000),需要 1000 次操作;而用倍增思想,我们可以先数到 2(1×2)、再到 4(2×2)、8(4×2)、...、512(2^9),再通过组合这些 "倍增步长"(512+256+128+64+32+8),仅需少数几次操作就能到达 1000。

在算法中,倍增思想的核心价值在于将时间复杂度从 O (n) 优化到 O (log n) 。这种优化在处理大数据(如 10^18 级别)时,几乎是不可或缺的 ------ 毕竟,O (n) 的算法在 n=10^18 时完全无法运行,而 O (log n) 仅需 60 次左右操作就能完成。

1.2 倍增思想的数学基础

倍增思想的数学支撑是二进制分解。任何一个正整数 n,都可以唯一分解为若干个不重复的 2 的幂次之和。例如:

  • 11 = 8 + 2 + 1 = 2³ + 2¹ + 2⁰
  • 100 = 64 + 32 + 4 = 2⁶ + 2⁵ + 2²
  • 1000 = 512 + 256 + 128 + 64 + 32 + 8 = 2⁹ + 2⁸ + 2⁷ + 2⁶ + 2⁵ + 2³

这种分解的优势在于:每个 "2^k" 项都是前一项的 2 倍,我们可以通过 "自乘" 快速计算出所有需要的 "2^k" 项,再根据目标数的二进制表示,选择性地将这些项组合起来,得到最终结果。

1.3 倍增思想的适用场景

倍增思想并非万能,但在以下场景中能发挥巨大作用:

  1. 幂运算 / 乘法运算:如快速幂(a^b mod p)、大整数乘法取模(a×b mod p,a、b 达 10^18 级别);
  2. 区间查询:如 RMQ(区间最值查询)、区间和查询等,通过预处理倍增数组实现 O (1) 或 O (log n) 查询;
  3. 树形结构:如 LCA(最近公共祖先),通过倍增预处理父节点信息,快速找到两个节点的公共祖先;
  4. 字符串处理:如 KMP 算法的优化、后缀数组的构建等;
  5. 其他大数据场景:如寻找一个数的约数、求解递推数列的第 n 项等。

本文将重点讲解倍增思想在快速幂大整数乘法取模中的应用(这是最基础也是最常用的场景),后续会延伸到其他高级应用。

二、倍增思想的入门实践:快速幂(O (log b) 求 a^b mod p)

2.1 问题引入:为什么需要快速幂?

在编程中,我们经常遇到 "计算 a 的 b 次幂对 p 取模" 的需求(例如密码学、数论问题、组合数学等场景)。如果直接使用朴素算法:

cpp 复制代码
// 朴素幂运算:O(b)时间复杂度
long long pow_naive(long long a, long long b, long long p) {
    long long res = 1;
    for (int i = 0; i < b; i++) {
        res = res * a % p;
    }
    return res;
}

当 b 的取值较大(如 b=10^18)时,这个算法会直接超时 ------ 因为 10^18 次循环是计算机无法在合理时间内完成的。

而快速幂算法通过倍增思想,将时间复杂度优化到 O (log b),即使 b=10^18,也仅需 60 次左右循环就能完成计算,效率提升巨大。

2.2 快速幂的原理:二进制分解 + 倍增计算

快速幂的核心思路的是将 b 分解为二进制,再通过倍增计算 a 的 2^k 次幂,最后将对应位为 1 的项相乘

以计算 3^11 mod 9 为例,步骤如下:

  1. 分解 b=11 的二进制:11 = 8 + 2 + 1 = 2³ + 2¹ + 2⁰,二进制表示为 1011;
  2. 倍增计算 a 的 2^k 次幂:
    • 2⁰次幂:3^1 = 3
    • 2¹ 次幂:3^2 = (3^1) × (3^1) = 9
    • 2² 次幂:3^4 = (3^2) × (3^2) = 81
    • 2³ 次幂:3^8 = (3^4) × (3^4) = 6561
  3. 筛选二进制中为 1 的项相乘:3^11 = 3^8 × 3^2 × 3^1;
  4. 每一步相乘后取模(避免溢出):(6561 × 9 × 3) mod 9 = (6561 mod 9) × (9 mod 9) × (3 mod 9) mod 9 = 0 × 0 × 3 mod 9 = 0?不对,重新计算:实际计算时,3^2=9 mod9=0,所以 3^11=3^8 × 3^2 × 3^1 = (3^8 mod9) × (0) × (3 mod9) = 0,结果正确。

再举一个示例:计算 2^10 mod9:

  1. 分解 b=10 的二进制:10=8+2=2³+2¹,二进制为 1010;
  2. 倍增计算:
    • 2^0 次幂:2^1=2
    • 2^1 次幂:2^2=4
    • 2^2 次幂:2^4=16
    • 2^3 次幂:2^8=256
  3. 相乘取模:(256 ×4) mod9 = 1024 mod9。因为 9×113=1017,1024-1017=7,所以结果为 7,与示例输出一致。

2.3 快速幂的 C++ 实现(递归 + 迭代版)

快速幂有两种常见实现方式:递归版(思路清晰)和迭代版(效率更高,无栈溢出风险)。

2.3.1 递归版快速幂

递归版的核心是 "分而治之":将 a^b 分解为 (a^(b/2))²,如果 b 是奇数,则再乘以 a。

cpp 复制代码
#include <iostream>
using namespace std;
typedef long long LL;

// 递归版快速幂:计算 (a^b) mod p
LL quick_pow_recursive(LL a, LL b, LL p) {
    // 递归终止条件:b=0时,任何数的0次幂都是1
    if (b == 0) return 1 % p;
    // 递归计算 a^(b/2) mod p
    LL mid = quick_pow_recursive(a, b >> 1, p);
    // 合并结果:(a^(b/2))^2 mod p
    LL res = mid * mid % p;
    // 如果b是奇数,需要再乘以a mod p
    if (b & 1) res = res * a % p;
    return res;
}

int main() {
    LL a, b, p;
    cin >> a >> b >> p;
    LL result = quick_pow_recursive(a, b, p);
    printf("%lld^%lld mod %lld=%lld\n", a, b, p, result);
    return 0;
}

优点:代码简洁,思路和二进制分解的逻辑高度一致,容易理解;

缺点:当 b 较大时(如 10^18),递归深度会达到 log2 (10^18)≈60,虽然不会栈溢出,但递归调用的开销比迭代版略大。

2.3.2 迭代版快速幂(推荐)

迭代版通过循环遍历 b 的二进制位,直接计算倍增项并累加结果,效率更高,是实际编程中最常用的版本。

cpp 复制代码
#include <iostream>
using namespace std;
typedef long long LL;

// 迭代版快速幂:计算 (a^b) mod p
LL quick_pow_iterative(LL a, LL b, LL p) {
    LL res = 1 % p;  // 初始化结果为1 mod p(处理p=1的特殊情况)
    a = a % p;       // 先将a对p取模,减少后续计算量
    while (b > 0) {
        // 如果当前b的最低位是1,将当前a乘到结果中
        if (b & 1) {
            res = res * a % p;
        }
        // 倍增a:a = a^2 mod p(对应下一个二进制位)
        a = a * a % p;
        // b右移一位,处理下一个二进制位
        b >>= 1;
    }
    return res;
}

int main() {
    LL a, b, p;
    cin >> a >> b >> p;
    LL result = quick_pow_iterative(a, b, p);
    printf("%lld^%lld mod %lld=%lld\n", a, b, p, result);
    return 0;
}

代码解析

  1. 初始化 res=1% p:处理 p=1 的特殊情况(任何数 mod1 都是 0);
  2. a=a% p:提前对 a 取模,避免 a 过大导致的溢出(尤其是当 a 本身大于 p 时);
  3. 循环遍历 b 的二进制位:
    • b&1:判断当前 b 的最低位是否为 1(如果是,说明需要将当前的 a 乘到结果中);
    • a = a*a%p:倍增 a,从 a^1→a^2→a^4→a^8→...;
    • b>>=1:b 右移一位,相当于除以 2,处理下一个二进制位。

测试用例:输入:2 10 9输出:2^10 mod 9=7与示例结果一致,证明代码正确性。

2.4 快速幂的边界情况处理

在实际使用中,需要注意以下边界情况,避免代码出错:

  1. p=1:任何数 mod1 都是 0,直接返回 0;
  2. a=0:当 b=0 时,0^0 无意义(通常题目会避免这种情况),当 b>0 时,0^b mod p=0;
  3. b=0:任何非零数的 0 次幂都是 1,所以返回 1 mod p;
  4. 数据溢出 :由于 a 和 b 可能很大(如 10^18),直接相乘会导致 64 位整数溢出。解决方案:
    • 提前对 a 取模(a=a% p);
    • 使用 "快速乘" 优化(下文会讲),将乘法转化为加法,避免溢出。

相关的题目链接如下:https://www.luogu.com.cn/problem/P1226

三、倍增思想的延伸:大整数乘法取模(a×b mod p)

3.1 问题引入:为什么需要大整数乘法取模?

当 a 和 b 的取值范围达到 10^18 级别时,即使使用 64 位整数(long long),直接计算 a×b 也会发生溢出 ------ 因为 10^18 ×10^18=10^36,远超 long long 的最大值(约 9×10^18)。

此时,我们需要一种高效的方法来计算 (a×b) mod p,而无需直接计算 a×b 的完整结果。这就是**"大整数乘法取模"** 问题,其解决方案同样基于倍增思想。

3.2 大整数乘法取模的原理:倍增 + 加法取模

大整数乘法取模的核心思路是将乘法转化为加法,通过倍增累加的方式计算结果,本质上和快速幂的逻辑一致:

  1. 将 b 分解为二进制:b = 2^k1 + 2^k2 + ... + 2^km
  2. 倍增计算 a×2^k mod p(即 a、2a、4a、8a、... mod p);
  3. 将对应二进制位为 1 的项累加,每次累加后取模,得到最终结果。

以计算 3×11 mod9 为例:

  1. 分解 b=11 的二进制:11=8+2+1=2³+2¹+2⁰;
  2. 倍增计算 a×2^k mod9:
    • 2^0:3×1=3 mod9=3
    • 2^1:3×2=6 mod9=6
    • 2^2:3×4=12 mod9=3
    • 2^3:3×8=24 mod9=6
  3. 累加对应项:3+6+6=15 mod9=6,结果正确(3×11=33 mod9=6)。

3.3 大整数乘法取模的 C++ 实现(快速乘)

这种方法也被称为**"快速乘"(Quick Multiplication)**,其迭代实现如下:

cpp 复制代码
#include <iostream>
using namespace std;
typedef long long LL;

// 快速乘:计算 (a * b) mod p,避免溢出
LL quick_mul(LL a, LL b, LL p) {
    LL res = 0 % p;  // 初始化结果为0(加法累加的初始值)
    a = a % p;       // 提前对a取模,减少计算量
    while (b > 0) {
        // 如果当前b的最低位是1,将当前a加到结果中
        if (b & 1) {
            res = (res + a) % p;
        }
        // 倍增a:a = a*2 mod p(对应下一个二进制位)
        a = (a + a) % p;
        // b右移一位,处理下一个二进制位
        b >>= 1;
    }
    return res;
}

int main() {
    LL a, b, p;
    cin >> a >> b >> p;
    LL result = quick_mul(a, b, p);
    printf("%lld * %lld mod %lld=%lld\n", a, b, p, result);
    return 0;
}

代码解析

  1. 初始化 res=0% p:加法累加的初始值,处理 p=1 的特殊情况;
  2. a=a% p:提前对 a 取模,避免 a 过大导致的溢出;
  3. 循环遍历 b 的二进制位:
    • b&1:判断当前 b 的最低位是否为 1(如果是,将当前的 a 加到结果中);
    • a = (a+a)%p:倍增 a,从 a→2a→4a→8a→...(本质是 a×2^k);
    • b>>=1:b 右移一位,处理下一个二进制位。

测试用例

输入:2 3 9

输出:2*3 mod9=6(正确);

输入:10^18 10^18 1e9+7(假设输入为 1000000000000000000 1000000000000000000 1000000007),代码能正常计算结果,无溢出。

3.4 快速幂 + 快速乘:解决超大数幂运算取模

当 a 和 b 都达到 10^18 级别时,快速幂中的res * a % p也会发生溢出。此时,我们可以用快速乘替代普通乘法,实现 "双重倍增",彻底避免溢出。

组合后的代码如下:

cpp 复制代码
#include <iostream>
using namespace std;
typedef long long LL;

// 快速乘:计算 (a * b) mod p,避免溢出
LL quick_mul(LL a, LL b, LL p) {
    LL res = 0 % p;
    a = a % p;
    while (b > 0) {
        if (b & 1) {
            res = (res + a) % p;
        }
        a = (a + a) % p;
        b >>= 1;
    }
    return res;
}

// 快速幂(结合快速乘):计算 (a^b) mod p,支持超大数
LL quick_pow(LL a, LL b, LL p) {
    LL res = 1 % p;
    a = a % p;
    while (b > 0) {
        if (b & 1) {
            res = quick_mul(res, a, p);  // 用快速乘替代普通乘法
        }
        a = quick_mul(a, a, p);        // 用快速乘替代普通乘法
        b >>= 1;
    }
    return res;
}

int main() {
    // 测试:计算 (1e18 ^ 1e18) mod (1e9+7)
    LL a = 1e18, b = 1e18, p = 1e9 + 7;
    LL result = quick_pow(a, b, p);
    printf("%lld^%lld mod %lld=%lld\n", a, b, p, result);
    return 0;
}

核心改进 :将快速幂中的res * a % pa * a % p替换为quick_mul(res, a, p)quick_mul(a, a, p),彻底解决了超大数乘法的溢出问题。

相关的题目链接如下:https://www.luogu.com.cn/problem/P10446


总结

倍增思想是算法优化中的 "利器",它看似简单,却能解决一系列大数据场景下的复杂问题。从快速幂到快速乘,再到 RMQ、LCA 等高级应用,倍增思想的核心逻辑始终如一 ------ 通过 "跳着走" 的方式,将复杂问题分解为高效的子问题。

本文通过数学原理、案例解析和 C++ 代码实现,详细讲解了倍增思想的基础应用。希望你在学习后,能够熟练掌握快速幂和快速乘的模板,并尝试将倍增思想运用到更多场景中。算法学习的核心是 "理解思想,灵活运用",而不是死记硬背模板。只有真正理解了倍增思想的底层逻辑,才能在面对不同问题时,快速想到对应的优化方案。

最后,建议你多做实战题目,通过练习巩固所学知识。如果在学习过程中遇到问题,可以回头再看本文的思路解析,或者查阅相关题解,不断加深对倍增思想的理解。祝你在算法的道路上越走越远!

相关推荐
Murphy_lx1 小时前
C++ 条件变量
linux·开发语言·c++
xie0510_1 小时前
C++入门
c++
AA陈超1 小时前
ASC学习笔记0027:直接设置属性的基础值,而不会影响当前正在生效的任何修饰符(Modifiers)
c++·笔记·学习·ue5·虚幻引擎
羚羊角uou1 小时前
【C++】智能指针
开发语言·c++
杜子不疼.1 小时前
【C++】哈希表基础:开放定址法 & 什么是哈希冲突?
c++·哈希算法·散列表
CoovallyAIHub1 小时前
分割万事万物的AI,再进化!Meta SAM 3 来了,支持中文提示词!
深度学习·算法·计算机视觉
九年义务漏网鲨鱼1 小时前
蓝桥杯算法——记忆化搜索
算法·职场和发展·蓝桥杯
武子康1 小时前
大数据-159 Apache Kylin Cube 实战:Hive 装载与预计算加速(含 Cuboid/实时 OLAP,Kylin 4.x)
大数据·后端·apache kylin
04aaaze1 小时前
C++(C转C++)
c语言·c++·算法