【问题标题】:Calculating Extremely Large Powers of 2计算 2 的极大幂
【发布时间】:2011-05-05 08:37:39
【问题描述】:

我用 Java 编写了一个计算 2 次方的程序,但它似乎效率很低。对于较小的功率(例如 2^4000),它会在不到一秒的时间内完成。但是,我正在计算 2^43112609,它比已知的最大素数大一。拥有超过 1200 万位数字,需要很长时间才能运行。到目前为止,这是我的代码:

import java.io.*;

public class Power
{
 private static byte x = 2;
 private static int y = 43112609;
 private static byte[] a = {x};
 private static byte[] b = {1};
 private static byte[] product;
 private static int size = 2;
 private static int prev = 1;
 private static int count = 0;
 private static int delay = 0;
 public static void main(String[] args) throws IOException
 {
  File f = new File("number.txt");
  FileOutputStream output = new FileOutputStream(f);
  for (int z = 0; z < y; z++)
  {
   product = new byte[size];
   for (int i = 0; i < a.length; i++)
   {
    for (int j = 0; j < b.length; j++)
    {
     product[i+j] += (byte) (a[i] * b[j]);
     checkPlaceValue(i + j);
    }
   }
   b = product;
   for (int i = product.length - 1; i > product.length - 2; i--)
   {
    if (product[i] != 0)
    {
     size++;
     if (delay >= 500) 
     {
      delay = 0;
      System.out.print(".");
     }
     delay++;
    }
   }
  }
  String str = "";
  for (int i = (product[product.length-1] == 0) ? 
   product.length - 2 : product.length - 1; i >= 0; i--)
  {
   System.out.print(product[i]);
   str += product[i];
  }
  output.write(str.getBytes());
  output.flush();
  output.close();
  System.out.println();
 }

 public static void checkPlaceValue(int placeValue)
 {
  if (product[placeValue] > 9)
  {
   byte remainder = (byte) (product[placeValue] / 10);
   product[placeValue] -= 10 * remainder;
   product[placeValue + 1] += remainder;
   checkPlaceValue(placeValue + 1);
  }
 }  
}

这不是为了学校项目或任何东西;就是图个好玩儿。任何有关如何提高效率的帮助将不胜感激!谢谢!

凯尔

附:我没有提到输出应该是 base-10,而不是二进制。

【问题讨论】:

  • 二进制表示非常简单:1000...00 :) 您不仅要计算 2^N,而且要打印为十进制,对吧?
  • 欧拉计划的好任务 :)

标签: java primes


【解决方案1】:

以二进制显示它既简单又快速 - 尽可能快地写入磁盘! 100000……:D

【讨论】:

  • 答案很有趣,但完全正确!作者没有提到他想要什么基数。
  • 其实十六进制和八进制也不难。
  • 哈哈,我在原始帖子中没有提到我希望输出为十进制。但是,是的,这非常聪明(而且是真的)!
【解决方案2】:

如前所述,2 的幂对应于二进制数字。二进制是以 2 为底的,所以每个数字都是前一个数字的两倍。

例如:

    1 = 2^0 = b1
    2 = 2^1 = b10
    4 = 2^2 = b100
    8 = 2^3 = b1000
    ...

二进制是以 2 为底的(这就是为什么它被称为“以 2 为底”,2 是指数的底),所以每个数字都是前一个数字的两倍。移位运算符(在大多数语言中为“

例如:

1 << 6 = 2^6 = 64

作为一个如此简单的二进制操作,大多数处理器可以非常快速地处理可以放入寄存器中的数字(8 - 64 位,具体取决于处理器)。处理更大的数字需要某种类型的抽象(例如 Bignum),但它仍然应该是一个非常快速的操作。不过,对 43112609 位执行此操作需要一些工作。

为了给你一点上下文,2

由于程序如此简单,还可以考虑切换到直接编译成汇编的语言,例如 C。

实际上,生成值是微不足道的(我们已经知道答案,一个 1 后跟 43112609 个零)。转换成十进制需要相当长的时间。

【讨论】:

  • 是的,左移 1 n 次得到 2^n,这在 C 中非常容易和快速。当然,您总是可以将最终输出转换为十进制,无需显示它二进制。
  • 请记住,尽管 C 中的整数太小,无法适应您将要创建的值,因此您需要在它之上进行某种抽象。
【解决方案3】:

正如@John SMith 建议的那样,您可以尝试。 2^4000

    System.out.println(new BigInteger("1").shiftLeft(4000));

编辑:将二进制转换为十进制是 O(n^2) 问题。当你将位数加倍时,每个操作的长度就会加倍,产生的位数也会加倍。

2^100,000 takes 0.166 s
2^1000,000 takes 11.7 s
2^10,000,000 should take 1200 seconds.

注意:所花费的时间完全在 toString() 中,而不是 shiftLeft,即使是 1000 万也需要

【讨论】:

  • ...你用 43112609 试过了吗?我做到了,但声明未能在一小时内完成......
  • 如果我们假设一个操作需要 1 纳秒,那么整个数字将在 516 小时内计算出来。真可怜。
  • 是的,我试过 BigInteger,但在我的电脑上也花了太长时间。
  • 问题是打印小数是 O(N^2) 操作。位数加倍时,产生的位数也会加倍,但每个操作的长度也会加倍。
【解决方案4】:

这里的关键是要注意:

2^2 = 4
2^4 = (2^2)*(2^2)
2^8 = (2^4)*(2^4)
2^16 = (2^8)*(2^8)
2^32 = (2^16)*(2^16)
2^64 = (2^32)*(2^32)
2^128 = (2^64)*(2^64)
... and in total of 25 steps ...
2^33554432 = (2^16777216)*(16777216)

后来:

2^43112609 = (2^33554432) * (2^9558177)

你可以用同样的方法找到剩余的(2^9558177),因为(2^9558177 = 2^8388608 * 2^1169569),你可以用同样的方法找到2^1169569,因为(2^1169569 = 2^1048576 * 2^120993),你可以用同样的方法找到2^120993,等等……

编辑:以前这部分有一个错误,现在已修复:

此外,进一步简化和优化注意到:

2^43112609 = 2^(0b10100100011101100010100001)
2^43112609 = 
      (2^(1*33554432))
    * (2^(0*16777216))
    * (2^(1*8388608))
    * (2^(0*4194304))
    * (2^(0*2097152))
    * (2^(1*1048576))
    * (2^(0*524288))
    * (2^(0*262144))
    * (2^(0*131072))
    * (2^(1*65536))
    * (2^(1*32768))
    * (2^(1*16384))
    * (2^(0*8192))
    * (2^(1*4096))
    * (2^(1*2048))
    * (2^(0*1024))
    * (2^(0*512))
    * (2^(0*256))
    * (2^(1*128))
    * (2^(0*64))
    * (2^(1*32))
    * (2^(0*16))
    * (2^(0*8))
    * (2^(0*4))
    * (2^(0*2))
    * (2^(1*1))

还要注意2^(0*n) = 2^0 = 1

使用此算法,您可以在 25 次乘法中计算 2^1、2^2、2^4、2^8、2^16 ... 2^33554432 的表。然后您可以将43112609 转换为其二进制表示,并使用少于25 次乘法轻松找到2^43112609。总的来说,您需要使用少于 50 次乘法来找到任何 2^n,其中 n 介于 0 和 67108864 之间。

【讨论】:

  • 但是“25 次加法”和“25 次乘法”中操作数的大小是多少?
  • 如果能生成一个一个一个或一个小组发出数字的算法真的很酷。
  • @Andrey:乘法将是巨大的,但与进行 43112609 乘法的天真方法相比,它仍然是一个巨大的节省。但是,我猜位数将少于 43112609 位(以 10 为基数),因为 2^43112609 以 2 为基数占用 43112609 位,而以 10 为基数的位数总是更少。
  • @meriton:是的,我犯了一个错误,不过现在已经修复了。
  • @Lie Ryan 好吧,与这样的操作数相乘需要很长时间,所以效率不会很高。但我认为你的想法比原来的要好得多。关于数字估计有规则2^10 ~ 10^3
【解决方案5】:

另一个需要注意的关键是,您的 CPU 在将整数和长整数相乘时比在 Java 中进行长整数乘法要快得多。将该数字分成长(64 字节)块,然后乘以并携带这些块而不是单个数字。再加上前面的答案(使用平方而不是 2 的顺序乘法)可能会加快 100 倍或更多。

编辑

我尝试编写一个分块和平方方法,它的运行速度比 BigInteger 稍慢(13.5 秒对 11.5 秒计算 2^524288)。在做了一些计时和实验之后,最快的方法似乎是与 BigInteger 类重复平方:

    public static String pow3(int n) {
    BigInteger bigint = new BigInteger("2");
    while (n > 1) {
        bigint = bigint.pow(2);
        n /= 2;
    }
    return bigint.toString();
}
  • 2 次幂次幂的一些计时结果(一些 n 为 2^(2^n))
  • 131072 - 0.83 秒
  • 262144 - 3.02 秒
  • 524288 - 11.75 秒
  • 1048576 - 49.66 秒

以这种增长速度,计算 2^33554432 大约需要 77 小时,更不用说存储和将所有幂加在一起得到最终结果 2^43112609 的时间。

编辑 2

实际上,对于非常大的指数,BigInteger.ShiftLeft 方法是最快的。我估计使用 ShiftLeft 的 2^33554432 大约需要 28-30 小时。想知道 C 或汇编版本需要多快...

【讨论】:

  • 我不熟悉 Java 的 BigInteger 实现,但是虽然我不怀疑它能够用 ShiftLefts 快速计算 2^33554432,但我怀疑它是否可以将其转换为小数 在您估计的 28-30 小时内。如前所述,计算二进制表示(或 BigInteger 的 base-32 表示)是微不足道的,它是二进制->十进制转换需要时间。
  • 我根据计算 2 的幂并将它们转换为 .toString() 并打印出来,得到十进制表示。
【解决方案6】:

令 n = 43112609。

假设:您想以十进制打印 2^n。

虽然填充位向量而不是二进制表示 2^n 是微不足道的,但将该数字转换为十进制表示法需要一段时间。例如,java.math.BigInteger.toString 的实现需要 O(n^2) 次操作。这可能就是为什么

BigInteger.ONE.shiftLeft(43112609).toString()

执行一小时后仍未终止...

让我们从您的算法的渐近分析开始。您的外部循环将执行 n 次。对于每次迭代,您将执行另一个 O(n^2) 操作。也就是说,您的算法是 O(n^3),因此预计可扩展性较差。

您可以通过使用将其减少到 O(n^2 log n)

x^64 = x^(2*2*2*2*2*2) = ((((((x^2)^2)^2)^2)^2)^2

(只需要8次乘法)而不是64次乘法

x^64 = x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x *x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x *x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x

(推广到任意指数作为练习留给您。提示:将指数写为二进制数 - 或查看 Lie Ryan 的答案)。

为了加快乘法速度,您可以使用Karatsuba Algorithm,将整体运行时间减少到 O(n^((log 3)/(log 2)) log n)。

【讨论】:

  • 如果我们假设一个操作需要 1 纳秒,那么整个数字将在 516 小时内计算出来。真可怜。
  • 如果连 Karatsuba 都不够快,请尝试 Schönhage–Strassen 算法:In practice the Schönhage–Strassen algorithm starts to outperform older methods such as Karatsuba and Toom–Cook multiplication for numbers beyond 2^(2^15) to 2^(2^17) (10,000 to 40,000 decimal digits).[4][5][6](来自 Wikipedia)。 OP的数字大约是2^(2^25)。
  • 安德烈,你是怎么得出这个数字的?您为 O 表示法隐藏的常数假设什么值?你怎么给我的算法和 O(n^2) 一样的时间估计?
【解决方案7】:

因为一个人实际上想要结果的所有数字(不像,例如 RSA,其中一个人只对残差 mod 一个比我们这里的数字小得多的数字感兴趣)我认为最好的方法可能是提取九使用乘法实现的长除法一次十进制数字。从残差为零开始,依次对每个 32 位应用以下内容(MSB 优先)

残基 =(残基 SHL 32)+数据 结果 = 0 温度 = (残基 >> 30) 温度 += (温度*316718722) >> 32 结果+=温度; 残留物-=温度* 1000000000; while (residue >= 1000000000) /* 我认为这个循环不会运行超过两次 */ { 结果++; 残基-= 1000000000; }

然后将结果存储在刚刚读取的 32 位中,并循环遍历每个低位字。最后一步之后的余数将是结果的后九位小数。由于以二进制形式计算 2 的幂既快速又简单,我认为除以转换为十进制可能是最好的方法。

顺便说一句,这在 vb.net 中计算 2^640000 大约需要 15 秒,所以 2^43112609 应该大约需要五个小时来计算所有 12,978,188 位。

【讨论】:

  • 我在弄清楚它是如何工作的时候遇到了一些麻烦。如果它可以在五个小时内完成,我会说这是对我的代码的巨大改进。
  • 基本上,序列是“将下一个“数字”移入余数;余数 div 1E9 是结果的下一个“数字”;余数 mod 1E9 是进入下一步的余数。就像人们会做的那样铅笔和纸,除了十亿基数而不是十亿基数。其他东西是对 div/mod 十亿的硬编码优化。它计算一个近似值并对其进行调整直到它是精确的。近似值可能会得到改进,但是挺好的。
  • 总的来说,目标只是通过反复说来拉出数字:next_digit is bignumber mod 1E9;大数 = 大数 div 1E9。所以每次通过都会产生九个十进制数字。
猜你喜欢
  • 2019-12-05
  • 2018-05-28
  • 1970-01-01
  • 2013-03-09
  • 2013-04-06
  • 1970-01-01
  • 2015-07-22
  • 2014-02-21
  • 1970-01-01
相关资源
最近更新 更多