平方求幂
在数学和程序设计中,平方求幂(英语:exponentiating by squaring)或快速幂是快速计算一个数(或更一般地说,一个半群的元素,如多项式或方阵)的大正整数乘幂的一般方法。这些算法可以非常通用,例如用在模算数或矩阵幂。对于通常使用加性表示法的半群,如密码学中使用的椭圆曲线,这种方法也称为double-and-add。
基本方法
该方法是基于观察到,对于正整数 ,可知
该方法使用指数的位(二进制的位,即bit,下文称为“位”)来确定计算哪些幂。
此例显示如何使用此方法计算 。 幂指数13的二进制为1101。这些位按照从左到右的顺序使用。 指数有4位,所以有4次迭代。
首先,将结果初始化为1:
- ,第1位 = 1,所以计算 。
- ,第2位 = 1,所以计算 。
- ,第3位 = 0,所以这一步什么都不做。
- ,第4位 = 1,所以计算 。
这可以按照下面的递归算法来实现:
Function exp_by_squaring(x, n)
if n < 0 then return exp_by_squaring(1 / x, -n);
else if n = 0 then return 1;
else if n = 1 then return x ;
else if n is even then return exp_by_squaring(x * x, n / 2);
else if n is odd then return x * exp_by_squaring(x * x, (n - 1) / 2);
尽管不是尾调用,但是通过引入辅助函数,该算法可以被重写成尾递归算法:
Function exp_by_squaring(x, n)
exp_by_squaring2(1, x, n)
Function exp_by_squaring2(y, x, n)
if n < 0 then return exp_by_squaring2(y, 1 / x, - n);
else if n = 0 then return y;
else if n = 1 then return x * y;
else if n is even then return exp_by_squaring2(y, x * x, n / 2);
else if n is odd then return exp_by_squaring2(x * y, x * x, (n - 1) / 2).
该算法的迭代版本的辅助空间是有界的,代码如下
Function exp_by_squaring_iterative(x, n)
if n < 0 then
x := 1 / x;
n := -n;
if n = 0 then return 1
y := 1;
while n > 1 do
if n is even then
x := x * x;
n := n / 2;
else
y := x * y;
x := x * x;
n := (n – 1) / 2;
return x * y
c++实现(非递归) 返回 对 求模
long long power(long long p, long long n)
{
long long ans = 1;
while (n)
{
if (n & 1) ans = (ans * p) % Mod;
p = p * p % Mod;
n >>= 1;
}
return ans;
}
计算复杂度
简要分析表明此算法用了 次平方,以及至多 次乘法,其中 表示向下取整函数。更确切地说,做乘法的次数比 的二进制展开的次数要少一次。对于 大于4左右的时候,这种算法在计算上就已经比天真地将它与自身重复地相乘更高效了。
每次平方的结果大约是前一次结果的两倍,因此,如果两个 位数的相乘的实现要进行 次计算(其中 为一固定值),那么计算 的复杂度为:
法
此算法先把指数展开成 形式,然后再计算 的值。它在1939年由Brauer首次提出。在下面的算法中,使用以下函数 和 ,其中 , 为奇数。
算法:
- 输入
- G 的一个元素 ,参数 ,一个非零整数 以及预计算的值 。
- 输出
- G 中的元素
y := 1; i := l-1 while i>=0 do (s,u) := f(ni) for j:=1 to k-s do y := y2 y := y*xu for j:=1 to s do y := y2 i := i-1 return y
为了获得最佳效率, 应该是满足
的最小整数。[1]
滑动窗口法
此方法是 法的更高效的变体。例如,要计算398次幂,二进制展开为 (110 001 110)2,采用长度为3的窗,使用 法,需要计算 1, 。 但也可以计算 1, ,这就会省下一次乘法,相当于是计算 (110 001 110)n2 的值
以下是一般算法:
算法:
- 输入
- G的元素 ,非负整数 ,一个参数 ,以及预计算的值 。
- 输出
- 元素
算法:
y := 1; i := l-1 while i > -1 do if ni=0 then y:=y2' i:=i-1 else s:=max{i-k+1,0} while ns=0 do s:=s+1 [notes 1] for h:=1 to i-s+1 do y:=y2 u:=(ni,ni-1,....,ns)2 y:=y*xu i:=s-1 return y
蒙哥马利阶梯法
求幂的许多算法都不提供对旁路攻击的防护。也就是说,监测到乘方运算的攻击者可以(部分)还原所计算的指数。就如众多公钥加密系统中那样,如果指数需要保密的话,这就是个问题了。一个叫做蒙哥马利阶梯[2]的方法解决了这个问题。
给定一个非零正整数的二进制展开 (其中 ),可以以下面方式计算 :
x1=x; x2=x2 for i=k-2 to 0 do If ni=0 then x2=x1*x2; x1=x12 else x1=x1*x2; x2=x22 return x1
该算法会执行一系列固定的操作(复杂度 ):无论每一位的具体值如何,指数中的每一位都会进行乘法和平方。
蒙哥马利阶梯法的这种具体实现还无法抵御缓存时序攻击:当根据秘密指数的位值访问不同的变量时,内存访问延迟仍可能被攻击者观察到。
固定基底的幂
当基底固定而指数变化时,可以使用几种方法来计算 。可以看出,预计算在这些算法中起着关键作用。
姚期智的方法
姚期智的方法与 法不同,是把指数以 为基底展开,并按上面的算法进行计算。令 , , , 为整数。
把指数 写成
- 其中对所有 都有
令 。该算法使用等式
给定 的元素 ,指数 写成上述形式,并且预先计算 的值,元素 就可以用下面的算法计算了。
y=1,u=1 and j=h-1 while j > 0 do for i=0 to w-1 do if ni=j then u=u*xbi y=y*u j=j-1 return y
如果令 , ,那么这些 就是 以 为基的每一位。姚期智的方法是把之前的那些 收集到 中,which appear to the highest power ;in the next round those with power are collected in as well etc. 变量 被原始的 乘了 次,内第二高的指数乘了 次…… 该算法计算 要用 次乘法,存储 个元素。[1]
欧几里得法
欧几里德法首先在P.D Rooij的《使用预计算和向量加法链的高效求幂》(Efficient exponentiation using precomputation and vector addition chains)中介绍。
这种计算群 G 中 ( 为自然数)的方法是,递归地使用下面的等式:
- , where
- (换句话说,用指数 与 的欧几里得除法来返回商 和余数 )。
给定群 G 中的基底元素 ,把指数 用姚期智的方法写出来,然后就可以用预计算的 个值 计算 了。
Begin loop Find , such that ; Find , such that ; Break loop if ; Let , and then let ; Compute recursively , and then let ; End loop; Return .
该算法首先在 中找到最大值,在找到集合 中的最大值。 然后递归求 的 次幂,把这个值乘以 ,赋值给 ;把 模 的结果赋值给 。
更多应用
这一思路还可用于计算大指数幂除以一个数的余数。特别是在密码学中,在整数除以q的余数的环中计算幂是很有用处的。在群中计算整数幂也是很有用的,使用法则
- Power(x, −n) = (Power(x, n))−1.
例如,如果使用朴素的方法,计算
- 13789722341 (mod 2345)
将花费很长时间以及大量的存储空间:计算 13789722341,并除以2345求余。即便使用更有效的方法也需要很长时间:求13789的平方,除以2345求余,将结果乘以13789,再求余,一直往下计算。这会需要 次模乘。
把“*”理解为 x*y = xy mod 2345(即相乘后取模)后,应用上面的平方求幂算法,只需要27次乘法和整数除法(所有这些都可以存储在单个机器字中)就可以完成计算了。
示例实现
通过2的幂进行计算
这是用Ruby写的上述算法的非递归实现。
由于低级语言会将 n=n/2 隐式向0取整,n=n-1 对那些语言来说,就是冗余的步骤了。 n[0]是n的二进制表示的最右边的位,所以如果它是1,则该数是奇数,如果它是零,则该数是偶数。它也是以2为模n的余数。
def power(x,n)
result = 1
while n.nonzero?
if n[0].nonzero?
result *= x
n -= 1
end
x *= x
n /= 2
end
return result
end
运行实例:计算 310
parameter x = 3 parameter n = 10 result := 1 Iteration 1 n = 10 -> n is even x := x2 = 32 = 9 n := n / 2 = 5 Iteration 2 n = 5 -> n is odd -> result := result * x = 1 * x = 1 * 32 = 9 n := n - 1 = 4 x := x2 = 92 = 34 = 81 n := n / 2 = 2 Iteration 3 n = 2 -> n is even x := x2 = 812 = 38 = 6561 n := n / 2 = 1 Iteration 4 n = 1 -> n is odd -> result := result * x = 32 * 38 = 310 = 9 * 6561 = 59049 n := n - 1 = 0 return result
运行实例:计算 310
result := 3 bin := "1010" Iteration for digit 2: result := result2 = 32 = 9 1010bin - Digit equals "0" Iteration for digit 3: result := result2 = (32)2 = 34 = 81 1010bin - Digit equals "1" --> result := result*3 = (32)2*3 = 35 = 243 Iteration for digit 4: result := result2 = ((32)2*3)2 = 310 = 59049 1010bin - Digit equals "0" return result
JavaScript-Demonstration: http://home.mnet-online.de/wzwz.de/temp/ebs/en.htm (页面存档备份,存于互联网档案馆)
幂的乘积的计算
平方求幂也可用于计算2个或多个幂的乘积。 如果基础群或半群是可交换的,那么常常可以通过同时计算乘积来减少乘法的次数。
例子
式子 a7×b5 可以分三步计算:
- ((a)2×a)2×a (计算 a7 需要四次乘法)
- ((b)2)2×b (计算 b5 需要三次乘法)
- (a7)×(b5) (计算二者乘积需要一次乘法)
所以总共需要八次乘法。
更快的解法是同时计算这两个幂
- ((a×b)2×a)2×a×b
总共只需要6次乘法。注意 a×b 计算了两次;结果可以在第一次计算后存储,这将乘法计数减少到5次。
有数字的例子:
- 27×35 = ((2×3)2×2)2×2×3 = (62×2)2×6 = 722×6 = 31,104
如果至少有两个指数大于1的话,同时计算幂就会比单独计算减少乘法次数。
使用变换
如果表达式在计算前进行变换,上面的例子 a7×b5 也可以只用5次乘法就计算出来:
a7×b5 = a2×(ab)5 其中 ab := a×b
- ab := a×b(一次乘法)
- a2×(ab)5 = ((ab)2×a)2×ab(四次乘法)
这个变换可以推广成下面的方案:
对于计算 aA×bB×...×mM×nN
首先:定义 ab := a×b, abc = ab×c, ...
然后:计算变换后的表达式 aA−B×abB−C×...×abc..mM−N×abc..mnN
在计算之前进行变换通常会减少乘法计数,但在某些情况下也会增加计数(请参见下面最后一个示例),因此在使用变换后的表达式进行计算之前,最好检查一下乘法的次数。
例子
对于下面的表达式,表中显示了分开计算每个幂,在不进行变换的情况下同时进行计算,以及在变换后同时进行计算的乘法次数。
例子 | a7×b5×c3 | a5×b5×c3 | a7×b4×c1 |
---|---|---|---|
分开计算 | [((a)2×a)2×a] × [((b)2)2×b] × [(c)2×c] (11次乘法) |
[((a)2)2×a] × [((b)2)2×b] × [(c)2×c] (10次乘法) |
[((a)2×a)2×a] × [((b)2)2] × [c] (8次乘法) |
同时计算 | ((a×b)2×a×c)2×a×b×c (8次乘法) |
((a×b)2×c)2×a×b×c (7次乘法) |
((a×b)2×a)2×a×c (6次乘法) |
变换 | a := 2 ab := a×b abc := ab×c (2次乘法) |
a := 2 ab := a×b abc := ab×c (2次乘法) |
a := 2 ab := a×b abc := ab×c (2次乘法) |
之后的计算 | (a×ab×abc)2×abc (4次乘法 ⇒ 总共6次) |
(ab×abc)2×abc (3次乘法 ⇒ 总共5次) |
(a×ab)2×a×ab×abc (5次乘法 ⇒ 总共7次) |
用有符号数字重新编码
在某些计算中,如果允许负系数(也就会需要用基底的倒数)的话,只要在 中计算倒数很快或者已经预先计算,求幂会更加高效。例如,当计算 时,二进制方法需要 次乘法和 次平方。不过可以用 次平方得到 ,然后乘以 得到 。
为此,定义以 为基数的整数 的有符号数字表示为
有符号二进制表示也就是选取 , 的表示法。记为 。有多种计算这种表示的方法。该表示不是唯一的,例如,取 。 和 给出了两个不同的有符号二进制表示,其中 表示 -1。由于在二进制方法中, 的基2表示的每个非零项都要计算乘法,因此感兴趣的是找到非零项数量最少的有符号二进制表示,即具有最小汉明重量的表示。有一种方法是计算非相邻形式(简称NAF)的有符号二进制表示,它满足对所有 , ,记为 。例如,478的NAF表示为 。这种表示总是有最小的汉明重量。下面的简单算法可以计算 的整数 的NAF表示:
for i = 0 to l - 1 do return
Koyama和Tsuruoka的另一种算法并不要求 这样的条件;它仍然可以让汉明重量最小化。
替代方法及推广
平方求幂可以看作是一个次优的加法链求幂算法:它通过由重复指数加倍(平方)和指数递增(乘以 )组成的加法链来计算指数。更一般地,如果允许任何先前计算的指数相加(通过乘以 的幂),有时可以让求幂运算的乘法次数更少(但通常使用更多的内存)。 时的最少次数:
- (平方求幂,6次乘法)
- (最优加法链,在复用 的情况下只需要5次乘法)
一般来说,求给定指数的最佳加法链是一个难题,因为没有已知的高效算法,所以最优链通常只用于小指数(比如,在编译器中已经预先存储了小指数幂的最佳链)。不过,有一些启发式算法,虽然不是最优的,但是由于额外的簿记工作和内存使用量的增加而导致的乘法次数少于平方求幂。无论如何,乘法的次数永远不会比 Θ(log n) 增长得更慢,所以这些算法只能减小平方求幂的渐进复杂度的常数因子。
参见
- 模幂运算
- 向量加法链
- 蒙哥马利算法
- 非相邻形式
- 加法链
注释
- ^ In this line, the loop finds the longest string of length less than or equal to 'k' which ends in a non zero value. And not all odd powers of 2 up to need be computed and only those specifically involved in the computation need be considered.
参考文献
- ^ 1.0 1.1 Cohen, H.; Frey, G. (编). Handbook of Elliptic and Hyperelliptic Curve Cryptography. Discrete Mathematics and Its Applications. Chapman & Hall/CRC. 2006. ISBN 9781584885184.
- ^ Montgomery, Peter L. Speeding the Pollard and Elliptic Curve Methods of Factorization (PDF). Math. Comput. 1987, 48 (177): 243–264 [2018-02-17]. (原始内容存档 (PDF)于2018-01-27).