检查长整数是不是为立方体的快速方法(在Java中)
Posted
技术标签:
【中文标题】检查长整数是不是为立方体的快速方法(在Java中)【英文标题】:Fast way to check if long integer is a cube (in Java)检查长整数是否为立方体的快速方法(在Java中) 【发布时间】:2015-08-14 19:23:14 【问题描述】:我正在编写一个程序,我需要检查某些大数(立方体的排列)是否是立方的(对于某些 n,等于 n^3)。
目前我只是使用方法
static boolean isCube(long input)
double cubeRoot = Math.pow(input,1.0/3.0);
return Math.round(cubeRoot) == cubeRoot;
但是在处理大数字(10 位以上)时,这非常慢。有没有更快的方法来确定整数是否是立方体?
【问题讨论】:
Math.cbrt(input) 是否比 Math.pow(input,1.0/3.0) 快? ? 我应该确实包括在内,这就是我之前使用的,并且花费了大约相同的时间 一个有点正交的建议是分析您的程序并检查isCube
是否确实是一个瓶颈。听起来像是排列数字,或将数字转换为长整数或双精度数字,实际上可能碰巧比检查数字是否为立方体要慢。
确实,我检查过,两者都是瓶颈。我现在专注于改进排列部分
【参考方案1】:
只有 2^21 个立方体不会溢出 long(如果允许负数,则为 2^22 - 1),因此您可以只使用 HashSet 查找。
【讨论】:
这将使您几乎每次调用都会导致缓存未命中,除非您将其称为 lot 或每次都使用类似的值。事实证明有一个整数立方根的整数算法,来自 Hacker's Delight。如果高性能值得花时间(和内存,以及预计算或嵌入式数据大小),那么请参阅我的答案。但是 +1 因为这很简单。【参考方案2】:Hacker's Delight 这本书有一个简短而快速的整数立方根函数,值得移植到 64 位长整数,见下文。
似乎测试一个数字是否是一个完美的立方比实际计算立方根更快。 Burningmath has a technique 使用“digital root”(对数字求和。重复直到它是一个数字)。如果数字根是 0、1 或 8,那么您的数字可能是一个完美的立方体。
此方法对于您置换(数字?)数字的情况可能非常有价值。如果你可以通过数字根排除一个数字,那么所有排列也都被排除了。
他们还描述了一种基于检查完美立方体的主要因素的技术。这看起来最适合心算,因为我认为因式分解比计算机上的立方根要慢。
无论如何,数字根对计算机来说很快,您甚至可以将数字作为一串数字开头。您仍然需要一个除以 10 的循环,但您的起点是输入的数字总和,而不是整数,因此不会有很多除法。 (整数除法比当前 CPU 上的乘法慢一个数量级,但除以编译时间常数可以是 optimized to multiply+shift with a fixed-point inverse。希望 Java JIT 编译器也使用它,甚至可能将它用于运行时常数。)
加上A. Webb's test(input % 819
-> 搜索 45 个条目的表)将排除 很多 输入作为不可能的完美立方体。
IDK 如果二分搜索、线性搜索或哈希/集是最好的。
这些测试可能是David Eisenstat's idea 的前端,它只是将一组long
s 存储在允许快速存在检查的数据结构中的完美立方体中。 (例如HashSet)。是的,缓存未命中的代价很高,至少在进行 HashSet 查找之前进行数字根测试可能是值得的,也许两者兼而有之。
您可以通过将它用于Bloom Filter 而不是精确集合 (David Ehrman's suggestion) 来使用更少的内存。这将为完整计算提供另一个候选拒绝前端。 guavac BloomFilter
implementation 需要一个“漏斗”函数来将对象转换为字节,在这种情况下应该是 f(x)=x)。
我怀疑 Bloom 过滤不会比精确的 HashSet 检查大获全胜,因为它需要多次内存访问。当您真的负担不起完整表的空间时,这是合适的,并且您要过滤掉的是非常昂贵的东西,例如磁盘访问。
整数立方根函数(如下)可能比单个缓存未命中更快。如果 cbrt 检查导致缓存未命中,那么当其数据被逐出时,您的其余代码可能也会遭受更多的缓存未命中。
Math.SE 有一个 question about this for perfect squares,但那是关于正方形,而不是立方体,所以这些都没有出现。不过,那里的答案确实讨论并避免了您方法中的问题。 >.
你的方法有几个问题:
The problem with using pow(x, 1./3)
is that 1/3 does not have an exact representation in floating point, so you're not "really" getting the cube root. 所以使用cbrt
。它不太可能变慢,除非它具有更高的准确性,但需要时间成本。
您假设Math.pow
或Math.cbrt
总是返回一个正好是整数的值,而不是 41.999999 或其他值。 Java docs说:
计算结果必须在精确结果的 1 ulp 范围内。
这意味着您的代码可能无法在符合标准的 Java 实现上运行。 比较浮点数是否完全相等是一件棘手的事情。 What Every Computer Scientist Should Know About Floating-Point Arithmetic 对浮点有很多话要说,但真的很长。 (有充分的理由。浮点数很容易使自己陷入困境。)另请参阅Comparing Floating Point Numbers, 2012 Edition,Bruce Dawson 的系列 FP 文章。
我认为它不适用于所有 long
值。 double
只能精确表示最大为 2^53 的整数(64 位 IEEE 双精度中尾数的大小)。 Math.cbrt
不能精确表示的整数更不可能是精确整数。
FP 立方根,然后测试得到的整数,避免了 FP 比较引入的所有问题:
static boolean isCube(long input)
double cubeRoot = Math.cbrt(input);
long intRoot = Math.round(cubeRoot);
return (intRoot*intRoot*intRoot) == input;
(四处搜索后,我在其他 *** / stackexchange 的答案中看到其他人也建议使用整数比较方法。)
如果您需要高性能,并且不介意拥有更复杂的功能和更多的源代码,那么就有可能。例如,使用具有整数数学的立方根逐次逼近算法。如果你最终达到n^3 < input <
(n+1)^3, then
input` 不是立方体的地步。
this math.SE question上有一些方法的讨论。
我不打算花时间详细研究整数立方根算法,因为cbrt
部分可能不是主要瓶颈。可能输入解析和字符串->长转换是瓶颈的主要部分。
其实我很好奇。原来在Hacker's Delight 中已经有一个整数立方根实现(使用/复制/分发even without attribution is allowed。AFAICT,它本质上是公共域代码。):
// Hacker's delight integer cube-root (for 32-bit integers, I think)
int icbrt1(unsigned x)
int s;
unsigned y, b;
y = 0;
for (s = 30; s >= 0; s = s - 3)
y = 2*y;
b = (3*y*(y + 1) + 1) << s;
if (x >= b)
x = x - b;
y = y + 1;
return y;
根据int
中的位数,30
看起来像一个幻数。将此移植到long
需要测试。 (另请注意,这是 C,但看起来它也应该用 Java 编译!)
IDK 如果这是 Java 人的常识,但 32 位 Windows JVM 不使用 server
JIT 引擎,也不会优化您的代码。
【讨论】:
感谢您的精彩回复。我现在已经从几分钟缩短到 2 秒(这是 Project Euler #62,顺便说一句)。关于排列,我发现的主要事情是,与其对一个长整数进行所有排列然后遍历一个巨大的列表,而是更快地查看两个长整数是否是彼此的字谜。我正在使用按位运算来做这件事,希望这能节省我正在做的事情的更多时间。[pre-check digital root] could be extremely valuable for your case of permuting (the digits of?) numbers
对于permutations of cubes
的情况应该没有意义(如果置换十进制表示的数字确实是permutations
应该在问题中)。否则,它会排除五分之三的情况 - 甚至在将字符转换为数字之前。
@greybeard,我这样做了,也放弃了只使用字谜匹配的想法(我意识到我排除了很多潜在的命中)。即使使用pre-check digital root
,在计算这些long
整数的排列时,我也会收到Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
错误。这当然是一个计算成本很高的问题。我最终可能会尝试使用布隆过滤器。在我遇到这个线程之前从未听说过它。有趣的概念。哦,是的,这就是 permutations
的含义(所以,即使不包括 3/5,仍然有船载)。
我说的是倒退:我首先计算给定long int
的所有排列,然后根据pre-check digital root
将它们从列表中过滤出来,如果值小于当前值n
正在测试,如果它们已经在多维数据集列表中。难怪这会在记忆中出现。【参考方案3】:
您可以首先通过对给定数字取模来消除大量候选人。例如,以数字 819
为模的立方体只能采用以下 45
值。
0 125 181 818 720 811 532 755 476
1 216 90 307 377 694 350 567 442
8 343 559 629 658 351 190 91 469
27 512 287 252 638 118 603 161 441
64 729 99 701 792 378 260 468 728
因此,在几乎 95% 的均匀分布情况下,您实际上不必计算三次根。
【讨论】:
对不起,可能是个愚蠢的问题,819 是从哪里来的? @Stas 我怀疑我刚刚搜索过它,例如在 R 中:which.min(lapply(1:1024,function(x) length(unique((1:x)^3 %% x))/x))
返回 819。我们希望唯一余数的百分比最低,并且余数能够放入一个相当小的表中。
@Stas 819 = 9*7*13,因此 Z/819 = Z/9 x Z/7 x Z/13。选择这些素数是因为通常,Z/p^n 的乘法群是 (p-1)*p^n-1 阶循环的。如果这个顺序可以被 3 整除,那么这些元素中只有 1/3 可以是立方体。因此,唯一有帮助的方法是如果 n=2、p=3(任何更高的 n 对您没有帮助)或 p == 1(mod 3)。前两个素数 == 1 (mod 3) 是 7 和 13。如果您想要更好的百分比,可以乘以 19(下一个素数 == 1 (mod 3))。【参考方案4】:
如果您只是将 int 更改为 long 并将 30 更改为 60,那么黑客程序似乎适用于长数字。如果您将 30 更改为 61,它似乎不起作用。
我不是很了解这个程序,所以我制作了另一个似乎可以在 Java 中运行的版本。
private static int cubeRoot(long n)
final int MAX_POWER = 21;
int power = MAX_POWER;
long factor;
long root = 0;
long next, square, cube;
while (power >= 0)
factor = 1 << power;
next = root + factor;
while (true)
if (next > n)
break;
if (n / next < next)
break;
square = next * next;
if (n / square < next)
break;
cube = square * next;
if (cube > n)
break;
root = next;
next += factor;
--power;
return (int) root;
【讨论】:
【参考方案5】:请定义很秀。这是一个测试程序:
public static void main(String[] args)
for (long v = 1; v > 0; v = v * 10)
long start = System.nanoTime();
for (int i = 0; i < 100; i++)
isCube(v);
long end = System.nanoTime();
System.out.println(v + ": " + (end - start) + "ns");
static boolean isCube(long input)
double cubeRoot = Math.pow(input,1.0/3.0);
return Math.round(cubeRoot) == cubeRoot;
输出是:
1: 290528ns
10: 46188ns
100: 45332ns
1000: 46188ns
10000: 46188ns
100000: 46473ns
1000000: 46188ns
10000000: 45048ns
100000000: 45048ns
1000000000: 44763ns
10000000000: 45048ns
100000000000: 44477ns
1000000000000: 45047ns
10000000000000: 46473ns
100000000000000: 47044ns
1000000000000000: 46188ns
10000000000000000: 65291ns
100000000000000000: 45047ns
1000000000000000000: 44477ns
我没有看到“大”数字对性能的影响。
【讨论】:
虽然有帮助,但不能作为“答案”。最好使用在线编译器来演示您的代码和输出,并在评论中分享链接。 我怀疑对isCube
的调用只是被优化了,因为结果没有发生任何事情。
@resueman 是的,你是对的,这最终会发生。如果把内循环改成10000,0和10的性能高,其余都是0ns。
@Andreas:由于任何数量的热身效应,例如 CPU 空闲与涡轮频率、缓存、JIT,前几次迭代可能很慢。你也没有说哪个JVM。 64 位 Oracle 和 OpenJDK JVM 默认为 server
VM,这对 JIT 更具侵略性,并且可能会优化掉循环。 44us 虽然是很多 CPU 周期;希望nanoTime()
没有那么多开销。
@Andreas:微基准测试流程的哪些部分较慢的总体思路是一个好主意,或者更好的是,使用分析器。不过,这不是一个很好的测试。我希望每个数字的一些在 string->float 转换中,cbrt
的成本可能相当稳定。 (即使结果不是整数,它也必须使结果达到完整的double
精度。)以上是关于检查长整数是不是为立方体的快速方法(在Java中)的主要内容,如果未能解决你的问题,请参考以下文章