逐比特校验法-大整数开平方根 Bit-by-Bit Large Number Square Root

文章目录

    • 问题描述&背景
      • 描述
      • 背景
    • 逐比特校验开平方根算法
      • 参考来源
      • 二分思想
      • 更进一步-比特法校验
      • 算法过程
    • 算法实现和验证
      • Python实现
      • 验证结果

问题描述&背景

描述

给定一个大整数(十进制下可能长达数百位) N N N,并保证其是某个整数的平方,如何精确又快速的求出其平方根?

背景

最近正在学习密码学相关的一些问题,遇到了一个题目,其中需要对几个巨大的整数(密码学日常 -_-)开平方根。遇到巨大的整数,自然想Python大法,毕竟对大整数的支持确实很棒,几乎无限位数的加法与乘法,运算速度也是巨快。没想到在给大整数开平方根这个看似轻巧的问题上卡住了,于是经过一番搜索,找到了一个基于二分思想、非常快速的逐比特验证方法,在这里和大家总结分享一下。

逐比特校验开平方根算法

参考来源

这篇文章比较全面总结了整型开根的各算法,包括精确解与近似的迭代法等:点我查看

二分思想

对大数做平方根时,如果直接做开根,大部分语言都会将其视作浮点型,从而带来误差,无法得到精确解;要想直接对大整型开根也是相当费时的操作。为了精确解,便想到通过循环迭代不断试错,找到精确的那个解,其平方等于 N N N,因为平方操作本质即乘法,是比较快速且准确的。
暴力迭代循环的次数过多,时间上一般不可忍受,于是可采用二分查找,显著减少搜索时间。

更进一步-比特法校验

二分查找仍然需要包含许多平方后比较的操作,运行速度仍然不尽人意。我们可以换一个思路:对于一个整数进行开平方根的操作,本质就是根据一个写为二进制后长 l l l位的整数 N N N构造一个二进制长为 ⌈ l 2 ⌉ \lceil \frac{l}{2} \rceil 2l位的整数 s q r t N sqrtN sqrtN,从高位到低位逐个比特验证每一位是0还是1

bit- k k k k 15 k_{15} k15 k 14 k_{14} k14 k 13 k_{13} k13 k 12 k_{12} k12 k 11 k_{11} k11 k 10 k_{10} k10 k 9 k_9 k9 k 8 k_8 k8 k 7 k_7 k7 k 6 k_6 k6 k 5 k_5 k5 k 4 k_4 k4 k 3 k_3 k3 k 2 k_2 k2 k 1 k_1 k1 k 0 k_0 k0 Dec
N N N 1 0 1 0 1 0 1 0 1 0 1 0 0 0 0 1 43681
s q r t N sqrtN sqrtN 1 1 0 1 0 0 0 1 209

引言1:注意到假设 s q r t N sqrtN sqrtN的二进制长为 k k k位,则其最小可能的数的平方为 N = ( 2 k − 1 ) 2 = 2 2 k − 2 N = (2^{k-1})^2 = 2^{2k-2} N=(2k1)2=22k2,长为 2 k − 1 2k-1 2k1位;最大可表示的数的平方为 N = ( 2 k − 1 ) 2 = 2 2 k − 2 k + 1 + 1 N = (2^k - 1)^2 = 2^{2k} - 2^{k+1} + 1 N=(2k1)2=22k2k+1+1,长为 2 k + 1 2k+1 2k+1位。故若输入 N N N的二进制长为 l l l,那么其平方根的二进制长最多为 ⌈ l 2 ⌉ \lceil \frac{l}{2} \rceil 2l位。所以我们只需判断末尾 ⌈ l 2 ⌉ \lceil \frac{l}{2} \rceil 2l位即可。

引言2:假设 s q r t N sqrtN sqrtN最终能表示成某一二进制数,其在 k i , k j , k m , k n , i > j > m > n k_i, k_j, k_m, k_n, i > j > m > n ki,kj,km,kn,i>j>m>n位有值,则其平方将表示成 N = ( 2 k i + 2 k j + 2 k m + 2 k n ) 2 N = (2^{k_i} + 2^{k_j} + 2^{k_m} + 2^{k_n})^2 N=(2ki+2kj+2km+2kn)2,展开得 ( 2 2 k i ) + ( 2 2 k j + 2 ⋅ 2 k i ⋅ 2 k j ) + ( 2 2 k m + 2 ⋅ 2 k i ⋅ 2 k m + 2 ⋅ 2 k j ⋅ 2 k m ) + ( 2 2 k n + 2 ⋅ 2 k i ⋅ 2 k n + 2 ⋅ 2 k j ⋅ 2 k n + 2 ⋅ 2 k m ⋅ 2 k n ) (2^{2k_i}) + (2^{2k_j} + 2 \cdot 2^{k_i} \cdot 2^{k_j}) + (2^{2k_m} + 2 \cdot 2^{k_i} \cdot 2^{k_m} + 2 \cdot 2^{k_j} \cdot 2^{k_m}) + (2^{2k_n} + 2 \cdot 2^{k_i} \cdot 2^{k_n} + 2 \cdot 2^{k_j} \cdot 2^{k_n} + 2 \cdot 2^{k_m} \cdot 2^{k_n}) (22ki)+(22kj+22ki2kj)+(22km+22ki2km+22kj2km)+(22kn+22ki2kn+22kj2kn+22km2kn) 2 k i ⋅ ( 2 k i ) + 2 k j ⋅ ( 2 k j + 2 k i + 1 ) + 2 k m ⋅ ( 2 k m + 2 k i + 1 + 2 k j + 1 ) + 2 k n ⋅ ( 2 k n + 2 k i + 1 + 2 k j + 1 + 2 k m + 1 ) 2^{k_i} \cdot (2^{k_i}) + 2^{k_j} \cdot (2^{k_j} + 2^{k_i+1}) + 2^{k_m} \cdot (2^{k_m} + 2^{k_i+1} + 2^{k_j+1}) + 2^{k_n} \cdot (2^{k_n} + 2^{k_i+1} + 2^{k_j+1} + 2^{k_m+1}) 2ki(2ki)+2kj(2kj+2ki+1)+2km(2km+2ki+1+2kj+1)+2kn(2kn+2ki+1+2kj+1+2km+1),注意这里特地按特定规律分了组,并且假定最后结果有4位为1以契合下面的例子,能讲述得更加清楚一些。

算法过程

由于均是比特操作,有关2的次方和乘除的操作都可以通过左移操作实现,效率上会快很多。以上面的 20 9 2 = 43681 209^2 = 43681 2092=43681为例,我们来逐步看一下算法流程:

  1. 初始时,输入N = 43681。sqrtN为0,shift 16 2 \frac{16}{2} 216 = 8位。
  2. 查看所需看的最高位 k 8 k_8 k8,设定一个mask = 100000000,即仅 k 8 k_8 k8位被置1的一个掩码。假如sqrtN k 8 k_8 k8位应该为0的话,那么N必定不应包含 2 k 8 ⋅ ( 2 k 8 ) 2^{k_8} \cdot (2^{k_8}) 2k8(2k8)一项。换言之,mask << shift一定超过了N。假如sqrtN k 8 k_8 k8位应该为1的话,那么mask << shift一定 ≤ \le N。根据此来判断得, k 8 k_8 k8位应为0。
  3. shift置为7,对应mask = 10000000。此时sqrtN仍为0,同上判断知, k 7 k_7 k7应为1。更新sqrtN += mask,并且N要减去该项,此时N仅留下了 2 k j ⋅ ( 2 k j + 2 k i + 1 ) + 2 k m ⋅ ( 2 k m + 2 k i + 1 + 2 k j + 1 ) + 2 k n ⋅ ( 2 k n + 2 k i + 1 + 2 k j + 1 + 2 k m + 1 ) 2^{k_j} \cdot (2^{k_j} + 2^{k_i+1}) + 2^{k_m} \cdot (2^{k_m} + 2^{k_i+1} + 2^{k_j+1}) + 2^{k_n} \cdot (2^{k_n} + 2^{k_i+1} + 2^{k_j+1} + 2^{k_m+1}) 2kj(2kj+2ki+1)+2km(2km+2ki+1+2kj+1)+2kn(2kn+2ki+1+2kj+1+2km+1)部分。
  4. shift置为6,对应mask = 1000000。此时查看 k 6 k_6 k6位,因为已知 k 7 k_7 k7应为1,于是假如sqrtN k 6 k_6 k6位应该为0的话,那么N必定不应包含 2 k 6 ⋅ ( 2 k 6 + 2 k 7 + 1 ) 2^{k_6} \cdot (2^{k_6} + 2^{k_7+1}) 2k6(2k6+2k7+1)一项。换言之,此时的(sqrtN(即之前得到的 2 k 7 2^{k_7} 2k7* 2 + mask) << shift一定超过了N。假如sqrtN k 6 k_6 k6位应该为1的话,那么该式一定 ≤ \le N。根据此来判断得, k 6 k_6 k6位应为1。更新sqrtN += mask,并且N要减去该项,此时N仅留下了 2 k m ⋅ ( 2 k m + 2 k i + 1 + 2 k j + 1 ) + 2 k n ⋅ ( 2 k n + 2 k i + 1 + 2 k j + 1 + 2 k m + 1 ) 2^{k_m} \cdot (2^{k_m} + 2^{k_i+1} + 2^{k_j+1}) + 2^{k_n} \cdot (2^{k_n} + 2^{k_i+1} + 2^{k_j+1} + 2^{k_m+1}) 2km(2km+2ki+1+2kj+1)+2kn(2kn+2ki+1+2kj+1+2km+1)部分。
  5. shift置为5,对应mask = 100000。同样得到 k 5 k_5 k5位应为0。
  6. shift置为4,对应mask = 10000。此时查看 k 4 k_4 k4位,因为已知 k 7 , k 6 k_7, k_6 k7,k6应为1,于是假如sqrtN k 4 k_4 k4位应该为0的话,那么N必定不应包含 2 k 4 ⋅ ( 2 k 4 + 2 k 7 + 1 + 2 k 6 + 1 ) 2^{k_4} \cdot (2^{k_4} + 2^{k_7+1} + 2^{k_6+1}) 2k4(2k4+2k7+1+2k6+1)一项。换言之,此时的(sqrtN(即之前得到的 2 k 7 + 2 k 6 2^{k_7} + 2^{k_6} 2k7+2k6* 2 + mask) << shift一定超过了 N N N。假如 s q r t N sqrtN sqrtN k 4 k_4 k4位应该为1的话,那么该式一定 ≤ \le N。根据此来判断得, k 4 k_4 k4位应为1。更新sqrtN += mask,并且N要减去该项,此时N仅留下了 2 k n ⋅ ( 2 k n + 2 k i + 1 + 2 k j + 1 + 2 k m + 1 ) 2^{k_n} \cdot (2^{k_n} + 2^{k_i+1} + 2^{k_j+1} + 2^{k_m+1}) 2kn(2kn+2ki+1+2kj+1+2km+1)部分。
  7. 以此类推,直到 N = 0 N = 0 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

如果大家有任何疑问或评论,欢迎在评论区留言~ 有关非平方数精确开方和非整型开方相关的算法,也欢迎补充~

你可能感兴趣的:(算法理论)