确定整数的平方根是否为整数的最快方法


1453

我正在寻找确定一个long值是否为完美平方(即其平方根是另一个整数)的最快方法:

  1. 通过使用内置Math.sqrt() 函数,我已经完成了简单的方法,但是我想知道是否有一种方法可以通过将自己限制为仅整数域来更快地完成操作。
  2. 维护查找表是不切实际的(因为大约有2 31.5个整数,其平方小于2 63)。

这是我现在做的非常简单明了的方法:

public final static boolean isPerfectSquare(long n)
{
  if (n < 0)
    return false;

  long tst = (long)(Math.sqrt(n) + 0.5);
  return tst*tst == n;
}

注意:我在许多Project Euler问题中都使用了此功能。因此,没有其他人将不得不维护此代码。这种微优化实际上可能会有所作为,因为挑战的一部分是在不到一分钟的时间内完成每种算法,并且在某些问题中,需要数百万次调用此函数。


我已经尝试过不同的解决方案:

  • 经过详尽的测试后,我发现0.5没有必要将Math.sqrt()的结果添加进去,至少在我的机器上没有。
  • 平方根倒数快速增快,但它给了不正确的结果对于n> = 410881.然而,所建议BobbyShaftoe,我们可以使用对于n <410881的FISR黑客攻击。
  • 牛顿的方法比慢Math.sqrt()。这可能是因为Math.sqrt()使用了类似于牛顿方法的方法,但是是在硬件中实现的,因此它比Java快得多。同样,牛顿法仍然需要使用双精度。
  • 一种经过改进的牛顿方法,其中使用了一些技巧,以便仅涉及整数数学运算,因此需要一些技巧来避免溢出(我希望此函数与所有正的64位带符号整数一起使用),但它仍然比慢Math.sqrt()
  • 二进制印章甚至更慢。这是有道理的,因为二进制印章平均需要16次通过才能找到64位数字的平方根。
  • 根据John的测试,or在C ++中,使用语句比使用a更快switch,但是在Java和C#中,or和之间似乎没有区别switch
  • 我还尝试制作一个查找表(作为64个布尔值的私有静态数组)。然后or,我只是说而不是switch或statement if(lookup[(int)(n&0x3F)]) { test } else return false;。令我惊讶的是,这慢了一点。这是因为数组边界是在Java中检查的

21
这是Java代码,其中int == 32位和long == 64位,并且均已签名。
基普

14
@Shreevasta:我已经对大值(大于2 ^ 53)进行了一些测试,并且您的方法给出了一些误报。遇到的第一个是n = 9007199326062755,这不是一个完美的正方形,但会作为一个整数返回。
Kip

37
请不要将其称为“ John Carmack骇客”。他没有提出。
user9282 2009年

84
@mamama-也许是,但是这归因于他。亨利·福特(Henry Ford)并没有发明汽车,莱特兄弟(Wright Bros.)也没有发明飞机,加勒里奥(Galleleo)也不是第一个发现地球绕太阳旋转的人……世界是由被盗的发明组成的爱)。
罗伯特·弗雷泽

4
通过使用类似的东西((1<<(n&15))|65004) != 0,而不是进行三个单独的检查,您可能会在“快速失败”中获得极小的速度提高。
2011年

Answers:


735

我想出了一种方法,至少在我的CPU(x86)和编程语言(C / C ++)上,它的工作速度比6bits + Cackack + sqrt代码快35%。您的结果可能会有所不同,尤其是因为我不知道Java因素将如何发挥作用。

我的方法是三方面的:

  1. 首先,过滤出明显的答案。这包括负数并查看最后4位。(我发现查看最后六个没有帮助。)我也对0回答是。(在阅读下面的代码时,请注意我的输入是int64 x。)
    if( x < 0 || (x&2) || ((x & 7) == 5) || ((x & 11) == 8) )
        return false;
    if( x == 0 )
        return true;
  2. 接下来,检查它是否是平方模255 = 3 * 5 *17。因为这是三个不同素数的乘积,所以残差mod 255中只有约1/8是平方。但是,根据我的经验,调用模运算符(%)所花费的成本要比一个人获得的收益高,因此我使用涉及255 = 2 ^ 8-1的位技巧来计算残差。(无论好坏,我都没有使用从单词中读取单个字节的技巧,而只是按位与和移位。)
    int64 y = x;
    y = (y & 4294967295LL) + (y >> 32); 
    y = (y & 65535) + (y >> 16);
    y = (y & 255) + ((y >> 8) & 255) + (y >> 16);
    // At this point, y is between 0 and 511.  More code can reduce it farther.
    要实际检查残差是否为正方形,我在预先计算的表中查找答案。
    if( bad255[y] )
        return false;
    // However, I just use a table of size 512
  3. 最后,尝试使用类似于Hensel引理的方法计算平方根。(我认为它不直接适用,但可以进行一些修改。)在此之前,我用二进制搜索将2的所有幂除掉:
    if((x & 4294967295LL) == 0)
        x >>= 32;
    if((x & 65535) == 0)
        x >>= 16;
    if((x & 255) == 0)
        x >>= 8;
    if((x & 15) == 0)
        x >>= 4;
    if((x & 3) == 0)
        x >>= 2;
    在这一点上,我们的数字必须是平方,必须是1模8。
    if((x & 7) != 1)
        return false;
    Hensel引理的基本结构如下。(注意:未经测试的代码;如果无效,请尝试t = 2或8。)
    int64 t = 4, r = 1;
    t <<= 1; r += ((x - r * r) & t) >> 1;
    t <<= 1; r += ((x - r * r) & t) >> 1;
    t <<= 1; r += ((x - r * r) & t) >> 1;
    // Repeat until t is 2^33 or so.  Use a loop if you want.
    这个想法是,在每次迭代中,您需要在x的“当前”平方根r上加一位。每个平方根都是2的幂的乘积,即t / 2。最后,r和t / 2-r将是x以t / 2为模的平方根。(请注意,如果r是x的平方根,那么-r也是。 )因为我们的实际平方根小于2 ^ 32,所以实际上我们可以仅检查r或t / 2-r是否为实平方根。在我的实际代码中,我使用以下修改后的循环:
    int64 r, t, z;
    r = start[(x >> 3) & 1023];
    do {
        z = x - r * r;
        if( z == 0 )
            return true;
        if( z < 0 )
            return false;
        t = z & (-z);
        r += (z & t) >> 1;
        if( r > (t >> 1) )
            r = t - r;
    } while( t <= (1LL << 33) );
    这里的加速可以通过三种方式获得:预先计算的起始值(相当于循环的约10次迭代),循环的较早退出以及跳过一些t值。在最后一部分中,我查看z = r - x * x,并将t设置为2的最大幂除以一点技巧。这使我可以跳过不会影响r值的t值。在我的情况下,预先计算的起始值选取了“最小正”平方根模8192。

即使这段代码无法为您更快地工作,我希望您喜欢其中包含的一些想法。随后是完整的,经过测试的代码,包括预先计算的表。

typedef signed long long int int64;

int start[1024] =
{1,3,1769,5,1937,1741,7,1451,479,157,9,91,945,659,1817,11,
1983,707,1321,1211,1071,13,1479,405,415,1501,1609,741,15,339,1703,203,
129,1411,873,1669,17,1715,1145,1835,351,1251,887,1573,975,19,1127,395,
1855,1981,425,453,1105,653,327,21,287,93,713,1691,1935,301,551,587,
257,1277,23,763,1903,1075,1799,1877,223,1437,1783,859,1201,621,25,779,
1727,573,471,1979,815,1293,825,363,159,1315,183,27,241,941,601,971,
385,131,919,901,273,435,647,1493,95,29,1417,805,719,1261,1177,1163,
1599,835,1367,315,1361,1933,1977,747,31,1373,1079,1637,1679,1581,1753,1355,
513,1539,1815,1531,1647,205,505,1109,33,1379,521,1627,1457,1901,1767,1547,
1471,1853,1833,1349,559,1523,967,1131,97,35,1975,795,497,1875,1191,1739,
641,1149,1385,133,529,845,1657,725,161,1309,375,37,463,1555,615,1931,
1343,445,937,1083,1617,883,185,1515,225,1443,1225,869,1423,1235,39,1973,
769,259,489,1797,1391,1485,1287,341,289,99,1271,1701,1713,915,537,1781,
1215,963,41,581,303,243,1337,1899,353,1245,329,1563,753,595,1113,1589,
897,1667,407,635,785,1971,135,43,417,1507,1929,731,207,275,1689,1397,
1087,1725,855,1851,1873,397,1607,1813,481,163,567,101,1167,45,1831,1205,
1025,1021,1303,1029,1135,1331,1017,427,545,1181,1033,933,1969,365,1255,1013,
959,317,1751,187,47,1037,455,1429,609,1571,1463,1765,1009,685,679,821,
1153,387,1897,1403,1041,691,1927,811,673,227,137,1499,49,1005,103,629,
831,1091,1449,1477,1967,1677,697,1045,737,1117,1737,667,911,1325,473,437,
1281,1795,1001,261,879,51,775,1195,801,1635,759,165,1871,1645,1049,245,
703,1597,553,955,209,1779,1849,661,865,291,841,997,1265,1965,1625,53,
1409,893,105,1925,1297,589,377,1579,929,1053,1655,1829,305,1811,1895,139,
575,189,343,709,1711,1139,1095,277,993,1699,55,1435,655,1491,1319,331,
1537,515,791,507,623,1229,1529,1963,1057,355,1545,603,1615,1171,743,523,
447,1219,1239,1723,465,499,57,107,1121,989,951,229,1521,851,167,715,
1665,1923,1687,1157,1553,1869,1415,1749,1185,1763,649,1061,561,531,409,907,
319,1469,1961,59,1455,141,1209,491,1249,419,1847,1893,399,211,985,1099,
1793,765,1513,1275,367,1587,263,1365,1313,925,247,1371,1359,109,1561,1291,
191,61,1065,1605,721,781,1735,875,1377,1827,1353,539,1777,429,1959,1483,
1921,643,617,389,1809,947,889,981,1441,483,1143,293,817,749,1383,1675,
63,1347,169,827,1199,1421,583,1259,1505,861,457,1125,143,1069,807,1867,
2047,2045,279,2043,111,307,2041,597,1569,1891,2039,1957,1103,1389,231,2037,
65,1341,727,837,977,2035,569,1643,1633,547,439,1307,2033,1709,345,1845,
1919,637,1175,379,2031,333,903,213,1697,797,1161,475,1073,2029,921,1653,
193,67,1623,1595,943,1395,1721,2027,1761,1955,1335,357,113,1747,1497,1461,
1791,771,2025,1285,145,973,249,171,1825,611,265,1189,847,1427,2023,1269,
321,1475,1577,69,1233,755,1223,1685,1889,733,1865,2021,1807,1107,1447,1077,
1663,1917,1129,1147,1775,1613,1401,555,1953,2019,631,1243,1329,787,871,885,
449,1213,681,1733,687,115,71,1301,2017,675,969,411,369,467,295,693,
1535,509,233,517,401,1843,1543,939,2015,669,1527,421,591,147,281,501,
577,195,215,699,1489,525,1081,917,1951,2013,73,1253,1551,173,857,309,
1407,899,663,1915,1519,1203,391,1323,1887,739,1673,2011,1585,493,1433,117,
705,1603,1111,965,431,1165,1863,533,1823,605,823,1179,625,813,2009,75,
1279,1789,1559,251,657,563,761,1707,1759,1949,777,347,335,1133,1511,267,
833,1085,2007,1467,1745,1805,711,149,1695,803,1719,485,1295,1453,935,459,
1151,381,1641,1413,1263,77,1913,2005,1631,541,119,1317,1841,1773,359,651,
961,323,1193,197,175,1651,441,235,1567,1885,1481,1947,881,2003,217,843,
1023,1027,745,1019,913,717,1031,1621,1503,867,1015,1115,79,1683,793,1035,
1089,1731,297,1861,2001,1011,1593,619,1439,477,585,283,1039,1363,1369,1227,
895,1661,151,645,1007,1357,121,1237,1375,1821,1911,549,1999,1043,1945,1419,
1217,957,599,571,81,371,1351,1003,1311,931,311,1381,1137,723,1575,1611,
767,253,1047,1787,1169,1997,1273,853,1247,413,1289,1883,177,403,999,1803,
1345,451,1495,1093,1839,269,199,1387,1183,1757,1207,1051,783,83,423,1995,
639,1155,1943,123,751,1459,1671,469,1119,995,393,219,1743,237,153,1909,
1473,1859,1705,1339,337,909,953,1771,1055,349,1993,613,1393,557,729,1717,
511,1533,1257,1541,1425,819,519,85,991,1693,503,1445,433,877,1305,1525,
1601,829,809,325,1583,1549,1991,1941,927,1059,1097,1819,527,1197,1881,1333,
383,125,361,891,495,179,633,299,863,285,1399,987,1487,1517,1639,1141,
1729,579,87,1989,593,1907,839,1557,799,1629,201,155,1649,1837,1063,949,
255,1283,535,773,1681,461,1785,683,735,1123,1801,677,689,1939,487,757,
1857,1987,983,443,1327,1267,313,1173,671,221,695,1509,271,1619,89,565,
127,1405,1431,1659,239,1101,1159,1067,607,1565,905,1755,1231,1299,665,373,
1985,701,1879,1221,849,627,1465,789,543,1187,1591,923,1905,979,1241,181};

bool bad255[512] =
{0,0,1,1,0,1,1,1,1,0,1,1,1,1,1,0,0,1,1,0,1,0,1,1,1,0,1,1,1,1,0,1,
 1,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,1,1,0,1,1,1,1,0,1,1,1,
 0,1,0,1,1,0,0,1,1,1,1,1,0,1,1,1,1,0,1,1,0,0,1,1,1,1,1,1,1,1,0,1,
 1,1,1,1,0,1,1,1,1,1,0,1,1,1,1,0,1,1,1,0,1,1,1,1,0,0,1,1,1,1,1,1,
 1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,0,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,
 1,1,1,1,1,1,0,1,1,0,1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,1,
 1,1,1,0,0,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,
 1,0,1,1,1,0,1,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,1,1,1,
 0,0,1,1,0,1,1,1,1,0,1,1,1,1,1,0,0,1,1,0,1,0,1,1,1,0,1,1,1,1,0,1,
 1,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,1,1,0,1,1,1,1,0,1,1,1,
 0,1,0,1,1,0,0,1,1,1,1,1,0,1,1,1,1,0,1,1,0,0,1,1,1,1,1,1,1,1,0,1,
 1,1,1,1,0,1,1,1,1,1,0,1,1,1,1,0,1,1,1,0,1,1,1,1,0,0,1,1,1,1,1,1,
 1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,0,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,
 1,1,1,1,1,1,0,1,1,0,1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,1,
 1,1,1,0,0,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,
 1,0,1,1,1,0,1,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,1,1,1,
 0,0};

inline bool square( int64 x ) {
    // Quickfail
    if( x < 0 || (x&2) || ((x & 7) == 5) || ((x & 11) == 8) )
        return false;
    if( x == 0 )
        return true;

    // Check mod 255 = 3 * 5 * 17, for fun
    int64 y = x;
    y = (y & 4294967295LL) + (y >> 32);
    y = (y & 65535) + (y >> 16);
    y = (y & 255) + ((y >> 8) & 255) + (y >> 16);
    if( bad255[y] )
        return false;

    // Divide out powers of 4 using binary search
    if((x & 4294967295LL) == 0)
        x >>= 32;
    if((x & 65535) == 0)
        x >>= 16;
    if((x & 255) == 0)
        x >>= 8;
    if((x & 15) == 0)
        x >>= 4;
    if((x & 3) == 0)
        x >>= 2;

    if((x & 7) != 1)
        return false;

    // Compute sqrt using something like Hensel's lemma
    int64 r, t, z;
    r = start[(x >> 3) & 1023];
    do {
        z = x - r * r;
        if( z == 0 )
            return true;
        if( z < 0 )
            return false;
        t = z & (-z);
        r += (z & t) >> 1;
        if( r > (t  >> 1) )
            r = t - r;
    } while( t <= (1LL << 33) );

    return false;
}

5
哇!我将尝试将其转换为Java并进行比较,并对结果进行准确性检查。我会让你知道我的发现。
Kip

79
哇真漂亮 我以前见过Hensel提升了(以模数为素计算多项式的根),但我什至没有意识到可以谨慎地降低引理来计算数字的平方根。这令人振奋:)
ShreevatsaR

3
@nightcracker事实并非如此。9 < 0 => false9&2 => 09&7 == 5 => false9&11 == 8 => false
primo 2012年

53
Maartinus在下方发布了一个2倍更快的解决方案(并且更短),稍晚一点,这似乎并没有引起太多的关注。
杰森·C

3
似乎通过滤除明显的平方可以在不同解决方案中获得很多速度优势。有没有人对通过Maartinus解决方案进行过滤然后再使用sqrt函数的情况进行基准测试,因为那是内置函数?
user1914292 2014年

376

我参加聚会很晚了,但是我希望提供一个更好的答案。更短(假设我的基准是正确的)也快得多

long goodMask; // 0xC840C04048404040 computed below
{
    for (int i=0; i<64; ++i) goodMask |= Long.MIN_VALUE >>> (i*i);
}

public boolean isSquare(long x) {
    // This tests if the 6 least significant bits are right.
    // Moving the to be tested bit to the highest position saves us masking.
    if (goodMask << x >= 0) return false;
    final int numberOfTrailingZeros = Long.numberOfTrailingZeros(x);
    // Each square ends with an even number of zeros.
    if ((numberOfTrailingZeros & 1) != 0) return false;
    x >>= numberOfTrailingZeros;
    // Now x is either 0 or odd.
    // In binary each odd square ends with 001.
    // Postpone the sign test until now; handle zero in the branch.
    if ((x&7) != 1 | x <= 0) return x == 0;
    // Do it in the classical way.
    // The correctness is not trivial as the conversion from long to double is lossy!
    final long tst = (long) Math.sqrt(x);
    return tst * tst == x;
}

第一个测试可快速捕获大多数非正方形。它使用打包成长列的64项表,因此没有数组访问成本(间接和边界检查)。对于均匀随机的long,到此结束的概率为81.25%。

第二个测试捕获在因式分解中所有具有奇数二的数字。该方法Long.numberOfTrailingZeros非常快,因为它可以将JIT编辑到单个i86指令中。

删除尾随的零后,第三个测试将处理二进制以011、101或111结尾的数字,这不是完美的平方。它还关心负数并处理0。

最终的测试回到double算术上。由于double尾数只有53位,因此从long到的转换double包括对大值的舍入。但是,测试是正确的(除非证明是错误的)。

尝试纳入mod255的想法没有成功。


3
移位值的隐式掩饰有点...邪恶。您知道为什么它在Java规范中吗?
dfeuer 2014年

5
@dfeuer我猜有两个原因:1.多调一点毫无意义。2.就像硬件在工作,任何使用按位运算的人都对性能感兴趣,因此做其他任何事情都是错误的。-goodMask测试做的,但它确实是之前的右移。因此,您必须重复一遍,但是这样更简单,并且AFAIK快一点并且同样出色。
maaartinus 2014年

2
@dfeuer对于基准测试,尽快给出答案很重要,尾随的零计数本身没有给出答案。这只是一个准备步骤。i86 / amd64可以做到。不知道移动设备中的小型CPU,但是最糟糕的是,Java必须为其生成一条AND指令,这肯定比另一种方法简单。
maaartinus 2014年

2
@Sebastian一个可能更好的测试:if ((x & (7 | Integer.MIN_VALUE)) != 1) return x == 0;
maaartinus

4
“因为double只有56位的尾数”->我会说它更可能具有53位的尾数。
chux-恢复莫妮卡

132

您必须进行一些基准测试。最佳算法将取决于输入的分布。

您的算法可能几乎是最佳算法,但是您可能需要在调用平方根例程之前进行快速检查以排除某些可能性。例如,通过按位进行“与”运算,以十六进制形式查看数字的最后一位。完美的平方只能以16为底的0、1、4或9结尾,因此对于75%的输入(假设它们是均匀分布的),您可以避免调用平方根来换取一些非常快速的比特摆动。

Kip对实现以下十六进制技巧的以下代码进行了基准测试。测试数字1到100,000,000时,此代码的运行速度是原始代码的两倍。

public final static boolean isPerfectSquare(long n)
{
    if (n < 0)
        return false;

    switch((int)(n & 0xF))
    {
    case 0: case 1: case 4: case 9:
        long tst = (long)Math.sqrt(n);
        return tst*tst == n;

    default:
        return false;
    }
}

当我在C ++中测试类似代码时,它实际上比原始代码运行得慢。但是,当我消除了switch语句时,十六进制把戏再次使代码快了两倍。

int isPerfectSquare(int n)
{
    int h = n & 0xF;  // h is the last hex "digit"
    if (h > 9)
        return 0;
    // Use lazy evaluation to jump out of the if statement as soon as possible
    if (h != 2 && h != 3 && h != 5 && h != 6 && h != 7 && h != 8)
    {
        int t = (int) floor( sqrt((double) n) + 0.5 );
        return t*t == n;
    }
    return 0;
}

消除switch语句对C#代码影响很小。


那是非常聪明的...不会想到的
沃伦

关于尾随位的好点。我会尝试将该测试与此处的其他一些评论结合起来。
PeterAllenWebb

3
一流的解决方案。想知道您是怎么想到的?是一个既定的原则还是您发现的东西?:D
Jeel Shah 2011年

3
@LarsH无需添加0.5,请参阅我的解决方案以获取证明的链接。
maaartinus 2014年

2
@JerryGoyal这取决于编译器和大小写的值。在理想的编译器中,切换总是至少与if-else一样快。但是编译器并不完美,因此最好像John一样尝试一下。
fishinear

52

我当时在想我在数值分析课程中度过的可怕时光。

然后我记得,雷神之锤的源代码在网上绕过了这个函数:

float Q_rsqrt( float number )
{
  long i;
  float x2, y;
  const float threehalfs = 1.5F;

  x2 = number * 0.5F;
  y  = number;
  i  = * ( long * ) &y;  // evil floating point bit level hacking
  i  = 0x5f3759df - ( i >> 1 ); // wtf?
  y  = * ( float * ) &i;
  y  = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
  // y  = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed

  #ifndef Q3_VM
  #ifdef __linux__
    assert( !isnan(y) ); // bk010122 - FPE?
  #endif
  #endif
  return y;
}

它基本上使用牛顿的逼近函数来计算平方根(请记住确切的名称)。

它应该是可用的,甚至可能更快,它来自于惊人的id软件的游戏之一!

它是用C ++编写的,但是一旦有了这个主意,在Java中重用相同的技术就不难了:

我最初在以下位置找到它:http : //www.codemaestro.com/reviews/9

牛顿的方法在Wikipedia上进行了解释:http//en.wikipedia.org/wiki/Newton%27s_method

您可以单击该链接以获取有关其工作原理的更多说明,但是如果您不太在意,那么从阅读博客和参加数值分析课程后,我大致会记得:

  • * (long*) &y本质上讲,它是一个快速转换为长整型的函数,因此可以对原始字节应用整数运算。
  • 0x5f3759df - (i >> 1);行是近似函数的预先计算的种子值。
  • * (float*) &i值转换回浮点。
  • y = y * ( threehalfs - ( x2 * y * y ) )行基本上再次遍历该函数的值。

逼近函数对结果的迭代次数越多,逼近函数给出的值越精确。在Quake的情况下,一次迭代“足够好”,但是如果不适合您……那么您可以根据需要添加尽可能多的迭代。

这应该更快,因为它可以减少在朴素的平方中执行的除法运算的数量,将其除以简单的除以2(实际上是* 0.5F乘法运算),并用一些固定数量的乘法运算代替。


9
应当注意,这返回1 / sqrt(number),而不是sqrt(number)。我做了一些测试,而这个失败开始在n = 410881:约翰·卡马克神奇公式返回642.00104,当实际的平方根是641

11
您可以查看Chris Lomonts关于快速反平方根的论文:lomont.org/Math/Papers/2003/InvSqrt.pdf它使用的技术与此处相同,但魔术数不同。本文解释了为什么选择了幻数。

4
另外,beyond3d.com / content / articles / 8beyond3d.com/content/articles/15阐明了此方法的起源。它通常归因于约翰·卡马克(John Carmack),但似乎原始代码(可能)是由加里·塔罗利(Gary Tarolli),格雷格·沃尔什(Greg Walsh)以及其他人编写的。

3
同样,您不能在Java中键入浮点数和整数。
锑2013年

10
@锑谁说?从Java 1.0.2开始,FloatToIntBitsIntToFloatBits已经存在。
corsiKa 2015年

38

我不确定它是否会更快甚至准确,但是您可以使用John Carmack的Magical Square Root算法更快地求解平方根。您可能可以轻松地针对所有可能的32位整数对此进行测试,并验证您是否确实获得了正确的结果,因为它只是一个近似值。但是,现在考虑到这一点,使用双精度也是近似值,因此我不确定该如何发挥作用。


10
我相信这些天Carmack的把戏是毫无意义的。内置的sqrt指令比以前快很多,因此您最好执行常规的平方根并测试结果是否为int更好。与往常一样,对其进行基准测试。
jalf

4
这个场所开始在n = 410881,约翰卡马克魔术公式返回642.00104,当实际平方根是641

11
我最近在Java游戏中使用了Carmack的技巧,它非常有效,加速了40%,因此至少在Java中仍然有用。
finnw 2010年

3
@Robert Fraser是整体帧率+ 40%。该游戏有一个粒子物理系统,几乎占用了所有可用的CPU周期,主要由平方根函数和最近舍入整数函数(我也使用类似的小技巧)
对其

5
链接断开。
皮克斯

36

如果您进行二进制运算以尝试找到“正确的”平方根,则可以很容易地检测出所获得的值是否足够接近以告知:

(n+1)^2 = n^2 + 2n + 1
(n-1)^2 = n^2 - 2n + 1

因此,计算后n^2,选项为:

  • n^2 = target:完成,返回true
  • n^2 + 2n + 1 > target > n^2 :您很亲密,但这并不完美:返回false
  • n^2 - 2n + 1 < target < n^2 :同上
  • target < n^2 - 2n + 1 :较低的二进制印章 n
  • target > n^2 + 2n + 1 :更高的二进制印章 n

(抱歉,这n用作您当前的猜测,并且target作为参数。对于造成的混淆,我们深表歉意!)

我不知道这是否会更快,但是值得一试。

编辑:二进制印章也不必占用整数的整个范围(2^x)^2 = 2^(2x),因此,一旦您在目标中找到了最高置位(可以用旋转技巧完成;我确切地忘记了如何做)您可以快速获得一系列潜在答案。请注意,幼稚的二进制印章最多只需要进行31或32次迭代。


我的钱就是用这种方法。避免调用sqrt(),因为它正在计算全平方根,并且只需要前几位数。
PeterAllenWebb

3
另一方面,如果浮点是在专用FP单元中完成的,则它可能会使用各种有趣的技巧。我不想在没有基准的情况下押注:)(我今晚可以在C#中尝试一下,只是为了看看...)
Jon Skeet

8
如今,硬件sqrt确实相当快。
亚当·罗森菲尔德

24

我对该线程中的几种算法进行了自己的分析,并得出了一些新结果。您可以在此答案的编辑历史记录中看到那些较旧的结果,但是由于我犯了一个错误,它们并不准确,并且浪费了时间来分析几种不太接近的算法。但是,从几个不同的答案中吸取教训,我现在有两种算法可以压碎该线程的“赢家”。这是我与其他人不同的核心工作:

// This is faster because a number is divisible by 2^4 or more only 6% of the time
// and more than that a vanishingly small percentage.
while((x & 0x3) == 0) x >>= 2;
// This is effectively the same as the switch-case statement used in the original
// answer. 
if((x & 0x7) != 1) return false;

但是,这条简单的代码通常会添加一两个非常快速的指令,从而大大简化了代码编写过程。 switch-case语句为一个if语句的过程。但是,如果许多测试数字具有显着的2的幂,它可能会增加运行时间。

以下算法如下:

  • 互联网 -Kip发布的答案
  • 杜伦 -我的修改后的答案以单次通过答案为基础
  • DurronTwo-我的修改后的答案使用了两次通过答案(@JohnnyHeggheim),并进行了其他一些小的修改。

如果数字是使用以下方式生成的,则这是运行时示例 Math.abs(java.util.Random.nextLong())

 0% Scenario{vm=java, trial=0, benchmark=Internet} 39673.40 ns; ?=378.78 ns @ 3 trials
33% Scenario{vm=java, trial=0, benchmark=Durron} 37785.75 ns; ?=478.86 ns @ 10 trials
67% Scenario{vm=java, trial=0, benchmark=DurronTwo} 35978.10 ns; ?=734.10 ns @ 10 trials

benchmark   us linear runtime
 Internet 39.7 ==============================
   Durron 37.8 ============================
DurronTwo 36.0 ===========================

vm: java
trial: 0

如果仅在前一百万个long上运行,则这是一个示例运行时:

 0% Scenario{vm=java, trial=0, benchmark=Internet} 2933380.84 ns; ?=56939.84 ns @ 10 trials
33% Scenario{vm=java, trial=0, benchmark=Durron} 2243266.81 ns; ?=50537.62 ns @ 10 trials
67% Scenario{vm=java, trial=0, benchmark=DurronTwo} 3159227.68 ns; ?=10766.22 ns @ 3 trials

benchmark   ms linear runtime
 Internet 2.93 ===========================
   Durron 2.24 =====================
DurronTwo 3.16 ==============================

vm: java
trial: 0

如您所见,DurronTwo对于大型输入而言,这样做会更好,因为它非常频繁地使用魔术,但是与第一种算法相比,它变得更加笨拙,并且Math.sqrt因为数字小得多。同时,简单者Durron是一个巨大的赢家,因为它不必在前100万个数字中进行四次除法。

这里是Durron

public final static boolean isPerfectSquareDurron(long n) {
    if(n < 0) return false;
    if(n == 0) return true;

    long x = n;
    // This is faster because a number is divisible by 16 only 6% of the time
    // and more than that a vanishingly small percentage.
    while((x & 0x3) == 0) x >>= 2;
    // This is effectively the same as the switch-case statement used in the original
    // answer. 
    if((x & 0x7) == 1) {

        long sqrt;
        if(x < 410881L)
        {
            int i;
            float x2, y;

            x2 = x * 0.5F;
            y  = x;
            i  = Float.floatToRawIntBits(y);
            i  = 0x5f3759df - ( i >> 1 );
            y  = Float.intBitsToFloat(i);
            y  = y * ( 1.5F - ( x2 * y * y ) );

            sqrt = (long)(1.0F/y);
        } else {
            sqrt = (long) Math.sqrt(x);
        }
        return sqrt*sqrt == x;
    }
    return false;
}

DurronTwo

public final static boolean isPerfectSquareDurronTwo(long n) {
    if(n < 0) return false;
    // Needed to prevent infinite loop
    if(n == 0) return true;

    long x = n;
    while((x & 0x3) == 0) x >>= 2;
    if((x & 0x7) == 1) {
        long sqrt;
        if (x < 41529141369L) {
            int i;
            float x2, y;

            x2 = x * 0.5F;
            y = x;
            i = Float.floatToRawIntBits(y);
            //using the magic number from 
            //http://www.lomont.org/Math/Papers/2003/InvSqrt.pdf
            //since it more accurate
            i = 0x5f375a86 - (i >> 1);
            y = Float.intBitsToFloat(i);
            y = y * (1.5F - (x2 * y * y));
            y = y * (1.5F - (x2 * y * y)); //Newton iteration, more accurate
            sqrt = (long) ((1.0F/y) + 0.2);
        } else {
            //Carmack hack gives incorrect answer for n >= 41529141369.
            sqrt = (long) Math.sqrt(x);
        }
        return sqrt*sqrt == x;
    }
    return false;
}

我的基准测试工具:(需要Google caliper 0.1-rc5)

public class SquareRootBenchmark {
    public static class Benchmark1 extends SimpleBenchmark {
        private static final int ARRAY_SIZE = 10000;
        long[] trials = new long[ARRAY_SIZE];

        @Override
        protected void setUp() throws Exception {
            Random r = new Random();
            for (int i = 0; i < ARRAY_SIZE; i++) {
                trials[i] = Math.abs(r.nextLong());
            }
        }


        public int timeInternet(int reps) {
            int trues = 0;
            for(int i = 0; i < reps; i++) {
                for(int j = 0; j < ARRAY_SIZE; j++) {
                    if(SquareRootAlgs.isPerfectSquareInternet(trials[j])) trues++;
                }
            }

            return trues;   
        }

        public int timeDurron(int reps) {
            int trues = 0;
            for(int i = 0; i < reps; i++) {
                for(int j = 0; j < ARRAY_SIZE; j++) {
                    if(SquareRootAlgs.isPerfectSquareDurron(trials[j])) trues++;
                }
            }

            return trues;   
        }

        public int timeDurronTwo(int reps) {
            int trues = 0;
            for(int i = 0; i < reps; i++) {
                for(int j = 0; j < ARRAY_SIZE; j++) {
                    if(SquareRootAlgs.isPerfectSquareDurronTwo(trials[j])) trues++;
                }
            }

            return trues;   
        }
    }

    public static void main(String... args) {
        Runner.main(Benchmark1.class, args);
    }
}

更新:我做了一个新的算法,在某些情况下更快,而在其他情况下更慢,我根据不同的输入获得了不同的基准。如果我们计算模0xFFFFFF = 3 x 3 x 5 x 7 x 13 x 17 x 241,我们可以消除97.82%不能为平方的数字。可以用5个按位运算在一行中完成(某种程度上):

if (!goodLookupSquares[(int) ((n & 0xFFFFFFl) + ((n >> 24) & 0xFFFFFFl) + (n >> 48))]) return false;

所得索引为1)残基,2)残基+ 0xFFFFFF或3)残基+ 0x1FFFFFE。当然,我们需要有一个残差模的查找表0xFFFFFF,该表的大小约为3mb(在这种情况下,它存储为ascii文本的十进制数字,不是最佳值,但显然可以用a来代替,ByteBuffer以此类推。)牛逼不管这么多,你可以在这里找到该文件(或生成它自己):

public final static boolean isPerfectSquareDurronThree(long n) {
    if(n < 0) return false;
    if(n == 0) return true;

    long x = n;
    while((x & 0x3) == 0) x >>= 2;
    if((x & 0x7) == 1) {
        if (!goodLookupSquares[(int) ((n & 0xFFFFFFl) + ((n >> 24) & 0xFFFFFFl) + (n >> 48))]) return false;
        long sqrt;
        if(x < 410881L)
        {
            int i;
            float x2, y;

            x2 = x * 0.5F;
            y  = x;
            i  = Float.floatToRawIntBits(y);
            i  = 0x5f3759df - ( i >> 1 );
            y  = Float.intBitsToFloat(i);
            y  = y * ( 1.5F - ( x2 * y * y ) );

            sqrt = (long)(1.0F/y);
        } else {
            sqrt = (long) Math.sqrt(x);
        }
        return sqrt*sqrt == x;
    }
    return false;
}

我将其加载到这样的boolean数组中:

private static boolean[] goodLookupSquares = null;

public static void initGoodLookupSquares() throws Exception {
    Scanner s = new Scanner(new File("24residues_squares.txt"));

    goodLookupSquares = new boolean[0x1FFFFFE];

    while(s.hasNextLine()) {
        int residue = Integer.valueOf(s.nextLine());
        goodLookupSquares[residue] = true;
        goodLookupSquares[residue + 0xFFFFFF] = true;
        goodLookupSquares[residue + 0x1FFFFFE] = true;
    }

    s.close();
}

示例运行时。Durron在我进行的每次试用中,它都胜过(第一版)。

 0% Scenario{vm=java, trial=0, benchmark=Internet} 40665.77 ns; ?=566.71 ns @ 10 trials
33% Scenario{vm=java, trial=0, benchmark=Durron} 38397.60 ns; ?=784.30 ns @ 10 trials
67% Scenario{vm=java, trial=0, benchmark=DurronThree} 36171.46 ns; ?=693.02 ns @ 10 trials

  benchmark   us linear runtime
   Internet 40.7 ==============================
     Durron 38.4 ============================
DurronThree 36.2 ==========================

vm: java
trial: 0

3
庞大的查询表似乎不是一个好主意。高速缓存未命中比x86硬件sqrt指令(约20个周期)要慢(约100到150个周期)。在吞吐量方面,您可以承受许多未解决的缓存丢失,但是您仍在驱逐其他有用的数据。如果查找表比其他任何选项都快很多,那么巨大的查找表才是值得的,并且此功能是整个程序性能的主要因素。
彼得·科德斯

1
@SwissFrank:完美平方检查是程序唯一要做的事情吗?查找表在一个微基准测试中看起来不错,该基准在一个紧密的循环中反复调用它,但是在实际程序中工作表中有其他数据的情况下,它并不理想。
彼得·科德斯

1
如果将0x1FFFFFE位的位图存储为打包位图,则占用4兆字节。在现代英特尔台式机上命中的L3缓存具有> 40个周期的延迟,而在大型Xeon上则更糟。比硬件sqrt + mul延迟长。如果存储为每个值1个字节的字节映射,则大约为32 MB。比多核Xeon的L3缓存大得多,Xeon的所有核心共享一个巨大的缓存。因此,如果您的输入数据在足够大的输入范围内具有均匀的随机分布,那么即使在紧密的循环中,也会有很多L2缓存未命中。(Intel上的专用每核L2仅为256k,具有约12个周期的延迟。)
Peter Cordes

1
@SwissFrank:哦,如果您要做的只是进行根检查,那么有可能通过位图来获得L3匹配。我正在查看延迟,但是许多未命中可以同时发生,因此吞吐量可能很好。OTOH,SIMD sqrtps吞吐量甚至sqrtpd(双精度)在Skylake上还不错,但并不比旧CPU的延迟好很多。无论如何,7-cpu.com/cpu/Haswell.html有一些不错的实验编号,以及用于其他CPU的页面。Agner Fog的microarch指南pdf提供了一些针对Intel和AMD uarches的缓存延迟时间数:agner.org/optimize
Peter Cordes

1
使用Java中的x86 SIMD是一个问题,并且当您增加int-> fp和fp-> int转换的成本时,似乎有可能使位图更好。您确实需要double避免将某些整数舍入到+ -2 ^ 24范围之外的整数(因此32位整数可以在该整数范围之外),并且精度要sqrtpd慢一些sqrtps,并且每条指令仅处理一半的元素(每个SIMD向量) 。
彼得·科德斯

18

使用牛顿法计算整数平方根应该更快,然后对该数字平方并进行校验,就像在当前解决方案中一样。牛顿法是其他答案中提到的Carmack解决方案的基础。您应该能够得到更快的答案,因为您只对根的整数部分感兴趣,从而可以更快地停止近似算法。

您可以尝试的另一种优化方法:如果数字的数字根以1、4、7 或9结尾,则该数字不是完美的平方。这可以用作在应用较慢的平方根算法之前消除60%输入的一种快速方法。


1
数字根是严格计算相当于模,所以应连同其他模方法这里,如模16和模255被认为是
基督教Oudard

1
您确定数字根等于模吗?正如链接所解释的,这似乎完全不同。注意列表是1,4,7,9而不是1,4,5,9。
Fractaly 2011年

1
十进制系统中的数字根等同于使用模9(以及dr(n)= 1 +((n-1)mod 9);因此也略有偏移)。数字0、1、4、5、9用于模16,数字0、1、4、7用于模9-对应于数字根的1、4、7、9。
汉斯·奥尔森

16

我希望此函数与所有正64位有符号整数一起使用

Math.sqrt()使用double作为输入参数,因此对于大于2 ^ 53的整数,您将无法获得准确的结果。


5
实际上,我已经测试了所有大于2 ^ 53的理想平方的答案,以及从每个理想平方以下的5到每个理想平方以上的5的所有数字,我得到了正确的结果。(当我将sqrt答案四舍五入为一个长整数,然后对该值求平方并进行比较时,舍入误差已得到纠正)
Kip

2
@Kip:我想我已经证明可行。
maaartinus 2013年

结果并非完全准确,但比您想象的要准确。如果我们假设在转换为double并在平方根之后至少有15个准确数字,那么这就足够了,因为我们需要的位数不超过11​​:10位(32位平方根)和少于1(小数位),因为+0.5四舍五入到最接近的值。
mwfearnley 2014年

3
Math.sqrt()并不完全准确,但不一定如此。在第一篇文章中,tst是一个接近sqrt(N)的整数。如果N不是平方,则无论tst的值是多少,tst * tst!=N。如果N是一个完美的平方,则sqrt(N)<2 ^ 32,并且只要计算sqrt(N)的误差<0.5,就可以了。
gnasher729 2014年

13

仅作记录,另一种方法是使用素数分解。如果分解的每个因子都是偶数,则该数字是一个完美的平方。因此,您要查看的是一个数字是否可以分解为素数平方的乘积。当然,您无需获取这种分解,只需查看它是否存在。

首先建立一个小于2 ^ 32的质数平方的表。它远远小于所有不超过此限制的整数表格。

一个解决方案将是这样的:

boolean isPerfectSquare(long number)
{
    if (number < 0) return false;
    if (number < 2) return true;

    for (int i = 0; ; i++)
    {
        long square = squareTable[i];
        if (square > number) return false;
        while (number % square == 0)
        {
            number /= square;
        }
        if (number == 1) return true;
    }
}

我想这有点神秘。它所做的是在每一步中检查素数平方是否除以输入数。如果这样做,则只要有可能,就将数字除以平方,以从素数分解中删除该平方。如果通过此过程,我们得出1,则输入数字是质数平方的分解。如果平方变得大于数字本身,则该平方或任何更大的平方都无法对其进行划分,因此该数字不能是质数平方的分解。

鉴于当今的sqrt是在硬件中完成的,并且需要在此处计算素数,所以我认为此解决方案的速度要慢得多。正如mrzl在他的回答中说的那样,但是它比使用sqrt的解决方案提供更好的结果,而sqrt不能在2 ^ 54上工作。


1
在当前硬件上,整数除法比FP sqrt慢。这个想法没有机会。>。<即使在2008年,Core2的sqrtsd吞吐量也只有6-58c。它idiv是每一个12-36cycles。(延迟与吞吐量类似:两个单元都没有管道传输)。
彼得·科德斯

sqrt不需要非常准确。这就是为什么要通过对结果进行整数平方并进行整数比较来确定输入整数是否具有确切的整数sqrt的原因。
彼得·科德斯 Peter Cordes)

11

有人指出,最后 d,一个完美平方一位只能取某些值。一个d数字的最后一位数字(基数b)与除以n时的其余数字相同,即。用C表示。nbdn % pow(b, d)

这可以推广到任何模数m,即。n % m可以用来排除某些百分比的数字成为完美的平方。您当前使用的模数是64,允许12,即。余数的19%,尽可能平方。通过少量编码,我发现了模数110880,它仅允许使用2016年。余数的1.8%为正方形。因此,根据模运算(即除法)的成本以及表查找与计算机上的平方根的关系,使用此模可能更快。

顺便说一句,如果Java可以为查询表存储一个打包的位数组,请不要使用它。如今,110880 32位字已不是太多的RAM,因此,获取机器字比获取单个位要快。


真好 您是通过代数运算还是通过反复试验得出的?我可以看到它为何如此有效-完美正方形之间发生了很多碰撞,例如333 ^ 2%110880 == 3 ^ 2、334 ^ 2%110880 == 26 ^ 2、338 ^ 2%110880 == 58 ^ 2。 。
finnw

IIRC是蛮力的,但请注意110880 = 2 ^ 5 * 3 ^ 2 * 5 * 7 * 11,它得出6 * 3 * 2 * 2 * 2-1 = 143个合适的除数。
休·艾伦

我发现由于查找的限制,44352的工作效率更高,通过率为2.6%。至少在我的实现中。
Fractaly 2011年

1
在当前x86硬件上,整数除法(idiv)等于FP sqrt(sqrtsd)或更差。另外,完全不同意避免位域。使用位域时,缓存命中率会好得多,测试位域中的位仅比测试整个字节多一个或两个简单的指令。(对于即使适合作为非位域也适合高速缓存的微型表,最好使用字节数组,而不是32bit整数。x86具有单字节访问,其速度与32bit dword相等。)
Peter Cordes

11

整数问题值得一个整数解决方案。从而

对(非负)整数进行二进制搜索以找到最大整数t使得t**2 <= n。然后测试是否r**2 = n准确。这需要时间O(log n)。

如果您不知道如何对正整数进行二进制搜索(因为集合是无界的),这很容易。首先,f(t) = t**2 - n以2的幂计算增加的​​函数f(在之上)。当您看到它变成正数时,您已经找到一个上限。然后,您可以进行标准的二进制搜索。


实际上,该时间至少O((log n)^2)是因为乘法不是恒定时间,但实际上具有的下界O(log n),当使用大的多精度数时,这变得很明显。但是此Wiki的范围似乎是64位,所以也许是nbd。

10

maaartinus解决方案的以下简化似乎使运行时减少了几个百分点,但是我不足以进行基准测试以产生可以信赖的基准测试:

long goodMask; // 0xC840C04048404040 computed below
{
    for (int i=0; i<64; ++i) goodMask |= Long.MIN_VALUE >>> (i*i);
}

public boolean isSquare(long x) {
    // This tests if the 6 least significant bits are right.
    // Moving the to be tested bit to the highest position saves us masking.
    if (goodMask << x >= 0) return false;
    // Remove an even number of trailing zeros, leaving at most one.
    x >>= (Long.numberOfTrailingZeros(x) & (-2);
    // Repeat the test on the 6 least significant remaining bits.
    if (goodMask << x >= 0 | x <= 0) return x == 0;
    // Do it in the classical way.
    // The correctness is not trivial as the conversion from long to double is lossy!
    final long tst = (long) Math.sqrt(x);
    return tst * tst == x;
}

值得一试的是如何省略第一次测试,

if (goodMask << x >= 0) return false;

会影响性能。


2
结果在这里。删除第一个测试是不好的,因为它可以很便宜地解决大多数情况。来源在我的答案中(已更新)。
maaartinus 2014年

9

为了提高性能,您常常不得不做出一些妥协。其他人表示了各种方法,但是,您注意到Carmack的破解速度更快,直到达到某些N值。然后,您应检查“ n”,如果小于n,则使用Carmack的破解,否则请使用其他描述的方法在这里的答案。


我也将您的建议纳入了解决方案。另外,很好的句柄。:)
Kip

8

通过结合使用该线程中其他人员提出的技术,这是我能想到的最快的Java实现。

  • Mod-256测试
  • 不精确的mod-3465测试(避免整数除法,但会带来一些误报)
  • 浮点平方根,取整并与输入值进行比较

我还尝试了这些修改,但是它们对性能没有帮助:

  • 附加的mod-255测试
  • 将输入值除以4的幂
  • 快速逆平方根(要处理高N值,它需要3次迭代,足以使其比硬件平方根函数慢。)

public class SquareTester {

    public static boolean isPerfectSquare(long n) {
        if (n < 0) {
            return false;
        } else {
            switch ((byte) n) {
            case -128: case -127: case -124: case -119: case -112:
            case -111: case -103: case  -95: case  -92: case  -87:
            case  -79: case  -71: case  -64: case  -63: case  -60:
            case  -55: case  -47: case  -39: case  -31: case  -28:
            case  -23: case  -15: case   -7: case    0: case    1:
            case    4: case    9: case   16: case   17: case   25:
            case   33: case   36: case   41: case   49: case   57:
            case   64: case   65: case   68: case   73: case   81:
            case   89: case   97: case  100: case  105: case  113:
            case  121:
                long i = (n * INV3465) >>> 52;
                if (! good3465[(int) i]) {
                    return false;
                } else {
                    long r = round(Math.sqrt(n));
                    return r*r == n; 
                }
            default:
                return false;
            }
        }
    }

    private static int round(double x) {
        return (int) Double.doubleToRawLongBits(x + (double) (1L << 52));
    }

    /** 3465<sup>-1</sup> modulo 2<sup>64</sup> */
    private static final long INV3465 = 0x8ffed161732e78b9L;

    private static final boolean[] good3465 =
        new boolean[0x1000];

    static {
        for (int r = 0; r < 3465; ++ r) {
            int i = (int) ((r * r * INV3465) >>> 52);
            good3465[i] = good3465[i+1] = true;
        }
    }

}

7

您应该从一开始就摆脱N的2幂部分。

2nd Edit 下面m的神奇表达应为

m = N - (N & (N-1));

而不是书面的

第二次编辑结束

m = N & (N-1); // the lawest bit of N
N /= m;
byte = N & 0x0F;
if ((m % 2) || (byte !=1 && byte !=9))
  return false;

第一次编辑:

小改进:

m = N & (N-1); // the lawest bit of N
N /= m;
if ((m % 2) || (N & 0x07 != 1))
  return false;

第一次编辑结束

现在照常继续。这样,到浮点部分时,您已经摆脱了2幂部分为奇数(大约一半)的所有数字,然后只考虑剩下的1/8。即您将浮点数放在数字的6%上。


7

标签中提到了Euler项目,其中许多问题都需要检查数字>> 2^64。当使用80字节缓冲区时,上面提到的大多数优化都不容易。

我使用了Java BigInteger和牛顿方法的稍微修改后的版本,该方法对整数的使用效果更好。问题是确切的平方n^2收敛于(n-1)而不是n因为n^2-1 = (n-1)(n+1)并且最终误差仅比最终除数低一步,并且算法终止。通过在计算错误之前将一个参数添加到原始参数很容易解决。(为多维数据集根添加两个,等等。)

该算法的一个很好的属性是,您可以立即判断该数字是否是一个完美的平方-牛顿方法的最终误差(而不是校正值)将为零。一个简单的修改也使您可以快速计算floor(sqrt(x))而不是最接近的整数。这在遇到几个Euler问题时很方便。


1
对于这些算法,我一直在想同样的事情,因为它们不能很好地转换为多精度缓冲区。因此以为我会在这里坚持下去...实际上,我发现了一个概率平方检验,对于大量数,它具有更好的渐近复杂性.....在数论应用中并不罕见。虽然不熟悉欧拉计画...看起来很有趣。

6

在Ruby中,这是旧的Marchant计算器算法从十进制到二进制的重做(很抱歉,我没有参考),专门针对此问题进行了调整:

def isexactsqrt(v)
    value = v.abs
    residue = value
    root = 0
    onebit = 1
    onebit <<= 8 while (onebit < residue)
    onebit >>= 2 while (onebit > residue)
    while (onebit > 0)
        x = root + onebit
        if (residue >= x) then
            residue -= x
            root = x + onebit
        end
        root >>= 1
        onebit >>= 2
    end
    return (residue == 0)
end

这是类似内容的改进(请不要因为编码风格/气味或笨拙的O / O而对我投反对票-这是至关重要的算法,而C ++不是我的母语)。在这种情况下,我们正在寻找残基== 0:

#include <iostream>  

using namespace std;  
typedef unsigned long long int llint;

class ISqrt {           // Integer Square Root
    llint value;        // Integer whose square root is required
    llint root;         // Result: floor(sqrt(value))
    llint residue;      // Result: value-root*root
    llint onebit, x;    // Working bit, working value

public:

    ISqrt(llint v = 2) {    // Constructor
        Root(v);            // Take the root 
    };

    llint Root(llint r) {   // Resets and calculates new square root
        value = r;          // Store input
        residue = value;    // Initialise for subtracting down
        root = 0;           // Clear root accumulator

        onebit = 1;                 // Calculate start value of counter
        onebit <<= (8*sizeof(llint)-2);         // Set up counter bit as greatest odd power of 2 
        while (onebit > residue) {onebit >>= 2; };  // Shift down until just < value

        while (onebit > 0) {
            x = root ^ onebit;          // Will check root+1bit (root bit corresponding to onebit is always zero)
            if (residue >= x) {         // Room to subtract?
                residue -= x;           // Yes - deduct from residue
                root = x + onebit;      // and step root
            };
            root >>= 1;
            onebit >>= 2;
        };
        return root;                    
    };
    llint Residue() {           // Returns residue from last calculation
        return residue;                 
    };
};

int main() {
    llint big, i, q, r, v, delta;
    big = 0; big = (big-1);         // Kludge for "big number"
    ISqrt b;                            // Make q sqrt generator
    for ( i = big; i > 0 ; i /= 7 ) {   // for several numbers
        q = b.Root(i);                  // Get the square root
        r = b.Residue();                // Get the residue
        v = q*q+r;                      // Recalc original value
        delta = v-i;                    // And diff, hopefully 0
        cout << i << ": " << q << " ++ " << r << " V: " << v << " Delta: " << delta << "\n";
    };
    return 0;
};

迭代次数看起来​​是O(ln n),其中n是v的位长,所以我怀疑这对于较大的v是否会节省很多。浮点数sqrt很慢,可能是100-200个周期,但整数数学不是免费。一打迭代,每个迭代有15个循环,这将是一次洗礼。不过,+ 1还是很有趣。
Tadmas

实际上,我相信加法和减法可以通过XOR来完成。
Brent.Longborough,

那是一个愚蠢的评论-只有加法运算可以由XOR完成;减法是算术运算。
Brent.Longborough,

1
无论如何,XOR的运行时间和加法之间确实有实质性的区别吗?
Tadmas

1
@Tadmas:可能不足以打破“稍后优化”规则。(:-)
Brent.Longborough,

6

如前所述,sqrt调用并不十分准确,但有趣的是,它不会在速度方面使其他答案消失。毕竟,sqrt的汇编语言指令序列很小。英特尔拥有硬件指令,我相信Java不会使用该指令,因为它不符合IEEE。

那为什么慢呢?因为Java实际上是通过JNI调用C例程,所以这样做实际上比调用Java子例程要慢,而Java子例程本身比内联调用要慢。这非常烦人,Java应该提供更好的解决方案,即在必要时构建浮点库调用。那好吧。

在C ++中,我怀疑所有复杂的选择都将失去速度,但是我还没有检查全部。我所做的以及Java员工会发现有用的东西是一个简单的技巧,是A. Rex建议的特殊情况测试的扩展。使用单个long值作为位数组,不检查边界。这样,您就可以进行64位布尔查询。

typedef unsigned long long UVLONG
UVLONG pp1,pp2;

void init2() {
  for (int i = 0; i < 64; i++) {
    for (int j = 0; j < 64; j++)
      if (isPerfectSquare(i * 64 + j)) {
    pp1 |= (1 << j);
    pp2 |= (1 << i);
    break;
      }
   }
   cout << "pp1=" << pp1 << "," << pp2 << "\n";  
}


inline bool isPerfectSquare5(UVLONG x) {
  return pp1 & (1 << (x & 0x3F)) ? isPerfectSquare(x) : false;
}

例程isPerfectSquare5在我的core2 duo机器上运行时间大约为1/3。我怀疑沿着相同的路线进行进一步的调整平均可以减少时间,但是每次检查时,您都在权衡更多的测试以消除更多的故障,因此您在这条路上不能走得更远。

当然,您可以单独检查高6位,而不用单独测试是否为负。

请注意,我正在做的是消除可能的平方,但是当我有一个可能的情况时,我必须调用原始的内联isPerfectSquare。

一次调用init2例程以初始化pp1和pp2的静态值。请注意,在我的C ++实现中,我使用了long long long,因此,由于您已签名,因此必须使用>>>运算符。

根本没有必要对数组进行边界检查,但是Java的优化程序必须很快地弄清这些东西,因此我不为此而怪罪。


3
我打赌你错了两次。1. Intel sqrt符合IEEE。唯一不符合要求的指令是用于lange参数的测角指令。2. Java将内部函数用于Math.sqrt,而不是JNI
maaartinus 2013年

1
您不是忘了使用pp2吗?我知道这pp1用于测试六个最低有效位,但是我认为测试接下来的六个位没有任何意义。
maaartinus

6

我喜欢在某些输入上使用几乎正确的方法的想法。这是“偏移量”较高的版本。该代码似乎可以正常工作,并通过了我的简单测试用例。

只需更换:

if(n < 410881L){...}

与此代码:

if (n < 11043908100L) {
    //John Carmack hack, converted to Java.
    // See: http://www.codemaestro.com/reviews/9
    int i;
    float x2, y;

    x2 = n * 0.5F;
    y = n;
    i = Float.floatToRawIntBits(y);
    //using the magic number from 
    //http://www.lomont.org/Math/Papers/2003/InvSqrt.pdf
    //since it more accurate
    i = 0x5f375a86 - (i >> 1);
    y = Float.intBitsToFloat(i);
    y = y * (1.5F - (x2 * y * y));
    y = y * (1.5F - (x2 * y * y)); //Newton iteration, more accurate

    sqrt = Math.round(1.0F / y);
} else {
    //Carmack hack gives incorrect answer for n >= 11043908100.
    sqrt = (long) Math.sqrt(n);
}

6

考虑到一般的位长(尽管我在这里使用了特定的类型),我尝试如下设计简单的算法。最初需要简单明显地检查0,1,2或<0。在不尝试使用任何现有数学函数的意义上,遵循以下意义很简单。大多数运算符可以替换为按位运算符。我还没有测试任何基准数据。我既不是数学方面的专家,也不是计算机算法设计方面的专家,我很乐意看到您指出问题。我知道那里有很多改进的机会。

int main()
{
    unsigned int c1=0 ,c2 = 0;  
    unsigned int x = 0;  
    unsigned int p = 0;  
    int k1 = 0;  
    scanf("%d",&p);  
    if(p % 2 == 0) {  
        x = p/2; 
    }  
    else {  
        x = (p/2) +1;  
    }  
    while(x) 
    {
        if((x*x) > p) {  
            c1 = x;  
            x = x/2; 
        }else {  
            c2 = x;  
            break;  
        }  
    }  
    if((p%2) != 0)  
        c2++;

    while(c2 < c1) 
    {  
        if((c2 * c2 ) == p) {  
            k1 = 1;  
            break;  
        }  
        c2++; 
    }  
    if(k1)  
        printf("\n Perfect square for %d", c2);  
    else  
        printf("\n Not perfect but nearest to :%d :", c2);  
    return 0;  
}  

@Kip:我的浏览器出现问题。
nabam serbang 2010年

1
您需要缩进。
Steve Kuo

5

当观察到正方形的最后n位时,我检查了所有可能的结果。通过连续检查更多位,可以消除多达5/6的输入。我实际上是设计用来实现Fermat的因式分解算法的,在那儿它非常快。

public static boolean isSquare(final long val) {
   if ((val & 2) == 2 || (val & 7) == 5) {
     return false;
   }
   if ((val & 11) == 8 || (val & 31) == 20) {
     return false;
   }

   if ((val & 47) == 32 || (val & 127) == 80) {
     return false;
   }

   if ((val & 191) == 128 || (val & 511) == 320) {
     return false;
   }

   // if((val & a == b) || (val & c == d){
   //   return false;
   // }

   if (!modSq[(int) (val % modSq.length)]) {
        return false;
   }

   final long root = (long) Math.sqrt(val);
   return root * root == val;
}

伪代码的最后一位可用于扩展测试以消除更多的值。上面的测试是针对k = 0、1、2、3

  • a的形式为(3 << 2k)-1
  • b的形式为(2 << 2k)
  • c的形式为(2 << 2k + 2)-1
  • d的形式为(2 << 2k-1)* 10

    它首先测试它是否具有平方幂为2的平方残差,然后基于最终模数进行测试,然后使用Math.sqrt进行最终测试。我从高层职位上提出了这个想法,并试图将其扩展。我感谢任何意见或建议。

    更新:使用模数(modSq)和模数基数为44352进行的测试,我的测试在OP更新中一次运行的96%的时间内运行,最大数量为1,000,000,000。


  • 2

    这是一个分而治之的解决方案。

    如果自然数(number)的平方根是自然数(solution),则可以solution根据以下位数轻松确定范围number

    • number有1位数字:solution范围= 1-4
    • number有2位数字:solution范围= 3-10
    • number3位数字:solution范围= 10-40
    • number有4位数字:solution范围= 30-100
    • number有5位数字:solution范围= 100-400

    注意到重复吗?

    您可以在二进制搜索方法中使用此范围,以查看是否存在solution以下情况:

    number == solution * solution

    这是代码

    这是我的课程SquareRootChecker

    public class SquareRootChecker {
    
        private long number;
        private long initialLow;
        private long initialHigh;
    
        public SquareRootChecker(long number) {
            this.number = number;
    
            initialLow = 1;
            initialHigh = 4;
            if (Long.toString(number).length() % 2 == 0) {
                initialLow = 3;
                initialHigh = 10;
            }
            for (long i = 0; i < Long.toString(number).length() / 2; i++) {
                initialLow *= 10;
                initialHigh *= 10;
            }
            if (Long.toString(number).length() % 2 == 0) {
                initialLow /= 10;
                initialHigh /=10;
            }
        }
    
        public boolean checkSquareRoot() {
            return findSquareRoot(initialLow, initialHigh, number);
        }
    
        private boolean findSquareRoot(long low, long high, long number) {
            long check = low + (high - low) / 2;
            if (high >= low) {
                if (number == check * check) {
                    return true;
                }
                else if (number < check * check) {
                    high = check - 1;
                    return findSquareRoot(low, high, number);
                }
                else  {
                    low = check + 1;
                    return findSquareRoot(low, high, number);
                }
            }
            return false;
        }
    
    }

    这是有关如何使用它的示例。

    long number =  1234567;
    long square = number * number;
    SquareRootChecker squareRootChecker = new SquareRootChecker(square);
    System.out.println(square + ": " + squareRootChecker.checkSquareRoot()); //Prints "1524155677489: true"
    
    long notSquare = square + 1;
    squareRootChecker = new SquareRootChecker(notSquare);
    System.out.println(notSquare + ": " + squareRootChecker.checkSquareRoot()); //Prints "1524155677490: false"

    2
    我喜欢这个概念,但我想礼貌地指出一个主要缺陷:数字以2为底的二进制数。toString与按位运算符相比,通过将基数2转换为基数10 是非常昂贵的操作。因此,为了满足问题的目标(性能),您必须使用按位运算符而不是基数为10的字符串。同样,我真的很喜欢你的概念。尽管如此,在为该问题发布的所有可能解决方案中,您的实现(目前为止)是最慢的。
    杰克·吉芬

    1

    如果需要考虑速度,为什么不将最常用的一组输入及其值划分到一个查找表中,然后针对特殊情况采取任何优化的魔术算法呢?


    问题是没有“常用输入集”-通常我要遍历一个列表,因此我不会两次使用相同的输入。
    Kip

    1

    应该有可能打包“如果最后X个数字为N,则不能是一个完美的正方形”,比这更有效!我将使用Java 32位整数,并产生足够的数据来检查数字的最后16位-即2048个十六进制整数值。

    ...

    好。我可能遇到了一些数字理论,而这超出了我本人的理解范围,或者我的代码中存在错误。无论如何,这是代码:

    public static void main(String[] args) {
        final int BITS = 16;
    
        BitSet foo = new BitSet();
    
        for(int i = 0; i< (1<<BITS); i++) {
            int sq = (i*i);
            sq = sq & ((1<<BITS)-1);
            foo.set(sq);
        }
    
        System.out.println("int[] mayBeASquare = {");
    
        for(int i = 0; i< 1<<(BITS-5); i++) {
            int kk = 0;
            for(int j = 0; j<32; j++) {
                if(foo.get((i << 5) | j)) {
                    kk |= 1<<j;
                }
            }
            System.out.print("0x" + Integer.toHexString(kk) + ", ");
            if(i%8 == 7) System.out.println();
        }
        System.out.println("};");
    }

    结果如下:

    (ed:在prettify.js中由于性能不佳而被删除;请查看修订历史记录以查看。)


    1

    牛顿法与整数算法

    如果希望避免非整数操作,则可以使用以下方法。它基本上使用针对整数算术修改的牛顿法。

    /**
     * Test if the given number is a perfect square.
     * @param n Must be greater than 0 and less
     *    than Long.MAX_VALUE.
     * @return <code>true</code> if n is a perfect
     *    square, or <code>false</code> otherwise.
     */
    public static boolean isSquare(long n)
    {
        long x1 = n;
        long x2 = 1L;
    
        while (x1 > x2)
        {
            x1 = (x1 + x2) / 2L;
            x2 = n / x1;
        }
    
        return x1 == x2 && n % x1 == 0L;
    }

    此实现无法与使用的解决方案竞争Math.sqrt。但是,可以通过使用其他一些文章中介绍的过滤机制来提高其性能。


    1

    只要起始值合理,用牛顿法计算平方根的速度就快得惊人。但是,没有一个合理的起始值,实际上,我们以二等分和log(2 ^ 64)行为结束。
    要真正快,我们需要一种快速的方法来获得一个合理的起始值,这意味着我们需要使用机器语言。如果处理器在奔腾中提供类似POPCNT的指令,该指令会计算前导零,我们可以使用它来获得一个起始值,该起始值的有效位为一半。小心地找到一个固定数量的牛顿步骤,将始终满足要求。(因此无需循环并具有非常快的执行速度。)

    第二种解决方案是通过浮点工具进行,该工具可能具有快速的sqrt计算(如i87协处理器)。即使通过exp()和log()进行偏移,也可能比牛顿退化为二进制搜索要快。这是一个棘手的方面,需要对处理器进行分析,然后再进行精炼。

    第三种解决方案解决了一个稍有不同的问题,但是值得一提,因为情况已在问题中进行了描述。如果要为数量略有不同的数字计算很多平方根,可以使用牛顿迭代法,如果您从不重新初始化起始值,而只需将其保留在上一次计算结束的位置。在至少一个欧拉问题中,我已经成功地使用了它。


    得到一个好的估计并不难。您可以使用数字的位数来估计解决方案的上下限。另请参阅我的答案,其中我提出了分而治之的解决方案。
    MWB

    POPCNT和计算位数之间有什么区别?除了可以在一纳秒内完成POPCNT。
    阿尔伯特·范德霍斯特

    1

    假设数字是一个完美的平方,则为一个数字的平方根。

    复杂度为log(n)

    /**
     * Calculate square root if the given number is a perfect square.
     * 
     * Approach: Sum of n odd numbers is equals to the square root of n*n, given 
     * that n is a perfect square.
     *
     * @param number
     * @return squareRoot
     */
    
    public static int calculateSquareRoot(int number) {
    
        int sum=1;
        int count =1;
        int squareRoot=1;
        while(sum<number) {
            count+=2;
            sum+=count;
            squareRoot++;
        }
        return squareRoot;
    }

    0

    如果您想提高速度,考虑到您的整数是有限大小的,我怀疑最快的方法将涉及(a)按大小划分参数(例如,按最大位集划分类别),然后根据完美正方形数组检查值在该范围内。


    2
    在很长的范围内有2 ^ 32个完美正方形。这个桌子会很大。同样,通过内存访问来计算值的优势可能是巨大的。
    PeterAllenWebb

    哦,不,没有,有2 ^ 16。2 ^ 32是2 ^ 16平方。有2 ^ 16。
    天体M鼬鼠

    3
    是的,但是long的范围是64位,而不是32位。sqrt(2 ^ 64)= 2 ^ 32。(我在忽略符号位以使数学更容易...实际上有(long)(2 ^ 31.5)= 3037000499完美平方)
    Kip

    0

    关于Carmac方法,似乎只需重复一次就很容易了,这应该使精度的位数翻倍。毕竟,这是一种极其截断的迭代方法-牛顿法,具有很好的首次猜测。

    关于您目前的最佳状态,我看到了两个微观优化:

    • 使用mod255在检查后将检查相对于0进行移动
    • 重新排列四的除权数以跳过所有常规检查(75%)的检查。

    即:

    // Divide out powers of 4 using binary search
    
    if((n & 0x3L) == 0) {
      n >>=2;
    
      if((n & 0xffffffffL) == 0)
        n >>= 32;
      if((n & 0xffffL) == 0)
          n >>= 16;
      if((n & 0xffL) == 0)
          n >>= 8;
      if((n & 0xfL) == 0)
          n >>= 4;
      if((n & 0x3L) == 0)
          n >>= 2;
    }

    甚至更好可能是一个简单的

    while ((n & 0x03L) == 0) n >>= 2;

    显然,知道每个检查点剔除多少个数字将很有趣-我相当怀疑这些检查是否真正独立,这使事情变得棘手。

    By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
    Licensed under cc by-sa 3.0 with attribution required.