乘法快速幂

例题:P1226 【模板】快速幂

给定三个整数 \(a,b,p\),求 \(a^b \bmod p\)\(0 \le a,b \le 2^{31}, \ a+b \gt 0, \ 2 \le p \lt 2^{31}\)

最朴素的想法是直接用一个循环,将 \(a\) 连乘 \(b\) 次,每次乘法后都对 \(p\) 取模。但题目给出的数据范围中,\(b\) 的值可以非常大,如果直接循环 \(b\) 次,计算量过大,会导致超时。

因此需要一种更高效的算法来处理大指数的幂运算,这就是快速幂算法。

快速幂的核心思想是二进制拆分,它将指数 \(b\) 拆解为二进制形式,从而显著减少乘法次数。例如,计算 \(a^{10}\),可以将指数 \(10\) 转换为二进制的 \(1010\)\(10 = 8 + 2 = 1 \cdot 2^3 + 0 \cdot 2^2 + 1 \cdot 2^1 + 0 \cdot 2^0\),所以,\(a^{10} = a^{8+2} = a^8 \cdot a^2\)

这样,就不需要计算 \(a^1, a^2, a^3, \dots, a^{10}\),而只需要计算 \(a^2, a^4, a^8\) 这些 \(a\)\(2^k\) 次幂。\(a^2\) 可以由 \(a^1 \cdot a^1\) 得到,\(a^4\) 可以由 \(a^2 \cdot a^2\) 得到,\(a^8\) 可以由 \(a^4 \cdot a^4\) 得到,这种方式可以很快地得到所有需要的 \(a^{2^k}\) 项。

最后,根据 \(b\) 的二进制表示中哪些位是 \(1\),将对应的 \(a^{2^k}\) 项乘起来即可。整个过程中的所有乘法都进行取模运算,以防止中间结果溢出。

参考代码
#include <cstdio>

/**
 * @brief 快速幂函数,计算 (x^y) % mod 的值
 * 
 * @param x 底数
 * @param y 指数
 * @param mod 模数
 * @return int 计算结果
 */
int quickpow(int x, int y, int mod) {
    // 初始化结果为 1。任何数的0次幂都是1,这是累乘的初始值。
    int res = 1;
    // 当指数 y 大于 0 时,循环处理
    while (y > 0) {
        // 判断 y 的二进制最低位是否为 1。
        // 如果是 1,说明当前位的权重需要乘入结果。
        if (y % 2 == 1) {
            // 将当前底数 x 乘到结果 res 中。
            // 使用 1ll 将 res 转换为 long long 类型,防止 res * x 的中间结果溢出 int。
            res = 1ll * res * x % mod;
        }
        // 底数自身平方,用于下一轮计算。
        // x 的值依次变为 a^1, a^2, a^4, a^8, ...
        // 同样使用 1ll 防止 x * x 的中间结果溢出。
        x = 1ll * x * x % mod;
        // 指数 y 右移一位(相当于除以 2),处理下一位。
        y /= 2;
    }
    // 返回最终计算结果
    return res;
}

int main()
{
    int a, b, p;
    // 读取输入的 a, b, p
    scanf("%d%d%d", &a, &b, &p);
    // 调用快速幂函数并按格式输出结果
    printf("%d^%d mod %d=%d\n", a, b, p, quickpow(a, b, p));
    return 0;
}

快速幂也可以通过递归函数来实现,这个思路建立在两个简单的数学式上:

  1. 如果指数 \(b\) 是偶数,那么 \(a^b = a^{b/2} \cdot a^{b/2} = (a^{b/2})^2\)
  2. 如果指数 \(b\) 是奇数,那么 \(a^{b-1} \cdot a = (a^{(b-1)/2})^2 \cdot a\)。由于 \(b/2\) 在整数除法中等于 \((b-1)/2\),所以也可以写成 \(a^b = (a^{b/2})^2 \cdot a\)

递归函数正是基于这个思想:

  • 递归的“递”过程:不断地将指数除以 \(2\),直到指数为 \(0\),这是递归的终止条件。
  • 递归的“归”过程:在从最深层返回时,根据当前指数的奇偶性,计算并返回当前层的结果。

这是一种分治的策略,将计算 \(a^b\) 的问题,在一次递归后缩小为计算 \(a^{b/2}\) 的问题,问题规模减半,因此算法的时间复杂度为 \(O(\log b)\),效率非常高。

参考代码
#include <cstdio>

/**
 * @brief 快速幂的递归实现,计算 (x^y) % mod 的值
 * 
 * @param x 底数
 * @param y 指数
 * @param mod 模数
 * @return int 计算结果
 */
int quickpow(int x, int y, int mod) {
    // 递归终止条件:任何数的 0 次幂都是 1
    if (y == 0) {
        return 1;
    }

    // 递归调用,计算 x^(y/2) % mod 的值
    // 通过将指数减半,不断缩小问题规模
    int tmp = quickpow(x, y / 2, mod);

    // 计算 (x^(y/2))^2 % mod 的值
    // 这是 y 为偶数时的结果
    // 使用 1ll 将 tmp 转换为 long long,防止 tmp * tmp 的中间结果溢出 int
    int res = 1ll * tmp * tmp % mod;

    // 如果 y 是奇数,还需要额外乘以一个 x
    // 因为 x^y = (x^(y/2))^2 * x
    if (y % 2 == 1) {
        res = 1ll * res * x % mod;
    }

    // 返回当前层计算的结果
    return res;
}

int main()
{
    int a, b, p;
    // 读取输入的 a, b, p
    scanf("%d%d%d", &a, &b, &p);
    // 调用快速幂函数并按格式输出结果
    printf("%d^%d mod %d=%d\n", a, b, p, quickpow(a, b, p));
    return 0;
}

选择题:现在用如下代码来计算 \(x^n\),其时间复杂度为?

double quick_power(double x, unsigned n) {
    if (n == 0) return 1;
    if (n == 1) return x;
    return quick_power(x, n / 2) * quick_power(x, n / 2) * ((n & 1) ? x : 1);
}
  • A. \(O(n)\)
  • B. \(O(1)\)
  • C. \(O(\log n)\)
  • D. \(O(n \log n)\)
答案

这道题的正确答案是 A

这是一个典型的递归问题,但代码的写法存在效率陷阱。

分析一下函数调用的过程:quick_power(x, n) 会调用两次 quick_power(x, n / 2),而每个 quick_power(x, n / 2) 又会调用两次 quick_power(n / 4)

以此类推,形成的递归过程大致如下(以 n=8 为例):

image

在每一层,函数调用的数量都翻倍。递归的深度是 \(O(\log n)\),但在最底层 \(n=1\) 时,总共大约有 \(n/2\) 个节点,整个递归的节点总数大约是 \(1+2+4 + \cdots + 2^{\log_2 n} \approx n\)

由于每次函数调用都包含常数时间的乘法操作,所以总的时间复杂度与调用次数成正比,即 \(O(n)\)


这是一种效率较低的快速幂实现,标准高效的快速幂算法会先计算一次 quick_power(x, n / 2) 并将其结果存入一个临时变量,然后复用这个结果。那样实现时间复杂度才是 \(O(\log n)\),因为这样每次只产生一个递归调用,递归深度为 \(O(\log n)\)

posted @ 2025-09-06 00:03  RonChen  阅读(22)  评论(0)    收藏  举报