项目欧拉#16 - C#2.0

时间:2009-03-24 14:13:16

标签: c# .net

我一直在使用C#2.0中的Project Euler Problem #16进行摔跤。问题的关键在于你必须计算并迭代一个长度为604位(或那里)的数字中的每个数字。然后,您将这些数字相加以产生答案。

这就出现了一个问题:C#2.0 没有可以处理这种计算精度的内置数据类型。我可以使用3rd party library,但如果没有外部库,那将无法以编程方式解决问题。我可以在Perl中解决它;但是我试图在C#2.0中解决它(我将尝试在Project Euler问题的下一次运行中使用C#3.0。)

问题

你有什么建议(不是答案!)来解决C#2.0中的项目Euler#16?有哪些方法可行?

  

NB :如果您决定发布答案,请在尝试前添加一个在其前面写过### Spoiler的块引用。

10 个答案:

答案 0 :(得分:15)

一系列数字。 32位无符号整数是32位二进制数。字符串“12345”是一系列5位数字。数字可以以多种方式存储:作为位,字符,数组元素等。 C#中具有完全精度的最大“本机”数据类型可能是十进制类型(128位,28-29位)。只需选择自己的数字存储方法,即可存储更大的数字。

至于其余部分,这会给你一个线索:

2 1 = 2
2 2 = 2 1 + 2 1
2 3 = 2 2 + 2 2

示例:

The sum of digits of 2^100000 is 135178
Ran in 4875 ms

The sum of digits of 2^10000 is 13561
Ran in 51 ms

The sum of digits of 2^1000 is 1366
Ran in 2 ms

SPOILER ALERT:C#中的算法和解决方案如下。

基本上,提到一个数字只不过是一个数字数组。这可以通过两种方式轻松表示:

  • 作为一个字符串;
  • 作为字符或数字的数组。

正如其他人所提到的,实际上建议以相反的顺序存储数字。它使计算更容易。我尝试了上述两种方法。我发现字符串和字符算法很烦人(在C / C ++中它更容易;语法在C#中显得很烦人)。

首先要注意的是,您可以使用一个数组执行此操作。您不需要在每次迭代时分配更多存储空间。如上所述,你可以通过将先前的2次幂加倍来找到2的幂。所以你可以通过加倍1千次来找到2 1000 。可以使用通用算法完成加倍:

carry = 0
foreach digit in array
  sum = digit + digit + carry
  if sum > 10 then
    carry = 1
    sum -= 10
  else
    carry = 0
  end if
  digit = sum
end foreach

此算法与使用字符串或数组基本相同。最后你只需加上数字。一个简单的实现可能会在每次迭代时将结果添加到新数组或字符串中。馊主意。真的慢了下来。如上所述,它可以在适当的位置完成。

但阵列应该有多大?那也很容易。在数学上你可以将2 ^ a转换为10 ^ f(a)其中f(a)是一个简单的对数转换,你需要的数字是从10的幂的下一个更高的整数。为简单起见,你可以使用:

digits required = ceil(power of 2 / 3)

这是一个近似且足够的。

您可以通过使用更大的数字来真正优化它。 32位有符号int可以存储+/- 20亿之间的数字(大约9个数字等于10亿,所以你可以使用32位int(有符号或无符号)基本上是10亿个“数字”。你可以工作你需要多少个int,创建那个数组,这就是运行整个算法所需的所有存储空间(130个字节),所有内容都已完成。

解决方案如下(在相当粗略的C#中):

    static void problem16a()
    {
        const int limit = 1000;
        int ints = limit / 29;
        int[] number = new int[ints + 1];
        number[0] = 2;
        for (int i = 2; i <= limit; i++)
        {
            doubleNumber(number);
        }
        String text = NumberToString(number);
        Console.WriteLine(text);
        Console.WriteLine("The sum of digits of 2^" + limit + " is " + sumDigits(text));
    }

    static void doubleNumber(int[] n)
    {
        int carry = 0;
        for (int i = 0; i < n.Length; i++)
        {
            n[i] <<= 1;
            n[i] += carry;
            if (n[i] >= 1000000000)
            {
                carry = 1;
                n[i] -= 1000000000;
            }
            else
            {
                carry = 0;
            }
        }
    }

    static String NumberToString(int[] n)
    {
        int i = n.Length;
        while (i > 0 && n[--i] == 0)
            ;
        String ret = "" + n[i--];
        while (i >= 0)
        {
            ret += String.Format("{0:000000000}", n[i--]);
        }
        return ret;
    }

答案 1 :(得分:4)

我使用C#解决了这个问题,当我发现Python可以通过一个简单的操作完成此操作时,我感到非常沮丧。

您的目标是使用int值数组创建一个添加计算机。

遵循

  

我最终使用了一个int数组   模拟添加机器的值,   但我向后代表了这个数字    - 你可以做,因为问题只要求数字的总和,   这意味着订单无关紧要。

     

你基本上在做什么   将值加倍1000倍,所以你   可以加倍存储在中的值1   数组的第一个元素,然后   继续循环,直到你的价值为止   超过10.这是你将拥有的地方   跟踪进位值。该   超过10的2的第一次幂是   16,所以数组中的元素之后   第五次迭代是6和1。

     

现在循环遍历数组   它从第一个值(6)开始   变成12(所以你保持最后   数字,并在下一个设置进位   数组的索引) - 何时    值加倍,得到2 ...加上1为进位位   等于3.现在你有2和3   数组代表32。

     

继续这个过程1000次   你将有一个大约600的阵列   您可以轻松添加的元素。

答案 2 :(得分:3)

假装你很年轻,用方纸。对我来说,这就像一个数字列表。然后加倍,你加倍每个数字,然后处理任何“进位”,减去10并将1添加到下一个索引。所以如果答案是1366 ......就像(完全没有优化,rot13):

hfvat Flfgrz;
hfvat Flfgrz.Pbyyrpgvbaf.Trarevp;    
pynff Cebtenz {
    fgngvp ibvq Pneel(Yvfg<vag> yvfg, vag vaqrk) {
        juvyr (yvfg[vaqrk] > 9) {
            yvfg[vaqrk] -= 10;
            vs (vaqrk == yvfg.Pbhag - 1) yvfg.Nqq(1);
            ryfr yvfg[vaqrk + 1]++;
        }
    }
    fgngvp ibvq Znva() {
        ine qvtvgf = arj Yvfg<vag> { 1 }; // 2^0
        sbe (vag cbjre = 1; cbjre <= 1000; cbjre++) {
            sbe (vag qvtvg = 0; qvtvg < qvtvgf.Pbhag; qvtvg++) {
                qvtvgf[qvtvg] *= 2;
            }
            sbe (vag qvtvg = 0; qvtvg < qvtvgf.Pbhag; qvtvg++) {
                Pneel(qvtvgf, qvtvg);
            }
        }

        qvtvgf.Erirefr();
        sbernpu (vag v va qvtvgf) {
            Pbafbyr.Jevgr(v);
        }
        Pbafbyr.JevgrYvar();

        vag fhz = 0;
        sbernpu (vag v va qvtvgf) fhz += v;
        Pbafbyr.Jevgr("fhz: ");
        Pbafbyr.JevgrYvar(fhz);
    }
}

答案 3 :(得分:3)

之前我已经解决了这个问题,现在我用C#3.0重新解决了这个问题。 :)

我刚刚编写了Multiply扩展方法,该方法采用IEnumerable<int>和乘数并返回IEnumerable<int>。 (每个int表示一个数字,第一个表示最不重要的数字。)然后,我只创建了一个包含项{1}的列表,并将其乘以2 1000次。使用Sum扩展方法添加列表中的项目非常简单。

19行代码,运行时间为13毫秒。在我的笔记本上。 :)

答案 4 :(得分:2)

如果您希望在C#中进行主要计算,则需要某种大整数实现(类似于gmp用于C / C ++)。编程是关于使用正确的工具来完成正确的工作。如果你找不到一个好的C#大整数库,那么用Python这样已经能够计算大数的语言来计算数字并不违反规则。然后,您可以通过您选择的方法将此数字放入C#程序,并迭代数字中的每个字符(您必须将其存储为字符串)。对于每个字符,将其转换为整数并将其添加到总数中,直到达到数字的末尾。如果你想要大整数,我用下面的python计算它。答案是进一步下降。

  

部分剧透

     

10715086071862673209484250490600018105614048117055336074437503883703510511249361   22493198378815695858127594672917553146825187145285692314043598457757469857480393   45677748242309854210746050623711418779541821530464749835819412673987675591655439   46077062914571196477686542167660429831652624386837205668069376

     

下面的剧透!

>>> val = str(2**1000)
>>> total = 0
>>> for i in range(0,len(val)): total += int(val[i])
>>> print total











1366

答案 5 :(得分:1)

如果你有红宝石,你可以很容易地计算出“2 ** 1000”并将其作为一个字符串。应该是在C#中轻松剪切/粘贴到字符串中。

扰流

  

在Ruby中:(2 ** 1000).to_s.split(//).injection(0){| x,y | X + y.to_i}

答案 6 :(得分:1)

  

扰流

     

如果您想查看解决方案   出my other answer。这是Java,但很容易移植到C#

这是一个线索:

用列表表示每个数字。这样你可以做基本的总和,如:

[1,2,3,4,5,6]
+       [4,5]
_____________
[1,2,3,5,0,1]

答案 7 :(得分:0)

将数字表示为整数序列的一种替代方法是将数字base 2 ^ 32表示为32位整数的列表,这是许多大整数库所做的。然后,您必须将数字转换为基数10以进行输出。这对于这个特殊的问题并没有太大的帮助 - 你可以直接写2 ^ 1000,然后必须除以10倍而不是将2乘以1000倍(或者,因为1000是0b1111101000。计算产品的2 ^ 8,32,64,128,256,512使用重复平方2 ^ 8 =(((2 ^ 2)^ 2)^ 2)))这需要更多空间和乘法,但操作要少得多) - 更接近正常大整数使用,因此您可能会发现它在以后的问题中更有用(如果您尝试使用digit-per int方法计算28433×2^(7830457)+1的最后十位数并重复添加,则可能需要一些时间(尽管在这种情况下)你可以使用modulo arthimetic,而不是添加数百万的数字))。

答案 8 :(得分:0)

我在此处发布的工作解决方案:http://www.mycoding.net/2012/01/solution-to-project-euler-problem-16/

代码:

import java.math.BigInteger;
public class Euler16 {

public static void main(String[] args) {
    int power = 1;
    BigInteger expo = new BigInteger("2");
    BigInteger num = new BigInteger("2");
    while(power < 1000){
        expo = expo.multiply(num);
        power++;
    }
    System.out.println(expo);   //Printing the value of 2^1000
    int sum = 0;
    char[] expoarr = expo.toString().toCharArray();
    int max_count = expoarr.length;
    int count = 0;
    while(count<max_count){ //While loop to calculate the sum of digits
        sum = sum + (expoarr[count]-48);
        count++;
    }
    System.out.println(sum);
}
}

答案 9 :(得分:0)

Euler problem #16已在这里多次讨论过,但我找不到一个能够很好地概述可能的解决方案方法的答案,即土地的原貌。这是我试图纠正这一点。

此概述适用于已找到解决方案的 ,希望获得更全面的信息。即使示例代码是C#,它基本上与语言无关。 C#2.0中没有一些功能可用,但它们并不是必不可少的 - 它们的目的只是为了让枯燥的东西尽量少用。

除了使用现成的BigInteger库(它不算数)之外,Euler#16的直接解决方案分为两个基本类别:本地执行计算 - 即在2的幂的基础上 - 和转换为十进制以获取数字,或直接在十进制基数中执行计算,以便数字可用而无需任何转换。

对于后者,有两个相当简单的选择:

  • 反复加倍
  • 通过反复平方来启动

原生计算+基数转换

这种方法最简单,其性能超过了使用.Net的内置BigInteger类型的天真解决方案。

实际计算很简单:只需执行1 << 1000的道德等价物,通过存储1000个二进制零并附加单个二进制1。

转换也非常简单,可以通过编写铅笔和纸分割方法来完成,并选择适当大的数字&#39;为了效率。中间结果的变量需要能够容纳两个数字&#39 ;;将long中的小数位数除以2会得到9个十进制数字,用于最大元数字(或者&#39;肢体&#39;,因为它通常在bignum传说中调用)。

class E16_RadixConversion
{
    const int BITS_PER_WORD = sizeof(uint) * 8;
    const uint RADIX = 1000000000;  // == 10^9

    public static int digit_sum_for_power_of_2 (int exponent)
    {
        var dec = new List<int>();
        var bin = new uint[(exponent + BITS_PER_WORD) / BITS_PER_WORD];
        int top = bin.Length - 1;

        bin[top] = 1u << (exponent % BITS_PER_WORD);

        while (top >= 0)
        {
            ulong rest = 0;

            for (int i = top; i >= 0; --i)
            {
                ulong temp = (rest << BITS_PER_WORD) | bin[i];
                ulong quot = temp / RADIX;  // x64 uses MUL (sometimes), x86 calls a helper function

                rest = temp - quot * RADIX;         
                bin[i] = (uint)quot;
            }

            dec.Add((int)rest);

            if (bin[top] == 0)
                --top;
        }

        return E16_Common.digit_sum(dec);
    }
}

我写了(rest << BITS_PER_WORD) | big[i]而不是使用operator +,因为这正是这里所需要的;不需要进行带有进位传播的64位加法。这意味着两个操作数可以直接写入寄存器对中的单独寄存器,也可以写入等效结构中的字段LARGE_INTEGER

在32位系统上,64位除法不能作为一些CPU指令内联,因为编译器无法知道算法保证商和余数适合32位寄存器。因此,编译器调用一个可以处理所有可能性的辅助函数。

这些系统可能会因使用较小的肢体而受益,即RADIX = 10000uint代替ulong来保持中间(双肢)结果。 C / C ++等语言的另一种选择是调用一个合适的编译器内部函数,它将原始的32位包装为32位到64位的乘法(假设用常数基数除以通过与反函数相乘来实现) 。相反,在64位系统上,如果编译器提供合适的64×64到128位乘法基元或允许内联汇编程序,则肢体大小可以增加到19位。

十进制加倍

反复加倍似乎是每个人的最爱,所以让我们接下来做。中间结果的变量需要保持一个数字&#39;加一个进位,为long每肢提供18位数。转到ulong不能改进事物(有0.04位丢失到19位加上进位),所以我们也可以坚持使用long

在二进制计算机上,小数肢与计算机字边界不一致。这使得有必要在计算的每个步骤期间对肢体执行模运算。这里,该模运算可以在进位的情况下减少到模数的减法,这比执行除法更快。内部循环中的分支可以通过比特来消除,但是对于基本算法的演示而言,这将是不必要的模糊。

class E16_DecimalDoubling
{
    const int DIGITS_PER_LIMB = 18;                  // == floor(log10(2) * (63 - 1)), b/o carry
    const long LIMB_MODULUS = 1000000000000000000L;  // == 10^18

    public static int digit_sum_for_power_of_2 (int power_of_2) 
    {
        Trace.Assert(power_of_2 > 0);

        int total_digits = (int)Math.Ceiling(Math.Log10(2) * power_of_2);
        int total_limbs = (total_digits + DIGITS_PER_LIMB - 1) / DIGITS_PER_LIMB;
        var a = new long[total_limbs];
        int limbs = 1;

        a[0] = 2;

        for (int i = 1; i < power_of_2; ++i)
        {
            int carry = 0;

            for (int j = 0; j < limbs; ++j)
            {
                long new_limb = (a[j] << 1) | carry;

                carry = 0;

                if (new_limb >= LIMB_MODULUS)
                {
                    new_limb -= LIMB_MODULUS;
                    carry = 1;
                }

                a[j] = new_limb;
            }

            if (carry != 0)
            {
                a[limbs++] = carry;
            }
        }

        return E16_Common.digit_sum(a);
    }
}

这与基数转换一样简单,但除了非常小的指数外,它也不会在任何地方执行(尽管它的小数位是18位小数)。原因是代码必须执行(指数-1)倍增,并且每次通过中完成的工作大约相当于总数(肢数)的一半。

重复平方

通过重复平方提供动力背后的想法是用少量乘法替换大量的倍增。

1000 = 2^3 + 2^5 + 2^6 + 2^7 + 2^8 + 2^9

x^1000 = x^(2^3 + 2^5 + 2^6 + 2^7 + 2^8 + 2^9)

x^1000 = x^2^3 * x^2^5 * x^2^6 * x^2^7 * x^2*8 * x^2^9

x ^ 2 ^ 3可以通过平方x三次得到,x ^ 2 ^ 5通过平方五次得到,依此类推。在二进制计算机上,指数分解为2的幂很容易获得,因为它是表示该数字的位模式。但是,即使非二进制计算机也应该能够测试数字是奇数还是偶数,或者将数字除以2。

乘法可以通过编写铅笔纸方法来完成;在这里,我使用辅助函数计算产品的一行,并将其添加到适当移位的结果中,这样部分产品的行不需要存储,以便稍后单独添加。计算过程中的中间值最多可以是两位数字&#39;在尺寸方面,四肢的宽度只能是反复加倍的一半(除了一个数字之外只需要一个额外的位)。

注意:计算的基数不是2的幂,因此这里的简单移位无法计算2的平方。从积极的方面来说,代码可用于计算除2之外的基数的权力。

class E16_DecimalSquaring
{
    const int DIGITS_PER_LIMB = 9;  // language limit 18, half needed for holding the carry
    const int LIMB_MODULUS = 1000000000;

    public static int digit_sum_for_power_of_2 (int e)
    {
        Trace.Assert(e > 0);

        int total_digits = (int)Math.Ceiling(Math.Log10(2) * e);
        int total_limbs = (total_digits + DIGITS_PER_LIMB - 1) / DIGITS_PER_LIMB;
        var squared_power = new List<int>(total_limbs) { 2 };
        var result = new List<int>(total_limbs);

        result.Add((e & 1) == 0 ? 1 : 2);

        while ((e >>= 1) != 0)
        {
            squared_power = multiply(squared_power, squared_power);

            if ((e & 1) == 1)
                result = multiply(result, squared_power);
        }

        return E16_Common.digit_sum(result);
    }

    static List<int> multiply (List<int> lhs, List<int> rhs)
    {
        var result = new List<int>(lhs.Count + rhs.Count);

        resize_to_capacity(result);

        for (int i = 0; i < rhs.Count; ++i)
            addmul_1(result, i, lhs, rhs[i]);

        trim_leading_zero_limbs(result);

        return result;
    }

    static void addmul_1 (List<int> result, int offset, List<int> multiplicand, int multiplier)
    {
        // it is assumed that the caller has sized `result` appropriately before calling this primitive
        Trace.Assert(result.Count >= offset + multiplicand.Count + 1);

        long carry = 0;

        foreach (long limb in multiplicand)
        {
            long temp = result[offset] + limb * multiplier + carry;
            carry = temp / LIMB_MODULUS;
            result[offset++] = (int)(temp - carry * LIMB_MODULUS);
        }

        while (carry != 0)
        {
            long final_temp = result[offset] + carry;
            carry = final_temp / LIMB_MODULUS;
            result[offset++] = (int)(final_temp - carry * LIMB_MODULUS);
        }
    }

    static void resize_to_capacity (List<int> operand)
    {
        operand.AddRange(Enumerable.Repeat(0, operand.Capacity - operand.Count));
    }

    static void trim_leading_zero_limbs (List<int> operand)
    {
        int i = operand.Count;

        while (i > 1 && operand[i - 1] == 0)
            --i;

        operand.RemoveRange(i, operand.Count - i);
    }   
}

这种方法的效率大致与基数转换相当,但这里有一些具体的改进。通过编写一个特殊的平方例程,可以将平方效率加倍,该例程利用ai*bj == aj*bi if a == b这一事实,将乘法次数减半。

此外,还有一些计算加法链的方法,与使用指数位确定平方/乘法计划相比,整体操作总量较少。

帮助程序代码和基准

用于对示例代码生成的元数字(小数肢)中的小数位求和的辅助代码是微不足道的,但为了您的方便,我还是将它发布在此处:

internal class E16_Common
{
    internal static int digit_sum (int limb)
    {
        int sum = 0;

        for ( ; limb > 0; limb /= 10)
            sum += limb % 10;

        return sum;
    }

    internal static int digit_sum (long limb)
    {
        const int M1E9 = 1000000000;

        return digit_sum((int)(limb / M1E9)) + digit_sum((int)(limb % M1E9));
    }

    internal static int digit_sum (IEnumerable<int> limbs)
    {
        return limbs.Aggregate(0, (sum, limb) => sum + digit_sum(limb));
    }

    internal static int digit_sum (IEnumerable<long> limbs)
    {
        return limbs.Select((limb) => digit_sum(limb)).Sum();
    }
}

这可以通过各种方式提高效率,但总体而言并不重要。

所有三个解都需要O(n ^ 2)时间,其中n是指数。换句话说,当指数增长十倍时,它们将花费一百倍的时间。通过采用分治策略,基数转换和重复平方都可以改善到大约O(n log n);我怀疑是否可以通过类似的方式改进加倍方案,但是从一开始就没有竞争力。

此处提供的所有三个解决方案都可用于打印实际结果,方法是使用合适的填充字符串化元数据并将它们连接起来。我将函数编码为返回数字和而不是带有小数肢的数组/列表,只是为了保持示例代码简单并确保所有函数具有相同的签名,以进行基准测试。

在这些基准测试中,.Net BigInteger类型包含如下:

static int digit_sum_via_BigInteger (int power_of_2)
{
    return System.Numerics.BigInteger.Pow(2, power_of_2)
        .ToString()
        .ToCharArray()
        .Select((c) => (int)c - '0')
        .Sum();
}

最后,C#代码的基准:

# testing decimal doubling ...

   1000:    1366 in      0,052 ms
  10000:   13561 in      3,485 ms
 100000:  135178 in    339,530 ms
1000000: 1351546 in 33.505,348 ms

# testing decimal squaring ...

   1000:    1366 in      0,023 ms
  10000:   13561 in      0,299 ms
 100000:  135178 in     24,610 ms
1000000: 1351546 in  2.612,480 ms

# testing radix conversion ...

   1000:    1366 in      0,018 ms
  10000:   13561 in      0,619 ms
 100000:  135178 in     60,618 ms
1000000: 1351546 in  5.944,242 ms

# testing BigInteger + LINQ ...

   1000:    1366 in      0,021 ms
  10000:   13561 in      0,737 ms
 100000:  135178 in     69,331 ms
1000000: 1351546 in  6.723,880 ms

如您所见,基数转换几乎与使用内置BigInteger类的解决方案一样慢。原因是运行时属于较新类型,它只对有符号整数类型执行某些标准优化,但对无符号整数类型执行某些标准优化(此处:通过常量实现除法与反向相乘)。

我还没有找到一种简单的方法来检查现有.Net程序集的本机代码,因此我决定采用不同的调查路径:我编写了E16_RadixConversion的变体进行比较,其中{{1} }和ulong分别由uintlong取代,int相应地减少了1。以下是时间安排:

BITS_PER_WORD

使用无符号类型的版本速度的三倍以上!编译器中有明确的麻木证据......

为了展示不同肢体大小的影响,我将C ++中的解决方案模板化为用作肢体的无符号整数类型。时间以肢体的字节大小和肢体中的小数位数为前缀,用冒号分隔。对于经常出现的操作字符串中的数字字符的情况没有时间,但可以肯定地说,这样的代码至少需要两倍于字节大小的肢体中使用两位数的代码。

# testing radix conv Int63 ...

   1000:    1366 in      0,004 ms
  10000:   13561 in      0,202 ms
 100000:  135178 in     18,414 ms
1000000: 1351546 in  1.834,305 ms

对于10 ^ 6的指数,只有64位肢体的时间,因为我没有耐心等待很多分钟才能获得完整的结果。基数转换的图片类似,但64位肢体没有行,因为我的编译器没有原生的128位整数类型。

# E16_DecimalDoubling

[1:02] e =    1000 ->    1366      0.308 ms
[2:04] e =    1000 ->    1366      0.152 ms
[4:09] e =    1000 ->    1366      0.070 ms
[8:18] e =    1000 ->    1366      0.071 ms

[1:02] e =   10000 ->   13561     30.533 ms
[2:04] e =   10000 ->   13561     13.791 ms
[4:09] e =   10000 ->   13561      6.436 ms
[8:18] e =   10000 ->   13561      2.996 ms

[1:02] e =  100000 ->  135178   2719.600 ms
[2:04] e =  100000 ->  135178   1340.050 ms
[4:09] e =  100000 ->  135178    588.878 ms
[8:18] e =  100000 ->  135178    290.721 ms

[8:18] e = 1000000 -> 1351546  28823.330 ms

有趣的是,简单地将代码编译为C ++并不能使它更快 - 也就是说,优化者无法找到C#抖动错过​​的任何低调的成果,除了不要趾高气扬关于惩罚无符号整数的界限。这就是为什么我喜欢在C#中进行原型设计的原因 - 与(未优化的)C ++相同的性能并没有任何麻烦。

这里是C ++版本的内容(没有像帮手模板这样无聊的东西等等),所以你可以看到我并没有作弊让C#看起来更好:

# E16_RadixConversion

[1:02] e =    1000 ->    1366      0.080 ms
[2:04] e =    1000 ->    1366      0.026 ms
[4:09] e =    1000 ->    1366      0.048 ms

[1:02] e =   10000 ->   13561      4.537 ms
[2:04] e =   10000 ->   13561      0.746 ms
[4:09] e =   10000 ->   13561      0.243 ms

[1:02] e =  100000 ->  135178    445.092 ms
[2:04] e =  100000 ->  135178     68.600 ms
[4:09] e =  100000 ->  135178     19.344 ms

[4:09] e = 1000000 -> 1351546   1925.564 ms

结论

这些基准测试也表明,这个欧拉任务 - 就像许多其他任务一样 - 似乎是在ZX81或Apple上解决的[,而不是我们的现代玩具,它的强度是百万倍。这里没有任何挑战,除非极限增加(指数10 ^ 5或10 ^ 6会更加充分)。

可以从GMP's overview of algorithms获得对实用技术状态的一个很好的概述。另一个优秀的算法概述是&#34;现代计算机算术&#34;理查德布伦特和保罗齐默尔曼。它包含了编码挑战和竞赛需要知道的内容,但不幸的是,深度不等于Donald Knuth在“计算机编程艺术”中的处理方式。

基数转换解决方案为一个代码挑战工具箱添加了一种有用的技术,因为给定的代码可以通过简单的扩展来转换任何旧的大整数而不仅仅是位模式template<typename W> struct E16_RadixConversion { typedef W limb_t; typedef typename detail::E16_traits<W>::long_t long_t; static unsigned const BITS_PER_WORD = sizeof(limb_t) * CHAR_BIT; static unsigned const RADIX_DIGITS = std::numeric_limits<limb_t>::digits10; static limb_t const RADIX = detail::pow10_t<limb_t, RADIX_DIGITS>::RESULT; static unsigned digit_sum_for_power_of_2 (unsigned e) { std::vector<limb_t> digits; compute_digits_for_power_of_2(e, digits); return digit_sum(digits); } static void compute_digits_for_power_of_2 (unsigned e, std::vector<limb_t> &result) { assert(e > 0); unsigned total_digits = unsigned(std::ceil(std::log10(2) * e)); unsigned total_limbs = (total_digits + RADIX_DIGITS - 1) / RADIX_DIGITS; result.resize(0); result.reserve(total_limbs); std::vector<limb_t> bin((e + BITS_PER_WORD) / BITS_PER_WORD); bin.back() = limb_t(limb_t(1) << (e % BITS_PER_WORD)); while (!bin.empty()) { long_t rest = 0; for (std::size_t i = bin.size(); i-- > 0; ) { long_t temp = (rest << BITS_PER_WORD) | bin[i]; long_t quot = temp / RADIX; rest = temp - quot * RADIX; bin[i] = limb_t(quot); } result.push_back(limb_t(rest)); if (bin.back() == 0) bin.pop_back(); } } }; 。重复的平方解决方案可以同样有用,因为将示例代码更改为2以外的其他内容也是微不足道的。

直接以10的幂进行计算的方法对于需要十进制结果的挑战非常有用,因为性能与本机计算处于同一个范围,但不需要单独的转换步骤(可能需要类似的数量)作为实际计算的时间。)