逐比特校驗法-大整數開平方根 Bit-by-Bit Large Number Square Root

目錄

問題描述&背景

描述

給定一個大整數(十進制下可能長達數百位)N ,並保證其是某個整數的平方,如何精確又快速的求出其平方根?

背景

最近正在學習密碼學相關的一些問題,遇到了一個題目,其中需要對幾個巨大的整數(密碼學日常 -_-)開平方根。遇到巨大的整數,自然想Python大法,畢竟對大整數的支持確實很棒,幾乎無限位數的加法與乘法,運算速度也是巨快。沒想到在給大整數開平方根這個看似輕巧的問題上卡住了,於是經過一番搜索,找到了一個基於二分思想、非常快速的逐比特驗證方法,在這裏和大家總結分享一下。

逐比特校驗開平方根算法

參考來源

這篇文章比較全面總結了整型開根的各算法,包括精確解與近似的迭代法等:點我查看

二分思想

對大數做平方根時,如果直接做開根,大部分語言都會將其視作浮點型,從而帶來誤差,無法得到精確解;要想直接對大整型開根也是相當費時的操作。爲了精確解,便想到通過循環迭代不斷試錯,找到精確的那個解,其平方等於N ,因爲平方操作本質即乘法,是比較快速且準確的。
暴力迭代循環的次數過多,時間上一般不可忍受,於是可採用二分查找,顯著減少搜索時間。

更進一步-比特法校驗

二分查找仍然需要包含許多平方後比較的操作,運行速度仍然不盡人意。我們可以換一個思路:對於一個整數進行開平方根的操作,本質就是根據一個寫爲二進制後長l 位的整數N構造一個二進制長爲l2 位的整數sqrtN ,從高位到低位逐個比特驗證每一位是0還是1

bit-k k15 k14 k13 k12 k11 k10 k9 k8 k7 k6 k5 k4 k3 k2 k1 k0 Dec
N 1 0 1 0 1 0 1 0 1 0 1 0 0 0 0 1 43681
sqrtN 1 1 0 1 0 0 0 1 209

引言1:注意到假設sqrtN 的二進制長爲k 位,則其最小可能的數的平方爲N=(2k1)2=22k2 ,長爲2k1 位;最大可表示的數的平方爲N=(2k1)2=22k2k+1+1 ,長爲2k+1 位。故若輸入N 的二進制長爲l ,那麼其平方根的二進制長最多爲l2 位。所以我們只需判斷末尾l2 位即可。

引言2:假設sqrtN 最終能表示成某一二進制數,其在ki,kj,km,kn,i>j>m>n 位有值,則其平方將表示成

N=(2ki+2kj+2km+2kn)2
,展開得
(22ki)+(22kj+22ki2kj)+(22km+22ki2km+22kj2km)+(22kn+22ki2kn+22kj2kn+22km2kn)
2ki(2ki)+2kj(2kj+2ki+1)+2km(2km+2ki+1+2kj+1)+2kn(2kn+2ki+1+2kj+1+2km+1)
,注意這裏特地按特定規律分了組,並且假定最後結果有4位爲1以契合下面的例子,能講述得更加清楚一些。

算法過程

由於均是比特操作,有關2的次方和乘除的操作都可以通過左移操作實現,效率上會快很多。以上面的2092=43681 爲例,我們來逐步看一下算法流程:

  1. 初始時,輸入N = 43681。sqrtN爲0,shift162 = 8位。
  2. 查看所需看的最高位k8 ,設定一個mask = 100000000,即僅k8 位被置1的一個掩碼。假如sqrtNk8 位應該爲0的話,那麼N必定不應包含2k8(2k8) 一項。換言之,mask << shift一定超過了N。假如sqrtNk8 位應該爲1的話,那麼mask << shift一定 N。根據此來判斷得,k8 位應爲0。
  3. shift置爲7,對應mask = 10000000。此時sqrtN仍爲0,同上判斷知,k7 應爲1。更新sqrtN += mask,並且N要減去該項,此時N僅留下了
    2kj(2kj+2ki+1)+2km(2km+2ki+1+2kj+1)+2kn(2kn+2ki+1+2kj+1+2km+1)
    部分。
  4. shift置爲6,對應mask = 1000000。此時查看k6 位,因爲已知k7 應爲1,於是假如sqrtNk6 位應該爲0的話,那麼N必定不應包含2k6(2k6+2k7+1) 一項。換言之,此時的(sqrtN(即之前得到的2k7* 2 + mask) << shift一定超過了N。假如sqrtNk6 位應該爲1的話,那麼該式一定 N。根據此來判斷得,k6 位應爲1。更新sqrtN += mask,並且N要減去該項,此時N僅留下了
    2km(2km+2ki+1+2kj+1)+2kn(2kn+2ki+1+2kj+1+2km+1)
    部分。
  5. shift置爲5,對應mask = 100000。同樣得到k5 位應爲0。
  6. shift置爲4,對應mask = 10000。此時查看k4 位,因爲已知k7,k6 應爲1,於是假如sqrtNk4 位應該爲0的話,那麼N必定不應包含2k4(2k4+2k7+1+2k6+1) 一項。換言之,此時的(sqrtN(即之前得到的2k7+2k6* 2 + mask) << shift一定超過了N 。假如sqrtNk4 位應該爲1的話,那麼該式一定 N。根據此來判斷得,k4 位應爲1。更新sqrtN += mask,並且N要減去該項,此時N僅留下了
    2kn(2kn+2ki+1+2kj+1+2km+1)
    部分。
  7. 以此類推,直到N=0 停止。

相信經過上面的敘述,大家已經瞭解了這一算法的精髓:以二進制比特的視角,充分運用移位操作的快速性。

算法實現和驗證

Python實現

Python代碼實現該算法如下,還是非常簡潔的。

## Bit-by-Bit Large Number Fast sqrt
from math import ceil, sqrt

# Fast bit-by-bit sqrt func
def bit_by_bit_sqrt(N):
    # Initialize
    shift = ceil(len(bin(N)) - 2)
    mask = 1 << shift
    sqrtN = 0
    # Loop starting from MSB
    while N > 0:
        # Generate intermediate term
        tmp = ((sqrtN << 1) + mask) << shift
        # Just if sqrtN includes that bit
        if tmp <= N:
            sqrtN += mask   # If so, update
            N -= tmp        # sqrtN and N
        # Advance
        shift -= 1
        mask >>= 1
    # Return result
    return sqrtN

# Testing
if __name__ == '__main__':
    N = 43681
    sqrtN = bit_by_bit_sqrt(N)
    print('original N: %d' % N)
    print('calc sqrtN: %d' % sqrtN)
    print('true sqrtN: %d' % sqrt(N))

驗證結果

original N: 43681
calc sqrtN: 209
true sqrtN: 209

如果大家有任何疑問或評論,歡迎在評論區留言~ 有關非平方數精確開方和非整型開方相關的算法,也歡迎補充~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章