如何在C#中将BigIntegers的划分评估为十进制而不是两倍?

时间:2017-01-30 00:41:01

标签: c# type-conversion

TL; DR

可以将

F# PowerPack's BigRational类型转换为double来评估该值。但是,在分子和分母达到一定大小后,返回的值为double.NaN

由于BigRational将分子和分母都跟踪为System.Numerics.BigInteger s,因此您可以使用对数属性的变通方法:

a / b = e^(ln(a) - ln(b))

使用我们的BigInteger分子和分母,我们可以调用

BigInteger num = myBigRational.Numerator;
BigInteger den = myBigRational.Denominator;
double value = Math.Exp(BigInteger.Log(num) - BigInteger.Log(den));

由于double类型的结构对于接近0的值的限制,我宁愿使用decimal。我只是没弄明白。

上下文

为了好玩,我正在编写一个使用arctan的{​​{3}}计算pi的程序。

arctan(x) = x - x^3/3 + x^5/5 - x^7/7 +  x^9/9 - ...

如果我们将系列评估为1,我们得到

arctan(1) = 1 - 1/3 + 1/5 - 1/7 + 1/9 - ...

arctan(1) = pi/4开始,我们可以将系列乘以4来计算pi。

我的程序的目标是计算收敛到pi accurate to n digits所需的系列术语的数量。例如,为了使系列精确到一位数(3),它需要前三个术语:

1 term:  4 * (1) = 4
2 terms: 4 * (1 - 1/3) = 2.666666666666667
3 terms: 4 * (1 - 1/3 + 1/5) = 3.466666666666667

要准确到2位数(3.1),它需要前19个术语。准确到3位数(3.14)需要前119个术语,依此类推。

我最初使用C#的decimal类型编写了我的程序:

const int MaxDigits = 20;
private static void RunDecimalCalculation()
{
    decimal pi = 0m; // our current approximation of pi
    decimal denominator = 1m;
    decimal addSubtract = 1m;
    ulong terms = 0;

    for (int digits = 0; digits < MaxDigits; digits++)
    {
        decimal piToDigits, upperBound;
        GetBounds(digits, out piToDigits, out upperBound);

        while (pi >= upperBound | pi < piToDigits)
        {
            pi += addSubtract * 4m / denominator;
            denominator += 2m;
            addSubtract *= -1m;
            terms++;
        }

        PrintUpdate(terms, digits, pi);
    }
}

/// <summary>
/// Returns the convergence bounds for <paramref name="digits"/> digits of pi.
/// </summary>
/// <param name="digits">Number of accurate digits of pi.</param>
/// <param name="piToDigits">Pi to the first <paramref name="digits"/> digits of pi.</param>
/// <param name="upperBound">same as <paramref name="piToDigits"/>, but with the last digit + 1</param>
/// <example>
/// <code>GetBounds(1)</code>:
///     piToDigits = 3
///     upperBound = 4
/// 
/// <code>GetBounds(2)</code>:
///     piToDigits = 3.1
///     upperbound = 3.2
/// </example>
private static void GetBounds(int digits, out decimal piToDigits, out decimal upperBound)
{
    int pow = (int)Math.Pow(10, digits);
    piToDigits = (decimal)Math.Floor(Math.PI * pow) / pow;
    upperBound = piToDigits + 1m / pow;
}

但我意识到,由于每次迭代中的舍入错误,在足够的条件之后,所需的术语数量可能会被关闭。所以,我开始研究F#PowerPack的BigRational并重新编写代码:

// very minor optimization by caching common values
static readonly BigRational Minus1 = BigRational.FromInt(-1);
static readonly BigRational One = BigRational.FromInt(1);
static readonly BigRational Two = BigRational.FromInt(2);
static readonly BigRational Four = BigRational.FromInt(4);

private static void RunBigRationalCalculation()
{
    BigRational pi = BigRational.Zero;
    ulong terms = 0;
    var series = TaylorSeries().GetEnumerator();

    for (int digits = 0; digits < MaxDigits; digits++)
    {
        BigRational piToDigits, upperBound;
        GetBounds(digits, out piToDigits, out upperBound);

        while (pi >= upperBound | pi < piToDigits)
        {
            series.MoveNext();
            pi += series.Current;
            terms++;
        }

        double piDouble = Math.Exp(BigInteger.Log(pi.Numerator) - BigInteger.Log(pi.Denominator));
        PrintUpdate(terms, digits, (decimal)piDouble);
    }
}

// code adapted from http://tomasp.net/blog/powerpack-numeric.aspx
private static IEnumerable<BigRational> TaylorSeries()
{
    BigRational n = One;
    BigRational q = One;

    while (true)
    {
        yield return q * Four / n;

        n += Two;
        q *= Minus1;
    }
}

不出所料,这个版本慢慢地运行 ,这很好。 (十进制版本需要34秒才能达到9个准确的数字; BigRational版本需要17秒才能达到5个准确的数字,并且已经运行了大约半个小时并且没有达到6个准确的数字)。但令我感到沮丧的是,double不如decimal准确,所以虽然术语的数量总是正确的,但显示的值是

double piDouble = Math.Exp(BigInteger.Log(pi.Numerator) - BigInteger.Log(pi.Denominator));

不准确。有没有办法解决这个问题,无论是通过数学魔法还是某些具有decimalMath.Exp()BigInteger.Log()的库?

1 个答案:

答案 0 :(得分:3)

  

如何在C#中将BigIntegers的划分评估为十进制而不是两倍?

你有BigIntegers N和D,并希望将精确值N / D近似为小数。 WOLOG认为两者都是正面的。

这很容易。首先解决这个问题:

  • 给定N和D,找到最小化N / D绝对值的BigInteger M - M / 10 28

第二,解决这个问题:

  • 给定M,找到BigInteger M'和非负整数E,使得M / 10 28 = M'/ 10 E 并且E被最小化。< / LI>

第三,解决这个问题:

  • 给定M',找到非负BigIntegers I0,I1,I2,使得M'= I0 + I1 * 2 32 + I2 * 2 64 并且每个都是严格小于2 32 。如果没有这样的I0,I1,I2,则您的号码不能表示为小数。

将I0,I1和I2转换为无符号整数然后转换为有符号整数。

现在您拥有了调用所需的一切

https://msdn.microsoft.com/en-us/library/bb1c1a6x(v=vs.110).aspx

嘿,你手边有一个小数。

那说:首先你不应该这样做。你正在BigRationals做数学;你为什么要把它们放到小数或双打?只需将pi大概逼近你想要的大概率,并将慢转换系列与之比较。

仅供参考,这个系列慢慢收敛非常。一旦你凭经验确定收敛速度有多慢,你能否提供收敛率的任何界限证明