← 返回题目列表

模幂运算为什么用分治快速幂?如何避免中间结果溢出?

中等 第 17 / 23 题 更新于 2026/07/31
分治快速幂取模

简化版

模幂 a^b mod m 可以用快速幂,把指数每次减半。

如果 b 是偶数,a^b = (a^(b/2))^2;如果 b 是奇数,额外乘一个 a

每次乘法后都取模,能控制数值大小,避免中间结果过大。

详细版

快速幂本质是分治:把指数 b 的问题拆成指数 b/2 的问题。

迭代写法更常见:

当 b 的当前二进制位为 1,把当前 base 乘进答案
每轮 base = base * base mod m
b 右移一位

复杂度从普通连乘的 O(b) 降到 O(log b)

在 JavaScript 中,如果数值很大,需要用 BigInt 或专门的乘法取模技巧,否则 Number 精度可能不够。

完整版教学

一、为什么普通连乘太慢

计算 a^b mod m 如果直接乘 b 次,指数很大时不可接受。例如 b = 1,000,000,000,逐次乘法要 10 亿轮。快速幂利用指数的二进制结构,把乘法次数降到 log b 级别,大约只需要 30 轮。

记忆钩子:快速幂不是少乘一点,而是把指数按二进制折半处理。

二、分治公式怎么来

b 是偶数:

a^b = a^(b/2) * a^(b/2)

b 是奇数:

a^b = a * a^(b-1)

所以递归可以先求一半,再平方;迭代可以按二进制位累积答案。这就是分治在指数问题上的体现。

三、为什么每步都能取模

模运算满足乘法兼容性:

(x * y) mod m = ((x mod m) * (y mod m)) mod m

因此每次乘完立即取模不会改变最终答案,却能让中间值保持在较小范围。这个性质是模幂能够高效计算的数学基础。

操作是否影响最终模值好处
最后统一取模不影响数学结果中间数可能巨大
每步乘后取模不影响数学结果控制数值大小

实际编程通常选择每步取模。

四、迭代代码模板

普通整数范围内可以这样写:

function modPow(a, b, mod) {
  let ans = 1 % mod
  let base = a % mod
  while (b > 0) {
    if (b % 2 === 1) ans = (ans * base) % mod
    base = (base * base) % mod
    b = Math.floor(b / 2)
  }
  return ans
}

如果输入可能超过安全整数范围,JavaScript 需要使用 BigInt

ans = (ans * base) % mod

其中 ans/base/mod 都应是 BigInt 类型。

五、带数字推演

计算 3^13 mod 713 的二进制是 1101

ans=1, base=3, b=13 -> 位为1,ans=3
base=2, b=6
base=4, b=3 -> 位为1,ans=5
base=2, b=1 -> 位为1,ans=3

最终答案是 3。只用了少量平方和乘法。

六、常见误区与追问

  • 误区:先算 a^b 再取模。 中间结果可能极大,甚至溢出或失去精度。
  • 误区:奇数指数处理后忘记继续平方 base。 迭代每轮都要推进 base 和指数。
  • 误区:JavaScript Number 随便乘大整数。 超过安全整数会有精度问题,应考虑 BigInt。
  • 追问:复杂度为什么是 O(log b)? 指数每轮除以 2,相当于处理二进制位数。
  • 追问:mod 为 1 怎么办? 所有数对 1 取模都是 0,初始化 1 % mod 能覆盖。

这些问题考的是数学性质和实现细节。

七、加强记忆

模幂记成“指数折半,乘完取模”。偶数指数平方半幂,奇数指数多乘一个底数;迭代时看二进制位,位为 1 就把当前 base 乘进答案。每一步都取模,既保证数学等价,又避免中间结果膨胀。大整数场景要额外注意语言精度。