【问题标题】:Project Euler #16 - C# 2.0欧拉计划 #16 - C# 2.0
【发布时间】:2009-03-24 14:13:15
【问题描述】:

我一直在与 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,您有什么建议(不是答案!)?什么方法可行?

注意:如果您决定发布答案,请在您的尝试前加上带有###Spoiler 的块引用。

【问题讨论】:

  • 问得很好,不要求直接回答。
  • @Brian R. Bondy:我知道我错过了看待问题的不同方式,我希望有人能让我知道看待问题的不同方式,以便我能解决并将该思维过程添加到我的曲目中。
  • 这个问题链接在this meta post。按照目前的标准,它太宽泛了。也许它应该被删除,@GeorgeStocker?

标签: c# .net


【解决方案1】:

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

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

21 = 2
22 = 21 + 21
23 = 22 + 22

例子:

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

剧透警告:C# 中的算法和解决方案如下。

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

  • 作为字符串;
  • 作为字符或数字数组。

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

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

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 位有符号整数可以存储 +/- 20 亿之间的数字(大约。9 位数字等于 10 亿,因此您可以使用 32 位整数(有符号或无符号)作为基本的十亿“数字”。您可以工作找出您需要多少个整数,创建该数组,这就是运行整个算法所需的所有存储空间(大约 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;
    }

【讨论】:

  • 有趣的是,我用 C# 解决了这个问题,方式与此不太相似,我在 4 秒内计算了 2^100000 的数字总和。 2^1000 几乎是瞬时的。您可以将其称为 CS101 答案。我称之为不直接回答。 (根据要求)
  • 2^3 = 8. 2^2 = 4. 2^3 怎么不等于 2^2 + 2^2?
  • 前 5 句话可能直接来自 CS101 书籍。我知道的那部分;它只是弄清楚如何利用二进制算术有帮助。我的高等数学不强,所以我提出这个问题是为了帮助我以新的方式思考这些事情。
  • 因为它是 2^2 + 2^1。右边的指数之和等于左边的指数。
  • 看看这是我的超级特别数学,你不可能理解。要么那个,要么我假设你是在乘法而不是加法,尽管事实上我在“纠正”你的时候甚至复制了加法符号。
【解决方案2】:

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

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

剧透如下

我最终使用了一个 int 数组 模拟加法机的值, 但我把数字倒过来 - 你可以这样做,因为问题只要求数字的总和, 这意味着顺序无关紧要。

你实际上在做的是 将值加倍 1000 倍,所以你 可以将存储在 数组的第一个元素,然后 继续循环直到你的值是 10岁以上。这就是你将拥有的地方 跟踪套利值。这 大于 10 的 2 的第一个幂是 16、所以数组中的元素经过 第 5 次迭代是 6 和 1。

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

继续这个过程 1000 次,然后 你将有一个大约 600 个数组 您可以轻松添加的元素。

【讨论】:

    【解决方案3】:

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

    我刚刚写了一个Multiply 扩展方法,它接受一个IEnumerable&lt;int&gt; 和一个乘数并返回一个IEnumerable&lt;int&gt;。 (每个 int 代表一个数字,第一个是最低有效位。)然后我刚刚创建了一个包含项目 { 1 } 的列表并将其乘以 2 a 1000 次。使用Sum 扩展方法在列表中添加项目很简单。

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

    【讨论】:

      【解决方案4】:

      假装你很年轻,用方形纸。对我来说,这就像一个数字列表。然后将其加倍,您将每个数字加倍,然后通过减去 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);
          }
      }
      

      【讨论】:

      • 我不确定它是好还是可怕,非常可悲的是,我可以在不做另一个 ROT13 的情况下阅读它。
      • 我现在看到的都是金发、黑发、红发。
      【解决方案5】:

      如果您希望在 C# 中进行主要计算,您将需要某种大整数实现(很像 C/C++ 的 gmp)。编程就是为正确的工作使用正确的工具。如果你找不到一个好的 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
      

      【讨论】:

      • 啊,你是对的。固定的。我一直在考虑数组索引并做了 len - 1。
      • 相当冗长:sum(map(int, str(2**1000)))
      • sum([int(x) for x in str(2**100000)])
      【解决方案6】:

      如果您有 ruby​​,您可以轻松计算“2**1000”并将其作为字符串获取。应该是在 C# 中轻松剪切/粘贴到字符串中。

      剧透

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

      【讨论】:

      • 同样是在 Perl 中;但这并不能真正帮助我使用 C# 2.0 的限制来解决它。不过,必须有办法解决它;提出这个问题是为了帮助我找出可能的路径。
      • 抱歉,我认为这是对这个值的回答,而不是创建通用解决方案。
      【解决方案7】:

      剧透

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

      这里有一个线索:

      用列表表示每个数字。这样你就可以做一些基本的总结,比如:

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

      【讨论】:

        【解决方案8】:

        将数字表示为整数序列的另一种方法是将数字基数 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 的最后十位数字并重复加法,则可能需要一些时间(尽管在这种情况下您可以使用模算术,而不是添加数百万位数的字符串))。

        【讨论】:

          【解决方案9】:

          我也在这里发布的工作解决方案: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);
          }
          }
          

          【讨论】:

            【解决方案10】:

            Euler problem #16 已在这里讨论过很多次,但我找不到可以很好地概述可能的解决方案的答案,就像它的情况一样。这是我试图纠正的尝试。

            本概述适用于已经找到解决方案并希望获得更全面了解的人。即使示例代码是 C#,它也基本上与语言无关。有一些在 C# 2.0 中不可用的功能的用法,但它们不是必需的 - 它们的目的只是让无聊的东西以最少的大惊小怪消失。

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

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

            • 重复加倍
            • 通过重复平方供电

            原生计算 + 基数转换

            这种方法是最简单的,它的性能超过了使用 .Net 的内置 BigInteger 类型的幼稚解决方案。

            实际计算很容易实现:只需执行1 &lt;&lt; 1000 的道德等价物,即存储 1000 个二进制零并附加一个单独的二进制 1。

            转换也很简单,可以通过编写铅笔和纸的划分方法来完成,为了提高效率,可以选择适当大的“数字”。中间结果的变量需要能够容纳两个“数字”;将适合 long 的小数位数除以 2 得到 9 个小数位,表示最大元数字(或“肢体”,因为它通常在 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 &lt;&lt; BITS_PER_WORD) | big[i] 而不是使用运算符+,因为这正是这里需要的;不需要进行带有进位传播的 64 位加法。这意味着这两个操作数可以直接写入寄存器对中它们各自的寄存器,或者写入像LARGE_INTEGER这样的等效结构中的字段。

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

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

            十进制加倍

            重复加倍似乎是每个人的最爱,所以接下来让我们这样做。中间结果的变量需要保存一个“数字”加上一个进位位,对于long,每个肢体有 18 个数字。转到ulong 并不能改善情况(19 位数字加上进位缺少 0.04 位),所以我们不妨坚持使用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 位小数的巨大元数字)。原因是代码必须执行 (exponent - 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 3 次平方得到,x^2^5 可以通过 5 次平方得到,以此类推。在二进制计算机上,很容易将指数分解为 2 的幂,因为它是表示该数字的位模式。然而,即使是非二进制计算机也应该能够测试一个数字是奇数还是偶数,或者将一个数字除以二。

            乘法可以用铅笔纸法编码;在这里,我使用了一个辅助函数,该函数计算一行乘积并将其添加到结果中适当移动的位置,以便以后不需要存储部分乘积的行以用于单独的加法步骤。计算过程中的中间值最多可以有两个“数字”大小,因此四肢的宽度只能是重复加倍时的一半(其中除了“数字”之外,只需要添加一个额外的位)。

            注意:计算的基数不是 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 的一个变体,以便比较 ulonguint分别被longint替换,BITS_PER_WORD相应减少1。以下是时间安排:

            # 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
            

            比使用无符号类型的版本快三倍多!编译器中 numbskullery 的明显证据......

            为了展示不同肢体大小的影响,我在 C++ 中对用作肢体的无符号整数类型的解决方案进行了模板化。计时以肢体的字节大小和肢体中的小数位数为前缀,用冒号分隔。对于在字符串中处理数字字符的常见情况没有时间安排,但可以肯定地说,这样的代码所花费的时间至少是在字节大小的肢体中使用两位数的代码的两倍。

            # 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
            

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

            # 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
            

            有趣的是,简单地将代码编译为 C++ 并不会使其更快 - 即,优化器找不到 C# 抖动错过的任何低悬的果实,除了不遵守关于惩罚无符号整数。这就是我喜欢在 C# 中进行原型设计的原因——性能与(未优化的)C++ 相同,而且没有任何麻烦。

            这里是 C++ 版本的精髓(没有大量无聊的东西,如帮助模板等),因此您可以看到我并没有为了让 C# 看起来更好而作弊:

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

            结论

            这些基准测试还表明,与许多其他任务一样,这项 Euler 任务似乎旨在在 ZX81 或 Apple ][ 上解决,而不是在功能强大一百万倍的现代玩具上解决。除非限制大幅增加(10^5 或 10^6 的指数更合适),否则这里没有任何挑战。

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

            基数转换解决方案为代码挑战工具箱添加了一项有用的技术,因为可以简单地扩展给定的代码以转换任何旧的大整数,而不仅仅是位模式1 &lt;&lt; exponent。重复平方解也同样有用,因为将示例代码更改为除 2 以外的其他值也很简单。

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多