平方求冪
在數學和程式設計中,平方求冪(英語: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).