平方根的快速逼近?

时间:2014-01-07 16:47:33

标签: c# biginteger approximation square-root

我正在编写一个正在编写的程序中进行一些计算。虽然我的问题可能看起来模糊不清,也许是可以避免的,但事实并非如此,即使是这样,我现在也不想避开它:-D

问题是我有一个巨大的数字(数千位,在某些时候可能有数百万),我需要找到一个近似的SQRT。我正在使用System.Numerics.BigInteger,我需要近似值小于或等于真实的SQRT。例如,如果提供的数字是100,我会满意7,8,9或10,但不满11。

当前代码:

public static BigInteger IntegerSqrt(BigInteger n)
{
    var oldValue = new BigInteger(0);
    BigInteger newValue = n;

    while (BigInteger.Abs(newValue - oldValue) >= 1)
    {
        oldValue = newValue;
        newValue = (oldValue + (n / oldValue)) / 2;
    }

    return newValue;
}

虽然这给了我正确的答案(据我所知),但它正试图得到一个精确的答案,这是疯狂的慢。

如果获得立方根,或其他任何类似的东西会更快,我会对此感到高兴。而且,是的,我知道快速找到平方根可以赚到很多钱,不是我想做的事情,我只是想快速近似...

你能给我的任何帮助都会很棒!


编辑 - Gabe

这是我正在使用的更新的IntegerSqrt函数,它似乎不会更快。

public static BigInteger IntegerSqrt(BigInteger n)
{
    var oldValue = n.RoughSqrt();
    BigInteger newValue = n;
    while (BigInteger.Abs(newValue - oldValue) >= 1)
    {
        oldValue = newValue;
        newValue = (oldValue + (n / oldValue)) / 2;
    }

    return newValue;
}


修改2 这是你在想什么? - 我在样本大数(50k数字)上运行了30分钟,并且在没有完成的情况下循环了大约100次。我觉得好像错过了什么......

public static BigInteger IntegerSqrt(BigInteger n)
{
    var oldValue = new BigInteger(0);
    BigInteger newValue = n.RoughSqrt();
    int i = 0;
    while (BigInteger.Abs(newValue - oldValue) >= 1)
    {
        oldValue = newValue;
        newValue = (oldValue + (n / oldValue)) / 2;
        i++;
    }
    return newValue;
}

6 个答案:

答案 0 :(得分:4)

使用我的回答here来计算Log 2 (N),我准备了一个测试用例。

虽然您的代码更接近实际平方根,但当输入数字变大时,我的测试显示数百万次加速

我认为您必须在更精确的结果数千/数百万次加速之间做出决定,

Random rnd = new Random();
var bi = new BigInteger(Enumerable.Range(0, 1000).Select(x => (byte)rnd.Next(16))
                                                 .ToArray());


var sqrt1 = bi.Sqrt().ToString();
var sqrt2 = IntegerSqrt(bi).ToString();

var t1 = Measure(10 * 200000, () => bi.Sqrt());
var t2 = Measure(10, () => IntegerSqrt(bi));

BigInteger.Sqrt

的扩展方法
public static class BigIntegerExtensions
{
    static int[] PreCalc = new int[] { 8, 7, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };
    static int Log2(this BigInteger bi)
    {
        byte[] buf = bi.ToByteArray();
        int len = buf.Length;
        return len * 8 - PreCalc[buf[len - 1]] - 1;
    }

    public static BigInteger Sqrt(this BigInteger bi)
    {
        return new BigInteger(1) << (Log2(bi) / 2);
    }
}

测量速度的代码

long Measure(int n,Action action)
{
    action(); 
    var sw = Stopwatch.StartNew();
    for (int i = 0; i < n; i++)
    {
        action();
    }
    return sw.ElapsedMilliseconds;
}

<强>结果:

在我的计算机中,两个代码都会在8秒内生成,但我发布的代码运行10*200000次,比较代码只运行10次。 (是的,很难相信,对于8000位数字,差异是200,000次)

当与16000位数进行比较时,差异会增加到1,000,000次......

答案 1 :(得分:3)

这是粗略的近似值,可以让你在平方根的2倍范围内:

System.Numerics.BigInteger RoughSqrt(System.Numerics.BigInteger x)
{
    var bytes = x.ToByteArray();    // get binary representation
    var bits = (bytes.Length - 1) * 8;  // get # bits in all but msb
    // add # bits in msb
    for (var msb = bytes[bytes.Length - 1]; msb != 0; msb >>= 1)
        bits++;
    var sqrtBits = bits / 2;    // # bits in the sqrt
    var sqrtBytes = sqrtBits / 8 + 1;   // # bytes in the sqrt
    // avoid making a negative number by adding an extra 0-byte if the high bit is set
    var sqrtArray = new byte[sqrtBytes + (sqrtBits % 8 == 7 ? 1 : 0)];
    // set the msb
    sqrtArray[sqrtBytes - 1] = (byte)(1 << (sqrtBits % 8));
    // make new BigInteger from array of bytes
    return new System.Numerics.BigInteger(sqrtArray);
}

不是以n作为近似值(newValue = n;)开始,而是先调用此(newValue = RoughSqrt(n);)。这将以更少的迭代次数为您提供答案。

如果你想取第N个根,你只需要改变粗略近似值以使用1 / N的位并改变牛顿方法以使用x^N的导数:

static BigInteger RoughRoot(BigInteger x, int root)
{
    var bytes = x.ToByteArray();    // get binary representation
    var bits = (bytes.Length - 1) * 8;  // get # bits in all but msb
    // add # bits in msb
    for (var msb = bytes[bytes.Length - 1]; msb != 0; msb >>= 1)
        bits++;
    var rtBits = bits / root + 1;   // # bits in the root
    var rtBytes = rtBits / 8 + 1;   // # bytes in the root
    // avoid making a negative number by adding an extra 0-byte if the high bit is set
    var rtArray = new byte[rtBytes + (rtBits % 8 == 7 ? 1 : 0)];
    // set the msb
    rtArray[rtBytes - 1] = (byte)(1 << (rtBits % 8));
    // make new BigInteger from array of bytes
    return new BigInteger(rtArray);
}

public static BigInteger IntegerRoot(BigInteger n, int root)
{
    var oldValue = new BigInteger(0);
    var newValue = RoughRoot(n, root);
    int i = 0;
    // I limited iterations to 100, but you may want way less
    while (BigInteger.Abs(newValue - oldValue) >= 1 && i < 100)
    {
        oldValue = newValue;
        newValue = ((root - 1) * oldValue 
                    + (n / BigInteger.Pow(oldValue, root - 1))) / root;
        i++;
    }
    return newValue;
}

答案 2 :(得分:3)

首先,以近似形式写下您的号码

  

a = n.10 2m

n是一个整数且大到可以使系统sqrt可用于其大小。 m也是一个整数。点意味着相乘。

您应该可以从初始整数中的位数和m中的位数获得n

请注意,这永远不会大于给定的数字(因为我们已将许多较低有效数字设置为0):因此满足答案的要求是下限。

平方根

  

a 1/2 = n 1/2 .10 m

计算速度快:第一部分使用系统sqrt;第二部分是一个微不足道的乘法;完全在BigInteger范围内。

此方法的准确性接近浮点精度,其速度与数字的大小无关。

答案 3 :(得分:2)

√ N ≈ ½(N/A + A)

这是牛顿平方根近似 - 发现使用... Google :-P

修改

以下功能返回

7.9025671444237E + 89&lt; -approx sqrt 7.9019961626505E + 89&lt; -actual sqrt

代表

624415433545435435343754652464526256453554543543254325247654435839457324987585439578943795847392574398025432658763408563498574398275943729854796875432457385798342759843263456484364

以下是我提出的建议(为自己的逻辑感到骄傲 - 而不是编码:-P)

好的,这是PHP(这很有趣:-D)但应该很容易转换。

看起来很可怕但不是

  1. 找到前3/4位数(取决于字符串长度)。

  2. 使用最接近的正方形技术找到这些数字的大约sqrt(例如,找到45的大约sqrt,我们知道最接近的整数正方形(正方形 - 遗忘项)是6(36)和7(49) - 和45在36和49之间的大约70% - 所以6.7是很好的近似值)

  3. 坚持使用牛顿方形近似公式。

  4. 我所写的是一种非常丑陋的方式 - 完成所有步骤 - 但实际上它更容易看到思维模式(我希望)。

    function approxSqrRt($number){
    
        $numLen = strlen($number);//get the length of the number - this is used to work out whether to get the first 3 or 4 digits from the number.
    
        $testNum = 0;
        $zerosToAdd = 0;
    
    
        //find out if even or odd number of digits as 400 and 40,000 yield same start 20 -> 200 BUT 4000 yields 63.245
        if($numLen % 2 == 0){
            $testNum = substr($number, 0, 4);
            $zerosToAdd = ($numLen - 4) / 2; 
        }else{
            $testNum = substr($number, 0, 3);
            $zerosToAdd = ($numLen - 3) / 2;
        }
    
        $square1 = 0;
    
        $x = 0;
        $z = false;
         //this should be made static array and look up nearest value but for this purpose it will do.
        // It is designed to find the nearest 2 squares - probably a much better way but was typing as I thought.
        while ($z == false){
            $x++;
            if(($x*$x) >= $testNum){
    
                $square1 = $x - 1;
    
                $z = true;
    
    
            }
    
    
    
        }
    
        $square2 = $square1 + 1;
    
    
    
        $ratio = 0;
        //ratio - we are simply qorking out if our square is closer to $square1 or $square2 and how far it is in between them - this will yield a much more accurate approximation.
        //
        if($testNum != ($square1 * $square1)){
            if($testNum != ($square2 * $square2)){
    
                $topOfEquation = $testNum;
                $BottomOfEquation = $square2 * $square2;
                $ratio = ($topOfEquation / $BottomOfEquation);
    
    
            }else{
                $ratio = 1;
            }
        }else{
            $ratio = 0;
        }
    
        //get the final approximation before adding the relevant number of 0's.
        $testNumSqrRt = $square1 + $ratio;
    
        //add the 0's on to make the number the correct length.
        $testNumberFinal = $testNumSqrRt  *  pow(10, $zerosToAdd);
    
        //run through Newtons Approximation.
        $approxSqrtFinal = (($number / $testNumberFinal) + $testNumberFinal) / 2;
    
       return $approxSqrtFinal;
    
    }
    
    
    
    $num = "624415433484364";
    
    echo approxSqrRt($num) . "<br/>";
    echo sqrt($num);
    

答案 4 :(得分:0)

Looking for an efficient integer square root algorithm for ARM Thumb2

无论如何,算法必须使用移位,+ / - 操作代替除法。

此外,如果您使用汇编语言实现例程,则可能会获得一些额外的优化。有关在C#中使用程序集的信息,请参阅:

http://www.codeproject.com/Articles/1392/Using-Unmanaged-code-and-assembler-in-C

答案 5 :(得分:0)

        static public double SQRTByGuess(double num)
        {
            // Assume that the number is less than 1,000,000. so that the maximum of SQRT would be 1000.
            // Lets assume the result is 1000. If you want you can increase this limit
            double min = 0, max = 1000;
            double answer = 0;
            double test = 0;
            if (num < 0)
            {
                Console.WriteLine("Negative numbers are not allowed");
                return -1;
            }
            else if (num == 0)
                return 0;

            while (true)
            {
                test = (min + max) / 2;
                answer = test * test;
                if (num > answer)
                {
                    // min needs be moved
                    min = test;
                }
                else if (num < answer)
                {
                    // max needs be moved
                    max = test;
                }
                if (num == answer)
                    break;
                if (num > (answer - 0.0001) &&
                    num < (answer + 0.0001))
                    break;
            }
            return test;
        }

参考:http://www.softwareandfinance.com/CSharp/Find_SQRT_Approximation.html