2

I'm playing around with some calculations in a program I'm writing. Although my problem may seem obscure and perhaps avoidable, it's not, and even if it is, I'd rather not avoid it for now :-D

The problem is that I have a huge number (thousands of digits, maybe millions at some point) which I need to find an approximate SQRT for. I'm using System.Numerics.BigInteger, I'm need the approximation to be less than or equal to the real SQRT. For example, if the number provided was 100, I'd be happy with 7, 8, 9 or 10, but not 11.

Current Code:

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;
}

Whilst this gives me the right answer (so far as I can tell), it's crazy slow as it's trying to get a precise answer.

If getting the cube-root, or anything else like that would be faster, I'd be happy with that. Also, yes, I know finding square roots quickly can make you lots of money, not really what I'm trying to do though, I just want a quick approximation...

Any help you could give me would be great!


Edit - Gabe

This is the updated IntegerSqrt function I am using which doesn't appear to work any quicker.

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;
}


Edit 2 Is this what you were thinking? - I ran this for 30 minutes on a sample large number (50k digits) and it looped around 100 times without completing. I feel as though I'm missing something...

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;
}
Faraday
  • 2,904
  • 3
  • 23
  • 46
  • 1
    http://www.school-for-champions.com/algebra/square_root_approx.htm#.Uswv7fRdV8E <- this is what you need] – GrahamTheDev Jan 07 '14 at 16:50
  • 1
    http://en.wikipedia.org/wiki/Methods_of_computing_square_roots – Bathsheba Jan 07 '14 at 16:52
  • But that requires me to take a guess at the actual square right? I can't do that... – Faraday Jan 07 '14 at 16:53
  • 1
    @Vijay: use the rough approximation (based on geometric mean) in the wikipedia link I posted to get the rough guess. Then one iteration of Newton Raphson. With a bit of trickery, you can adjust so you never overshoot the value. – Bathsheba Jan 07 '14 at 16:54
  • @Bathsheba - That approximation just truncates the number of digits down to allow a faster search for a sqrt, not really a good approximation. Right? – Faraday Jan 07 '14 at 16:54
  • @Bathsheba - I read the article before I posted here, given your rep, I'm assuming I don't understand something... Could you give me a slighter harder nudge in the right direction? – Faraday Jan 07 '14 at 16:56
  • To be honest, most algorithms for square root works using logs. Which is pretty fast anyways. – Aron Jan 07 '14 at 17:04
  • @Aron - I tried using `BigInteger.Pow(new BigInteger(BigInteger.Log(myBigInteger) / Math.Log(2)), 2);` - but I get NaN (I think because my numbers are too big) - Works perfectly with numbers like 10000000000 – Faraday Jan 07 '14 at 17:07
  • @Bathsheba - I'll try my best, but it's like you're speaking Greek! ;) If you can help me out with some code, I'm happy to wait and give you a bounty for it too... I'm totally stumped at the moment! – Faraday Jan 07 '14 at 17:23
  • ok so I wrote the asnwer in PHP but should be easy to understand - will work for MASSIVE numbers provided you can actually pass them into the function in the first place. – GrahamTheDev Jan 07 '14 at 18:38
  • 1
    If it looped without completing, you may have used a negative number as your test case. Make sure you do an Abs or something on your test number first. – Gabe Jan 08 '14 at 17:49

6 Answers6

4

Using my answer here to calculate Log2(N), I prepared a test case.

Although your code is closer to the real square root, my tests shows millions of times speed up when the input number is getting larger.

I think you will have to decide between more precise result vs. thousands/millions of times speed-up,

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));

The extension method for 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);
    }
}

Code to measure the speed

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

RESULTS:

In my computer, both code results ~in 8 seconds, but my posted code runs 10*200000 times, comparing your code runs only 10 times. (Yes, hard to believe, for 8000 bit numbers the difference is 200,000 times)

When comparing with 16000 bit numbers the difference raises to 1,000,000 times...

Community
  • 1
  • 1
L.B
  • 114,136
  • 19
  • 178
  • 224
  • Hi, I get an Index Out Of Bounds error when I try to use your code. My BigInteger is getting `buf[len-1]` as `217` which is too large. I don't understand your look up well enough to expand it, could you elaborate? Or am I missing something? – Faraday Jan 08 '14 at 11:30
  • 1
    @Vijay make sure that *last* byte of array(passed to the BigInteger constructor) is less that 128(which means positive BigInteger). if it is more that 127 than most significant bit of it is set meaning it is an negative number. EX: {0xff, 0xff, 0x **7** f} – L.B Jan 08 '14 at 13:24
  • How easy would it be to modify this to work with ^3, ^4, ^5 etc? I assume I would need to update PreCalc, but I have no idea how, you were right about the number being negative, so that is solved, but in order to understand the answer properly, I'd like to know how to change the root approximation to work with different roots, if you wouldn't mind! – Faraday Jan 08 '14 at 18:01
  • 1
    @Vijay I don't get what you mean by `^3, ^4, ^5`. But This answer would only work for Base2. The trick is: the `digitcount-1` of a binary number is its Log2. For ex; `8=1000b` and `NumberOfDigits(1000b)-1=3`. How to calc the precalc? Just run the following program and see the count of the leading zeros. `for (BigInteger i = 0; i < 128; i++ ) { byte b = i.ToByteArray().Last(); Console.WriteLine(Convert.ToString((int)i, 2).PadLeft(8, '0')); }` – L.B Jan 08 '14 at 20:10
  • 1
    @Vijay to make the story short, To calculate the Log2 I simply count the number of bits in the byte arrar(array's len*8), while skipping the leading zeros. `len*8-leadingZeros-1`. – L.B Jan 08 '14 at 20:15
  • 1
    @Vijay and lastly, `new BigInteger(1) << (Log2(bi) / 2);` is equal to `Math.Pow(2 , Log2(bi) / 2)`. For ex, `pow(2,5) = 1<<5`. As you can see All of them are just binary arithmetic tricks. thats all. – L.B Jan 08 '14 at 20:23
  • aaaah, ok, so PreCalc makes sense how. My ^3/4/5 thing was my way of asking a way to find the CubeRoot (^3), or Root7 ^7. Thanks again. – Faraday Jan 08 '14 at 21:22
  • 1
    @Vijay That is not a part of your question? why do you need it? – L.B Jan 08 '14 at 21:27
  • I don't need it as such, I'd just appreciate knowing how to do it in a similar way to the way your code is written. If you would prefer me to ask this in another question, I am happy to do just that. :) – Faraday Jan 08 '14 at 21:39
  • @Vijay `Log3(x)=Log2(x)/Log2(3)`. So since I posted Log2, I think you can extend it. – L.B Jan 08 '14 at 21:59
  • @Vijay Also don't forget `(bi * N * N).Sqrt() / N` would give you a better approximation than `bi.Sqrt()` – L.B Jan 08 '14 at 22:46
3

Here's the rough approximation that will get you to within a factor of 2 of your square root:

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);
}

Instead of starting with n as your approximation (newValue = n;), you would start off by calling this (newValue = RoughSqrt(n);). This will get you to your answer with way fewer iterations.

If you want to take the Nth root instead, you just have to change the rough approximation to use 1/Nth the bits and change Newton's method to use the derivative of 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;
}
Gabe
  • 84,912
  • 12
  • 139
  • 238
  • Hey, after 100k iterations I stopped it running. I think the problem is in my function. When I put in `var oldValue = n.RoughSqrt();`, it gets overwritten on the first line inside the while loop, I assume this is incorrect, so I would move the `oldValue = newValue;` line to the bottom of the while loop, except that would break the while loop (only evaluate once). So, how would I change my function to work work your code? - Updated question to show updated code. – Faraday Jan 08 '14 at 12:09
  • 1
    @Vijay: I'm working with numbers on the order of 24k digits and sometimes it converges after 15 iterations, and other times not at all. It's not clear why. – Gabe Jan 08 '14 at 15:04
  • 1
    @Vijay: Oops, it looks like I was using negative numbers, which obviously won't converge to a real sqrt. My tests with 240k numbers are all converging after 18 iterations. – Gabe Jan 08 '14 at 15:18
  • How easy would it be to modify this to work with ^3, ^4, ^5 etc? – Faraday Jan 08 '14 at 17:59
  • 1
    Are you asking how you could make it take the 3rd, 4th, etc. root? – Gabe Jan 08 '14 at 18:12
  • Yeah, so if I wanted to find the cuberoot (^3) instead of square root (^2), what would I need to do? I'm trying to understand the code better and make it so that I can modify it for other requirements I have. I'll add a bounty to the question once the timer elapses to ensure you don't sped more time than is worth it for the points! :) – Faraday Jan 08 '14 at 18:20
  • 1
    @Vijay: OK, I added more general code to take any positive integer root. – Gabe Jan 08 '14 at 20:37
  • Thanks for the update, I tried it with some roots and it seemed to work until I hit root 50 (could be specific to the numbers I'm working with too). But when I tried it with root 50, it returned a number which when raised to the power of 50 was larger than the original number, this isn't allowed... Could you assist in fixing that rule? It's just different wording for the rule I wrote in my question, which is "The rough root must be <= the real root". Thanks again – Faraday Jan 08 '14 at 23:13
  • 1
    @Vijay: Yes, it looks like the rough root wouldn't converge quickly enough. The `var rtBits = bits / root + 1` was missing its `+1`, so the current code seems to converge easily within the 100 iterations. – Gabe Jan 09 '14 at 05:35
3

Firstly, write your number in the approximate form

a = n.102m

Where n is an integer and as large as you can make it subject to a system sqrt being available for its size. m is also an integer. The dot means multiply.

You should be able to get m from the number of digits in the initial integer and the number of digits in n.

Note that this will never be greater than the given number (as we have set many of the less significant digits to 0): so satisfying the requirement of the answer being a lower bound.

The square root is

a1/2 = n1/2.10m

This is fast to compute: the first part using the system sqrt; the second part is a trivial multiplication; well within the scope of BigInteger.

The accuracy of this approach approaches floating point precision and its speed is independent of the size of the number.

Bathsheba
  • 231,907
  • 34
  • 361
  • 483
  • Out of curiosity: Is `sqrt` faster than `Math.Pow(n, .5)`? – Phantômaxx Nov 20 '17 at 11:20
  • 1
    `sqrt` is not slower, if you get my meaning. – Bathsheba Nov 20 '17 at 11:21
  • OK, I do. I was thinking so, because (in theory) a root is a series of divisions, which are very time consuming. While a power elevation is a series of multiplications (much faster). But, as you say, the `sqrt` method implementation may already use multiplications. Or logarithms. – Phantômaxx Nov 20 '17 at 11:39
  • 1
    @BernoulliGate: A typical CPU uses a Newton-Raphson style algorithm for `sqrt`. Note that IEEE754 mandates it must return the best result possible. In C at least you can't say the same for `pow`, where `pow(x,y)` is evaluated as `exp(y log x)`. I don't know how `pow` is evaluated in C#. – Bathsheba Nov 20 '17 at 11:53
2
√ N ≈ ½(N/A + A)

That is Newtons Square Root Approximation - found using...Google :-P

EDIT

The function below returns

7.9025671444237E+89 <-approx sqrt 7.9019961626505E+89 <-actual sqrt

for

624415433545435435343754652464526256453554543543254325247654435839457324987585439578943795847392574398025432658763408563498574398275943729854796875432457385798342759843263456484364

Here is what I came up with (proud of myself for the logic - not so much for the coding :-P)

Ok so this is PHP (was fun to do this :-D) but should be easy to convert.

Looks scary but it isn't

  1. find first 3/4 digits (depending on string length).

  2. Find the approx sqrt of those digits using closest squares technique (e.g. to find approx sqrt of 45 we know nearest whole number squares (perfect squares - forget term) are 6 (36) and 7 (49) - and that 45 is about 70% of the way between 36 and 49 - so 6.7 is good approximation)

  3. Stick this through Newtons square approximation formula.

What I have written is a very ugly way of doing it - with all the steps left in - but it actually makes it easier to see the thought pattern (I hope).

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);
GrahamTheDev
  • 22,724
  • 2
  • 32
  • 64
  • Your approximation function is close to what I need, but due to the hugely varying length of the numbers, how would I know what to truncate down to? Also, does this deal with keeping the SQRT result less than or equal to the real sqrt? And thanks, even just reading your code was helpful! – Faraday Jan 08 '14 at 11:42
  • ok the part to know what to truncate down to is the strlen (string length) part - this will always give 4 digits off the front if there are an even number of digits and 3 if odd - eg sqrt 400 = 20, 4000 = 63.25 40,000 = 200, 400,000 = 632.5 - for every 100 (10 square) you shift the decimal place 1. Then just add back in the zeros after equation - you will have to validate if the sqrt is lower than the actual one. - best way would be to multiply $ratio by 0.8 - this will then always be lower (but less accurate) – GrahamTheDev Jan 08 '14 at 17:06
0

Looking for an efficient integer square root algorithm for ARM Thumb2

Anyway the algorithm must use shifts, +/- operations instead division.

Also if you implement the routine in assembly language, you might get some extra optimization. For using assembly in C#, please see:

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

Community
  • 1
  • 1
Flanker
  • 408
  • 3
  • 7
  • 2
    Implementing the sqrt routine in assembly won't help; you'd have to implement the whole BigInteger library in assembly. As is, none of those routines are faster than Newton's method with a good starting approximation. – Gabe Jan 08 '14 at 17:44
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;
        }

Reference: http://www.softwareandfinance.com/CSharp/Find_SQRT_Approximation.html