La forma más rápida de determinar si la raíz cuadrada de un entero es un entero


1454

Estoy buscando la forma más rápida de determinar si un longvalor es un cuadrado perfecto (es decir, su raíz cuadrada es otro número entero):

  1. Lo hice de la manera más fácil, usando la Math.sqrt() función incorporada, pero me pregunto si hay una manera de hacerlo más rápido restringiéndote a un dominio solo de enteros.
  2. Mantener una tabla de búsqueda no es práctico (ya que hay alrededor de 2 31.5 enteros cuyo cuadrado es menor que 2 63 ).

Aquí está la forma muy simple y directa en que lo estoy haciendo ahora:

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

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

Nota: Estoy usando esta función en muchos problemas del Proyecto Euler . Entonces nadie más tendrá que mantener este código. Y este tipo de micro-optimización en realidad podría marcar la diferencia, ya que parte del desafío es hacer todos los algoritmos en menos de un minuto, y esta función deberá llamarse millones de veces en algunos problemas.


He probado las diferentes soluciones al problema:

  • Después de pruebas exhaustivas, descubrí que 0.5no es necesario agregar al resultado de Math.sqrt (), al menos no en mi máquina.
  • La raíz cuadrada inversa rápida fue más rápida, pero dio resultados incorrectos para n> = 410881. Sin embargo, como lo sugiere BobbyShaftoe , podemos usar el hack de FISR para n <410881.
  • El método de Newton fue un poco más lento que Math.sqrt(). Esto probablemente se deba a que Math.sqrt()usa algo similar al Método de Newton, pero implementado en el hardware, por lo que es mucho más rápido que en Java. Además, el método de Newton todavía requería el uso de dobles.
  • Un método modificado de Newton, que usaba algunos trucos para que solo se involucrara la matemática entera, requería algunos hacks para evitar el desbordamiento (quiero que esta función funcione con todos los enteros con signo positivo de 64 bits), y aún así fue más lento que Math.sqrt().
  • El corte binario fue aún más lento. Esto tiene sentido porque el corte binario requerirá en promedio 16 pases para encontrar la raíz cuadrada de un número de 64 bits.
  • Según las pruebas de John, el uso de orsentencias es más rápido en C ++ que el uso de a switch, pero en Java y C # parece que no hay diferencia entre ory switch.
  • También intenté hacer una tabla de búsqueda (como una matriz estática privada de 64 valores booleanos). Entonces, en lugar de cambiar o ordeclarar, solo diría if(lookup[(int)(n&0x3F)]) { test } else return false;. Para mi sorpresa, esto fue (solo un poco) más lento. Esto se debe a que los límites de la matriz se verifican en Java .

21
Este es el código Java, donde int == 32 bits y largo == 64 bits, y ambos están firmados.
Kip

14
@Shreevasta: He realizado algunas pruebas en valores grandes (mayores que 2 ^ 53), y su método da algunos falsos positivos. El primero encontrado es para n = 9007199326062755, que no es un cuadrado perfecto pero se devuelve como uno.
Kip

37
Por favor, no lo llames el "truco de John Carmack". No se le ocurrió.
user9282

84
@mamama - Quizás, pero se le atribuye a él. Henry Ford no inventó el automóvil, el Wright Bros. no inventó el avión, y Galleleo no fue el primero en descubrir que la Tierra giraba alrededor del sol ... el mundo está hecho de inventos robados (y amor).
Robert Fraser

44
Puede obtener un pequeño aumento de velocidad en el 'error rápido' al usar algo como ((1<<(n&15))|65004) != 0, en lugar de tener tres controles separados.
Nabb

Respuestas:


736

Descubrí un método que funciona ~ 35% más rápido que sus 6 bits + Carmack + código sqrt, al menos con mi CPU (x86) y lenguaje de programación (C / C ++). Sus resultados pueden variar, especialmente porque no sé cómo se desarrollará el factor Java.

Mi enfoque es triple:

  1. Primero, filtra las respuestas obvias. Esto incluye números negativos y mirar los últimos 4 bits. (Descubrí que mirar los últimos seis no ayudó.) También respondo que sí para 0. (Al leer el código a continuación, tenga en cuenta que mi entrada es int64 x).
    if( x < 0 || (x&2) || ((x & 7) == 5) || ((x & 11) == 8) )
        return false;
    if( x == 0 )
        return true;
  2. A continuación, verifique si es un módulo cuadrado 255 = 3 * 5 * 17. Debido a que es un producto de tres primos distintos, solo aproximadamente 1/8 de los residuos mod 255 son cuadrados. Sin embargo, en mi experiencia, llamar al operador de módulo (%) cuesta más que el beneficio que se obtiene, por lo que utilizo trucos de bits que involucran 255 = 2 ^ 8-1 para calcular el residuo. (Para bien o para mal, no estoy usando el truco de leer bytes individuales de una palabra, solo bit a bit y turnos).
    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.
    
    Para verificar si el residuo es un cuadrado, busco la respuesta en una tabla calculada previamente.
    if( bad255[y] )
        return false;
    // However, I just use a table of size 512
    
  3. Finalmente, intente calcular la raíz cuadrada usando un método similar al lema de Hensel . (No creo que sea aplicable directamente, pero funciona con algunas modificaciones). Antes de hacerlo, divido todas las potencias de 2 con una búsqueda binaria:
    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;
    En este punto, para que nuestro número sea un cuadrado, debe ser 1 mod 8.
    if((x & 7) != 1)
        return false;
    La estructura básica del lema de Hensel es la siguiente. (Nota: código no probado; si no funciona, intente t = 2 u 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.
    La idea es que en cada iteración, agregue un bit en r, la raíz cuadrada "actual" de x; cada raíz cuadrada es un módulo preciso con una potencia cada vez mayor de 2, es decir, t / 2. Al final, r y t / 2-r serán raíces cuadradas de x módulo t / 2. (Tenga en cuenta que si r es una raíz cuadrada de x, entonces también lo es -r. Esto es cierto incluso los números de módulo, pero tenga cuidado, modulo algunos números, las cosas pueden tener incluso más de 2 raíces cuadradas; en particular, esto incluye potencias de 2. ) Debido a que nuestra raíz cuadrada real es menor que 2 ^ 32, en ese momento podemos verificar si r o t / 2-r son raíces cuadradas reales. En mi código real, uso el siguiente bucle modificado:
    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) );
    La aceleración aquí se obtiene de tres maneras: valor de inicio precalculado (equivalente a ~ 10 iteraciones del bucle), salida anterior del bucle y omitiendo algunos valores t. Para la última parte, miro z = r - x * xy configuro que t sea la mayor potencia de 2 dividiendo z con un poco de truco. Esto me permite omitir los valores t que no habrían afectado el valor de r de todos modos. El valor de inicio precalculado en mi caso selecciona el módulo de raíz cuadrada "más pequeño positivo" 8192.

Incluso si este código no funciona más rápido para usted, espero que disfrute algunas de las ideas que contiene. El código completo y probado sigue, incluidas las tablas precalculadas.

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

55
¡Guauu! Intentaré convertir esto a Java y hacer una comparación, así como una verificación de precisión de los resultados. Te dejaré saber lo que encuentre.
Kip

79
Wow, esto es hermoso Había visto a Hensel levantarse antes (calcular las raíces de los polinomios módulo a primo), pero ni siquiera me había dado cuenta de que el lema podía reducirse cuidadosamente para calcular las raíces cuadradas de los números; esto es ... edificante :)
ShreevatsaR

3
@nightcracker No lo hace. 9 < 0 => false`` 9&2 => 0` 9&7 == 5 => false` 9&11 == 8 => false.
primo

53
Maartinus publicó una solución 2 veces más rápida (y mucho más corta) a continuación, un poco más tarde, que no parece estar recibiendo mucho amor.
Jason C

3
Parece que gran parte de la ventaja de velocidad en las diferentes soluciones se obtiene al filtrar los cuadrados obvios. ¿Alguien comparó la situación de filtrar a través de la solución de Maartinus y luego usar la función sqrt, ya que es una función incorporada?
user1914292

378

Llego bastante tarde a la fiesta, pero espero dar una mejor respuesta; más corto y (suponiendo que mi punto de referencia sea ​​correcto) también mucho más rápido .

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

La primera prueba atrapa la mayoría de los no cuadrados rápidamente. Utiliza una tabla de 64 elementos empaquetada en un largo, por lo que no hay costo de acceso a la matriz (indirección y verificación de límites). Para un azar uniforme long, hay un 81.25% de probabilidad de terminar aquí.

La segunda prueba captura todos los números que tienen un número impar de dos en su factorización. El método Long.numberOfTrailingZeroses muy rápido ya que obtiene JIT-ed en una sola instrucción i86.

Después de eliminar los ceros finales, la tercera prueba maneja los números que terminan en 011, 101 o 111 en binario, que no son cuadrados perfectos. También se preocupa por los números negativos y también maneja el 0.

La prueba final recurre a la doublearitmética. Como doubletiene solo 53 bits de mantisa, la conversión de longa doubleincluye redondeo para valores grandes. No obstante, la prueba es correcta (a menos que la prueba sea ​​incorrecta).

Intentar incorporar la idea mod255 no tuvo éxito.


3
Ese enmascaramiento implícito del valor del cambio es un poco ... malvado. ¿Tienes alguna idea de por qué está en la especificación de Java?
dfeuer

66
@dfeuer Supongo que hay dos razones: 1. Cambiar por más no tiene sentido. 2. Es como si el HW funciona y cualquiera que use operaciones bit a bit está interesado en el rendimiento, por lo que hacer cualquier otra cosa estaría mal. - La goodMaskprueba lo hace, pero lo hace antes del cambio a la derecha. Tendría que repetirlo, pero de esta manera es más simple y AFAIK un poco más rápido e igualmente bueno.
maaartinus

3
@dfeuer Para el punto de referencia, es importante dar respuesta lo antes posible, y el recuento cero final en sí no da respuesta; Es solo un paso preparatorio. i86 / amd64 hazlo. No tengo idea de las pequeñas CPU en los móviles, pero en el peor de los casos, Java tiene que generar una instrucción AND para ellos, que seguramente es más simple que al revés.
maaartinus

2
Un @Sebastian probablemente mejor prueba: if ((x & (7 | Integer.MIN_VALUE)) != 1) return x == 0;.
maaartinus

44
"Como el doble tiene solo 56 bits de mantisa" -> Diría que es más probable que tenga uno de 53 bits . También
chux - Restablecer Monica

132

Tendrás que hacer algunos benchmarking. El mejor algoritmo dependerá de la distribución de sus entradas.

Su algoritmo puede ser casi óptimo, pero es posible que desee hacer una comprobación rápida para descartar algunas posibilidades antes de llamar a su rutina de raíz cuadrada. Por ejemplo, mire el último dígito de su número en hexadecimal haciendo un bit y "." Los cuadrados perfectos solo pueden terminar en 0, 1, 4 o 9 en la base 16, por lo que para el 75% de sus entradas (suponiendo que estén distribuidas uniformemente) puede evitar una llamada a la raíz cuadrada a cambio de un poco de giro de bits muy rápido.

Kip comparó el siguiente código que implementa el truco hexadecimal. Al probar los números 1 a 100,000,000, este código se ejecutó dos veces más rápido que el original.

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

Cuando probé el código análogo en C ++, en realidad fue más lento que el original. Sin embargo, cuando eliminé la declaración de cambio, el truco hexadecimal una vez más hizo que el código fuera dos veces más rápido.

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

La eliminación de la declaración de cambio tuvo poco efecto en el código C #.


eso es bastante inteligente ... no habría pensado en eso
warren

Buen punto sobre los bits finales. Intentaría combinar esa prueba con algunos de los otros comentarios aquí.
PeterAllenWebb

3
Excelente solución. ¿Te preguntas cómo se te ocurrió? ¿Es un principio bastante establecido o simplemente algo que descubriste? : D
Jeel Shah

3
@LarsH No es necesario agregar 0.5, vea mi solución para obtener un enlace a la prueba.
maaartinus

2
@JerryGoyal Depende del compilador y de los valores de los casos. En un compilador perfecto, un cambio siempre es al menos tan rápido como si no. Pero los compiladores no son perfectos, por lo que es mejor probarlo, como lo hizo John.
fishinear

52

Estaba pensando en los horribles momentos que pasé en el curso de Análisis Numérico.

Y luego recuerdo, había esta función dando vueltas alrededor de la red desde el código fuente de Quake:

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

Que básicamente calcula una raíz cuadrada, utilizando la función de aproximación de Newton (no puedo recordar el nombre exacto).

Debería ser utilizable e incluso podría ser más rápido, ¡es de uno de los fenomenales juegos de software de identificación!

Está escrito en C ++, pero no debería ser demasiado difícil reutilizar la misma técnica en Java una vez que tenga la idea:

Originalmente lo encontré en: http://www.codemaestro.com/reviews/9

El método de Newton explicado en wikipedia: http://en.wikipedia.org/wiki/Newton%27s_method

Puede seguir el enlace para obtener más explicaciones sobre cómo funciona, pero si no le importa mucho, entonces esto es más o menos lo que recuerdo al leer el blog y al tomar el curso de Análisis numérico:

  • el * (long*) &yes básicamente una función rápida-convertir-a largo para operaciones de números enteros se pueden aplicar en los bytes sin formato.
  • la 0x5f3759df - (i >> 1);línea es un valor semilla precalculado para la función de aproximación.
  • la * (float*) &iconvierte a la parte trasera valor de punto flotante.
  • la y = y * ( threehalfs - ( x2 * y * y ) )línea básicamente itera el valor sobre la función nuevamente.

La función de aproximación proporciona valores más precisos cuanto más itera la función sobre el resultado. En el caso de Quake, una iteración es "lo suficientemente buena", pero si no fuera por usted ... entonces podría agregar tanta iteración como necesite.

Esto debería ser más rápido porque reduce el número de operaciones de división realizadas en el enraizamiento cuadrado ingenuo a una simple división por 2 (en realidad, una * 0.5Foperación de multiplicación) y lo reemplaza con un número fijo de operaciones de multiplicación.


99
Cabe señalar que esto devuelve 1 / sqrt (número), no sqrt (número). He realizado algunas pruebas, y esto falla a partir de n = 410881: la fórmula mágica de John Carmack devuelve 642.00104, cuando la raíz cuadrada real es 641.
Kip

11
Puedes mirar el artículo de Chris Lomonts sobre raíces cuadradas inversas rápidas: lomont.org/Math/Papers/2003/InvSqrt.pdf Utiliza la misma técnica que aquí, pero con un número mágico diferente. El documento explica por qué se eligió el número mágico.

44
Además, beyond3d.com/content/articles/8 y beyond3d.com/content/articles/15 arrojan algo de luz sobre los orígenes de este método. A menudo se le atribuye a John Carmack, pero parece que el código original fue (posiblemente) escrito por Gary Tarolli, Greg Walsh y probablemente otros.

3
Además, no puede escribir flotantes e inpun en Java.
Antimonio

10
@Antimonio que dice? FloatToIntBits e IntToFloatBits han existido desde java 1.0.2.
corsiKa

38

No estoy seguro de si sería más rápido, o incluso preciso, pero podría usar el algoritmo de la raíz cuadrada mágica de John Carmack para resolver la raíz cuadrada más rápido. Probablemente podría probar esto fácilmente para todos los posibles enteros de 32 bits y validar que realmente obtuvo los resultados correctos, ya que es solo una aproximación. Sin embargo, ahora que lo pienso, usar dobles también se está aproximando, así que no estoy seguro de cómo entraría en juego.


10
Creo que el truco de Carmack es bastante inútil en estos días. La instrucción sqrt incorporada es mucho más rápida de lo que solía ser, por lo que es mejor que realice una raíz cuadrada regular y pruebe si el resultado es un int. Como siempre, compárelo.
jalf

44
Esto se rompe a partir de n = 410881, la fórmula mágica de John Carmack devuelve 642.00104, cuando la raíz cuadrada real es 641.
Kip

11
Hace poco utilicé el truco de Carmack en un juego de Java y fue muy efectivo, ya que aceleró alrededor del 40%, por lo que sigue siendo útil, al menos en Java.
finnw

3
@Robert Fraser Sí + 40% en la velocidad de fotogramas general. El juego tenía un sistema de física de partículas que ocupaba casi todos los ciclos de CPU disponibles, dominada por la función de raíz cuadrada y la función de ronda a más cercano entero (que también había optimizado utilizando un truco poco haciendo girar similar.)
finnw

55
El enlace está roto.
Pixar

36

Si hace un corte binario para tratar de encontrar la raíz cuadrada "correcta", puede detectar con bastante facilidad si el valor que tiene es lo suficientemente cercano como para decir:

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

Entonces, habiendo calculado n^2, las opciones son:

  • n^2 = target: hecho, devuelve verdadero
  • n^2 + 2n + 1 > target > n^2 : estás cerca, pero no es perfecto: devuelve falso
  • n^2 - 2n + 1 < target < n^2 : ídem
  • target < n^2 - 2n + 1 : corte binario en una parte inferior n
  • target > n^2 + 2n + 1 : corte binario en un nivel superior n

(Lo sentimos, esto se usa ncomo su suposición actual y targetpara el parámetro. ¡Disculpe la confusión!)

No sé si será más rápido o no, pero vale la pena intentarlo.

EDITAR: El corte binario tampoco tiene que abarcar todo el rango de enteros, por (2^x)^2 = 2^(2x)lo que una vez que haya encontrado el bit establecido superior en su objetivo (que se puede hacer con un truco de giro de bits; me olvido exactamente cómo) puede obtener rápidamente una variedad de posibles respuestas. Eso sí, un ingenuo corte binario solo tomará hasta 31 o 32 iteraciones.


Mi dinero está en este tipo de enfoque. Evite llamar a sqrt () ya que calcula una raíz cuadrada completa y solo necesita los primeros dígitos.
PeterAllenWebb

3
Por otro lado, si el punto flotante se realiza en una unidad FP dedicada, puede estar utilizando todo tipo de trucos divertidos. No me gustaría apostar sin un punto de referencia :) (aunque puedo probarlo esta noche en C #, solo para ver ...)
Jon Skeet

8
Los sqrts de hardware son bastante rápidos en estos días.
Adam Rosenfield el

24

Ejecuté mi propio análisis de varios de los algoritmos en este hilo y obtuve algunos resultados nuevos. Puede ver esos resultados anteriores en el historial de edición de esta respuesta, pero no son precisos, ya que cometí un error y perdí el tiempo analizando varios algoritmos que no están cerca. Sin embargo, sacando lecciones de varias respuestas diferentes, ahora tengo dos algoritmos que aplastan al "ganador" de este hilo. Aquí está lo más importante que hago de manera diferente a todos los demás:

// 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;

Sin embargo, esta línea simple, que la mayoría de las veces agrega una o dos instrucciones muy rápidas, simplifica enormemente la switch-casedeclaración en una declaración if. Sin embargo, puede aumentar el tiempo de ejecución si muchos de los números probados tienen importantes factores de potencia de dos.

Los siguientes algoritmos son los siguientes:

  • Internet - Respuesta publicada de Kip
  • Durron : mi respuesta modificada utilizando la respuesta de un paso como base
  • DurronTwo - Mi respuesta modificada usando la respuesta de dos pasos (por @JohnnyHeggheim), con algunas otras ligeras modificaciones.

Aquí hay un ejemplo de tiempo de ejecución si los números se generan usando 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

Y aquí hay un ejemplo de tiempo de ejecución si se ejecuta solo en el primer millón de largos:

 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

Como puede ver, DurronTwofunciona mejor para entradas grandes, porque usa el truco de magia muy a menudo, pero se golpea en comparación con el primer algoritmo y Math.sqrtporque los números son mucho más pequeños. Mientras tanto, el más simple Durrones un gran ganador porque nunca tiene que dividirse entre 4 muchas veces en el primer millón de números.

Aquí está 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;
}

Y 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;
}

Y mi arnés de referencia: (Requiere 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);
    }
}

ACTUALIZACIÓN: He creado un nuevo algoritmo que es más rápido en algunos escenarios, más lento en otros, he obtenido diferentes puntos de referencia basados ​​en diferentes entradas. Si calculamos el módulo 0xFFFFFF = 3 x 3 x 5 x 7 x 13 x 17 x 241, podemos eliminar el 97.82% de los números que no pueden ser cuadrados. Esto puede hacerse (más o menos) en una línea, con 5 operaciones bit a bit:

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

El índice resultante es 1) el residuo, 2) el residuo + 0xFFFFFFo 3) el residuo + 0x1FFFFFE. Por supuesto, necesitamos tener una tabla de búsqueda para el módulo de residuos 0xFFFFFF, que se trata de un archivo de 3mb (en este caso almacenado como números decimales de texto ASCII, no óptimo pero claramente mejorable con ay ByteBufferasí sucesivamente. Pero como eso es precalculación, no funciona) Importa mucho. Puede encontrar el archivo aquí (o generarlo usted mismo):

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

Lo cargo en una booleanmatriz como esta:

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

Ejemplo de tiempo de ejecución. Se superó Durron(versión uno) en cada prueba que ejecuté.

 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
Una tabla de búsqueda gigante no parece una buena idea. Una pérdida de caché es más lenta (~ 100 a 150 ciclos) que la instrucción sqrt de hardware x86 (~ 20 ciclos). En cuanto al rendimiento, puede soportar una gran cantidad de errores de caché pendientes, pero aún está expulsando otros datos útiles. Una gran tabla de búsqueda solo valdría la pena si fuera MUCHO más rápido que cualquier otra opción, y esta función fue el factor principal en el rendimiento de todo su programa.
Peter Cordes

1
@SwissFrank: ¿es la comprobación de cuadrados perfectos lo único que hace su programa? Una tabla de búsqueda puede verse bien en un microbenchmark que lo llama repetidamente en un ciclo cerrado, pero en un programa real que tiene otros datos en su conjunto de trabajo, no es bueno.
Peter Cordes

1
Un mapa de bits de bits 0x1FFFFFE toma 4 mega bytes si se almacena como un mapa de bits empaquetados. Un hit de caché L3 en una computadora de escritorio Intel moderna tiene> 40 ciclos de latencia, y peor en un Xeon grande; más largo que hardware sqrt + mul latencia. Si se almacena como un byte -map con 1 byte por valor, es de aproximadamente 32 MB; más grande que el caché L3 de cualquier cosa menos un Xeon de muchos núcleos donde todos los núcleos comparten un gran caché. Entonces, si los datos de sus entradas tienen una distribución aleatoria uniforme en un rango de entradas lo suficientemente grande, obtendrá muchas fallas de caché L2 incluso en un bucle cerrado. (L2 privado por núcleo en Intel es de solo 256k, con una latencia de ~ 12 ciclos).
Peter Cordes

1
@SwissFrank: Oh, si todo lo que estás haciendo es verificar la raíz, entonces hay potencial para esto con un mapa de bits para obtener visitas L3. Estaba mirando la latencia, pero muchas fallas pueden estar en fuga a la vez, por lo que el rendimiento es potencialmente bueno. OTOH, el sqrtpsrendimiento SIMD o incluso sqrtpd(doble precisión) no es tan malo en Skylake, pero no es mucho mejor que la latencia en las CPU antiguas. De todos modos, 7-cpu.com/cpu/Haswell.html tiene algunos buenos números experimentales y páginas para otras CPU. El pdf de la guía de microarquitectura de Agner Fog tiene algunos números de latencia de caché para Intel y AMD uarches: agner.org/optimize
Peter Cordes

1
Usar x86 SIMD de Java es un problema, y ​​para cuando agregue el costo de la conversión int-> fp y fp-> int, es posible que un mapa de bits sea mejor. Necesita doubleprecisión para evitar redondear algún número entero fuera del rango + -2 ^ 24 (por lo que un número entero de 32 bits puede estar fuera de eso), y sqrtpdes más lento que sqrtps, además de procesar la mitad de elementos por instrucción (por vector SIMD) .
Peter Cordes

18

Debería ser mucho más rápido usar el método de Newton para calcular la raíz cuadrada entera , luego cuadrar este número y verificar, como lo hace en su solución actual. El método de Newton es la base de la solución Carmack mencionada en algunas otras respuestas. Debería poder obtener una respuesta más rápida ya que solo está interesado en la parte entera de la raíz, lo que le permite detener el algoritmo de aproximación antes.

Otra optimización que puede probar: si la raíz digital de un número no termina en 1, 4, 7 o 9, el número no es un cuadrado perfecto. Esto se puede usar como una forma rápida de eliminar el 60% de sus entradas antes de aplicar el algoritmo de raíz cuadrada más lento.


1
La raíz digital es estrictamente computacionalmente equivalente al módulo, por lo que debe considerarse junto con otros métodos de módulo aquí, como el mod 16 y el mod 255.
Christian Oudard

1
¿Estás seguro de que la raíz digital es equivalente al módulo? Parece ser algo completamente diferente como se explica en el enlace. Observe que la lista es 1,4,7,9 no 1,4,5,9.
Fractaly

1
La raíz digital en el sistema decimal es equivalente a usar el módulo 9 (pozo dr (n) = 1 + ((n-1) mod 9); también un ligero cambio). Los números 0,1,4,5,9 son para el módulo 16, y 0, 1, 4, 7 son para el módulo 9, que corresponden a 1, 4, 7, 9 para la raíz digital.
Hans Olsson

16

Quiero que esta función funcione con todos los enteros con signo de 64 bits positivos

Math.sqrt()funciona con dobles como parámetros de entrada, por lo que no obtendrá resultados precisos para enteros mayores de 2 ^ 53 .


55
De hecho, probé la respuesta en todos los cuadrados perfectos mayores que 2 ^ 53, así como en todos los números desde 5 debajo de cada cuadrado perfecto hasta 5 arriba de cada cuadrado perfecto, y obtengo el resultado correcto. (el error de redondeo se corrige cuando redondeo la respuesta sqrt a un valor largo, luego cuadro ese valor y lo comparo)
Kip

2
@Kip: Creo que he demostrado que funciona .
maaartinus

Los resultados no son perfectamente precisos, pero más precisos de lo que piensas. Si suponemos al menos 15 dígitos exactos después de la conversión a doble y después de la raíz cuadrada, entonces eso es suficiente, porque no necesitamos más de 11:10 dígitos para la raíz cuadrada de 32 bits y menos de 1 para un decimal, porque el +0.5 se redondea al más cercano.
mwfearnley

3
Math.sqrt () no es totalmente preciso, pero no tiene que ser así En la primera publicación, tst es un número entero cercano a sqrt (N). Si N no es un cuadrado, entonces tst * tst! = N, sin importar el valor de tst. Si N es un cuadrado perfecto, entonces sqrt (N) <2 ^ 32, y siempre que sqrt (N) se calcule con un error <0.5, estamos bien.
gnasher729

13

Solo para el registro, otro enfoque es usar la descomposición primaria. Si cada factor de la descomposición es par, entonces el número es un cuadrado perfecto. Entonces, lo que quiere es ver si un número puede descomponerse como un producto de cuadrados de números primos. Por supuesto, no necesita obtener dicha descomposición, solo para ver si existe.

Primero construya una tabla de cuadrados de números primos que sean menores que 2 ^ 32. Esto es mucho más pequeño que una tabla de todos los enteros hasta este límite.

Una solución sería así:

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

Supongo que es un poco críptico. Lo que hace es verificar en cada paso que el cuadrado de un número primo divida el número de entrada. Si lo hace, divide el número por el cuadrado tanto como sea posible, para eliminar este cuadrado de la descomposición primaria. Si por este proceso llegamos a 1, entonces el número de entrada fue una descomposición del cuadrado de los números primos. Si el cuadrado se vuelve más grande que el número en sí, entonces no hay forma de que este cuadrado, o cualquier cuadrado más grande, pueda dividirlo, por lo que el número no puede ser una descomposición de cuadrados de números primos.

Dado el sqrt de hoy en día hecho en hardware y la necesidad de calcular números primos aquí, supongo que esta solución es mucho más lenta. Pero debería dar mejores resultados que la solución con sqrt que no funcionará durante 2 ^ 54, como dice mrzl en su respuesta.


1
la división de enteros es más lenta que FP sqrt en el hardware actual. Esta idea no tiene ninguna posibilidad. >. <Incluso en 2008, el sqrtsdrendimiento de Core2 es uno por 6-58c. Su idives una por 12-36cycles. (latencias similares a los rendimientos: ninguna unidad está canalizada).
Peter Cordes

sqrt no necesita ser perfectamente preciso. Es por eso que verifica mediante un cuadrado al cuadrado el resultado y haciendo una comparación de enteros para decidir si el entero de entrada tenía un sqrt entero exacto.
Peter Cordes

11

Se ha señalado que los últimos ddígitos de un cuadrado perfecto solo pueden tomar ciertos valores. Los últimos ddígitos (en la base b) de un número nson los mismos que el resto cuando nse divide por bd, es decir. en C notación n % pow(b, d).

Esto se puede generalizar a cualquier módulo m, es decir. n % mse puede usar para descartar que algunos porcentajes de números sean cuadrados perfectos. El módulo que está utilizando actualmente es 64, que permite 12, es decir. 19% de los residuos, como posibles cuadrados. Con un poco de codificación encontré el módulo 110880, que solo permite 2016, es decir. 1.8% de los residuos como posibles cuadrados. Entonces, dependiendo del costo de una operación de módulo (es decir, división) y una búsqueda de tabla versus una raíz cuadrada en su máquina, el uso de este módulo podría ser más rápido.

Por cierto, si Java tiene una manera de almacenar una matriz de bits empaquetada para la tabla de búsqueda, no la use. 110880 Las palabras de 32 bits no tienen mucha RAM en estos días y buscar una palabra de máquina será más rápido que recuperar un solo bit.


Agradable. ¿Lo resolviste algebraicamente o por prueba y error? Puedo ver por qué es tan efectivo: muchas colisiones entre cuadrados perfectos, por ejemplo 333 ^ 2% 110880 == 3 ^ 2, 334 ^ 2% 110880 == 26 ^ 2, 338 ^ 2% 110880 == 58 ^ 2 .. .
finnw

IIRC fue fuerza bruta, pero tenga en cuenta que 110880 = 2 ^ 5 * 3 ^ 2 * 5 * 7 * 11, lo que da 6 * 3 * 2 * 2 * 2 - 1 = 143 divisores propios.
Hugh Allen el

Descubrí que debido a las limitaciones de la búsqueda, 44352 funciona mejor, con una tasa de aprobación del 2.6%. Al menos en mi implementación.
Fractaly

1
La división entera ( idiv) es igual o peor en costo a FP sqrt ( sqrtsd) en el hardware x86 actual. Además, completamente en desacuerdo con evitar los campos de bits. La tasa de aciertos de caché será mucho mejor con un campo de bits, y probar un poco en un campo de bits es solo una o dos instrucciones más simples que probar un byte completo. (Para las tablas pequeñas que caben en la memoria caché, incluso como campos que no son de bits, sería mejor una matriz de bytes, no entradas de 32 bits. X86 tiene acceso de un solo byte con la misma velocidad de 32 bits dword.)
Peter Cordes

11

Un problema entero merece una solución entera. Así

Haga una búsqueda binaria en los enteros (no negativos) para encontrar el mayor entero t tal que t**2 <= n. Luego prueba si r**2 = nexactamente. Esto lleva tiempo O (log n).

Si no sabe cómo buscar binariamente los enteros positivos porque el conjunto no tiene límites, es fácil. Empiezas calculando tu función creciente f (arriba f(t) = t**2 - n) en potencias de dos. Cuando vea que se vuelve positivo, ha encontrado un límite superior. Entonces puedes hacer una búsqueda binaria estándar.


En realidad, el tiempo sería al menos O((log n)^2)porque la multiplicación no es un tiempo constante sino que, de hecho, tiene un límite inferior O(log n), que se hace evidente cuando se trabaja con números grandes de precisión múltiple. Pero el alcance de esta wiki parece ser de 64 bits, por lo que tal vez sea nbd.

10

La siguiente simplificación de la solución de maaartinus parece reducir algunos puntos porcentuales del tiempo de ejecución, pero no soy lo suficientemente bueno en la evaluación comparativa para producir una referencia en la que pueda confiar:

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

Valdría la pena comprobar cómo omitir la primera prueba,

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

afectaría el rendimiento


2
Los resultados están aquí . Eliminar la primera prueba es malo, ya que resuelve la mayoría de los casos de manera bastante económica. La fuente está en mi respuesta (actualizada).
maaartinus

9

Para el rendimiento, a menudo tiene que hacer algunos compromisos. Otros han expresado varios métodos, sin embargo, notó que el hack de Carmack fue más rápido hasta ciertos valores de N. Luego, debe verificar la "n" y si es menor que ese número N, use el hack de Carmack, de lo contrario use algún otro método descrito en las respuestas aquí.


También he incorporado tu sugerencia a la solución. Además, buen manejo. :)
Kip

8

Esta es la implementación de Java más rápida que se me ocurrió, usando una combinación de técnicas sugeridas por otros en este hilo.

  • Prueba Mod-256
  • Prueba Inexact mod-3465 (evita la división de enteros a costa de algunos falsos positivos)
  • Raíz cuadrada de punto flotante, redondear y comparar con el valor de entrada

También experimenté con estas modificaciones pero no ayudaron al rendimiento:

  • Prueba mod-255 adicional
  • Dividiendo el valor de entrada por potencias de 4
  • Raíz cuadrada inversa rápida (para trabajar con valores altos de N, necesita 3 iteraciones, lo suficiente para hacerlo más lento que la función de raíz cuadrada de hardware).

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

Deberías deshacerte de la parte de 2 potencias de N desde el principio.

2da Edición La expresión mágica para m a continuación debe ser

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

y no como está escrito

Fin de la 2da edición

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

1ra Edición:

Mejora menor:

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

Fin de la primera edición

Ahora continúa como siempre. De esta manera, cuando llegas a la parte de coma flotante, ya te has deshecho de todos los números cuya parte de 2 potencias es impar (aproximadamente la mitad), y luego solo consideras 1/8 de lo que queda. Es decir, ejecuta la parte de coma flotante en el 6% de los números.


7

El proyecto Euler se menciona en las etiquetas y muchos de los problemas en él requieren verificar números >> 2^64. La mayoría de las optimizaciones mencionadas anteriormente no funcionan fácilmente cuando trabaja con un búfer de 80 bytes.

Utilicé Java BigInteger y una versión ligeramente modificada del método de Newton, una que funciona mejor con enteros. El problema era que los cuadrados exactos n^2convergían en (n-1)lugar de nporque n^2-1 = (n-1)(n+1)y el error final estaba solo un paso debajo del divisor final y el algoritmo terminaba. Fue fácil de solucionar agregando uno al argumento original antes de calcular el error. (Agregue dos para las raíces cúbicas, etc.)

Un buen atributo de este algoritmo es que puede saber de inmediato si el número es un cuadrado perfecto: el error final (no la corrección) en el método de Newton será cero. Una modificación simple también le permite calcular rápidamente en floor(sqrt(x))lugar del entero más cercano. Esto es útil con varios problemas de Euler.


1
Estaba pensando lo mismo acerca de estos algoritmos que no se traducen bien a buffers de precisión múltiple. Así que pensé en poner esto aquí ... En realidad, encontré una prueba de cuadratura probabilística con una mejor complejidad asintótica para grandes números ..... donde las aplicaciones de la teoría de números no se encuentran con poca frecuencia . Aunque no estoy familiarizado con el Proyecto Euler ... parece interesante.

6

Esta es una reelaboración de decimal a binario del antiguo algoritmo de calculadora Marchant (lo siento, no tengo una referencia), en Ruby, adaptado específicamente para esta pregunta:

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

Aquí hay una solución de algo similar (por favor, no me rechace por codificar estilos / olores u O / O torpe: es el algoritmo lo que cuenta, y C ++ no es mi idioma de origen). En este caso, estamos buscando residuos == 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;
};

El número de iteraciones parece O (ln n), donde n es la longitud de bits de v, así que dudo que esto ahorre mucho para v más grande. El punto flotante sqrt es lento, tal vez 100-200 ciclos, pero la matemática entera no es libre tampoco. Una docena de iteraciones con 15 ciclos cada una, y sería un lavado. Aún así, +1 por ser interesante.
Tadmas el

En realidad, creo que XOR puede sumar y restar.
Brent.Longborough

Ese fue un comentario tonto: solo la adición puede ser realizada por un XOR; La resta es aritmética.
Brent.Longborough

1
¿Existe realmente alguna diferencia sustancial entre el tiempo de ejecución de XOR y la suma de todos modos?
Tadmas el

1
@Tadmas: probablemente no sea suficiente para romper la regla de "optimizar más tarde". (:-)
Brent.Longborough

6

La llamada sqrt no es perfectamente precisa, como se ha mencionado, pero es interesante e instructivo que no elimina las otras respuestas en términos de velocidad. Después de todo, la secuencia de instrucciones en lenguaje ensamblador para un sqrt es pequeña. Intel tiene una instrucción de hardware, que Java no utiliza, creo, porque no cumple con IEEE.

Entonces, ¿por qué es lento? Debido a que Java en realidad está llamando a una rutina C a través de JNI, y en realidad es más lento hacerlo que llamar a una subrutina Java, que en sí es más lenta que hacerlo en línea. Esto es muy molesto, y Java debería haber encontrado una solución mejor, es decir, incorporar llamadas de biblioteca de punto flotante si fuera necesario. Oh bien.

En C ++, sospecho que todas las alternativas complejas perderían velocidad, pero no las he verificado todas. Lo que hice, y lo que la gente de Java encontrará útil, es un simple truco, una extensión de las pruebas de casos especiales sugeridas por A. Rex. Use un solo valor largo como una matriz de bits, que no esté marcada en los límites. De esa manera, tiene una búsqueda booleana de 64 bits.

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

La rutina isPerfectSquare5 se ejecuta en aproximadamente 1/3 del tiempo en mi máquina core2 duo. Sospecho que más ajustes a lo largo de la misma línea podrían reducir el tiempo más en promedio, pero cada vez que verifica, está intercambiando más pruebas por más eliminación, por lo que no puede avanzar mucho más en ese camino.

Ciertamente, en lugar de tener una prueba de negativo por separado, puede verificar los 6 bits altos de la misma manera.

Tenga en cuenta que todo lo que estoy haciendo es eliminar posibles cuadrados, pero cuando tengo un caso potencial tengo que llamar al original, en línea, isPerfectSquare.

La rutina init2 se llama una vez para inicializar los valores estáticos de pp1 y pp2. Tenga en cuenta que en mi implementación en C ++, estoy usando unsigned long long, por lo que, dado que está firmado, tendría que usar el operador >>>.

No hay necesidad intrínseca de verificar los límites de la matriz, pero el optimizador de Java tiene que resolver esto bastante rápido, así que no los culpo por eso.


3
Apuesto a que te equivocas dos veces. 1. Intel sqrt cumple con IEEE. Las únicas instrucciones no conformes son las instrucciones goniométricas para argumentos extraños. 2. Java usa intrínsecos para Math.sqrt, no JNI .
maaartinus

1
¿No te olvidaste de usar pp2? Entiendo que pp1se usa para probar los seis bits menos significativos, pero no creo que probar los próximos seis bits tenga sentido.
maaartinus

6

Me gusta la idea de usar un método casi correcto en algunas de las entradas. Aquí hay una versión con un "desplazamiento" más alto. El código parece funcionar y pasa mi caso de prueba simple.

Simplemente reemplace su:

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

código con este:

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

Teniendo en cuenta la longitud de bits general (aunque he usado un tipo específico aquí), traté de diseñar algo simplista como se muestra a continuación. Inicialmente se requiere una verificación simple y obvia para 0,1,2 o <0. Lo siguiente es simple en el sentido de que no intenta usar ninguna función matemática existente. La mayor parte del operador puede ser reemplazado por operadores de bits. Sin embargo, no he probado con ningún dato de referencia. No soy experto en matemáticas ni en diseño de algoritmos informáticos en particular, me encantaría verte señalando el problema. Sé que hay muchas posibilidades de mejora allí.

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: Algún problema con mi navegador.
nabam serbang

1
Necesitas un poco de sangría.
Steve Kuo

5

Verifiqué todos los resultados posibles cuando se observan los últimos n bits de un cuadrado. Al examinar sucesivamente más bits, se pueden eliminar hasta 5/6 de las entradas. De hecho, diseñé esto para implementar el algoritmo de factorización de Fermat, y es muy rápido allí.

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

El último bit de pseudocódigo se puede usar para extender las pruebas para eliminar más valores. Las pruebas anteriores son para k = 0, 1, 2, 3

  • a es de la forma (3 << 2k) - 1
  • b es de la forma (2 << 2k)
  • c es de la forma (2 << 2k + 2) - 1
  • d es de la forma (2 << 2k - 1) * 10

    Primero prueba si tiene un residual cuadrado con módulos de potencia de dos, luego prueba en función de un módulo final, luego usa Math.sqrt para hacer una prueba final. Se me ocurrió la idea desde la publicación superior e intenté extenderla. Agradezco cualquier comentario o sugerencia.

    Actualización: Utilizando la prueba por un módulo, (modSq) y una base de módulo de 44352, mi prueba se ejecuta en el 96% del tiempo de la actualización de OP para números de hasta 1,000,000,000.


  • 2

    Aquí hay una solución de divide y vencerás.

    Si la raíz cuadrada de un número natural ( number) es un número natural ( solution), puede determinar fácilmente un rango solutionbasado en el número de dígitos de number:

    • numbertiene 1 dígito: solutionen rango = 1 - 4
    • numbertiene 2 dígitos: solutionen rango = 3 - 10
    • numbertiene 3 dígitos: solutionen rango = 10-40
    • numbertiene 4 dígitos: solutionen rango = 30-100
    • numbertiene 5 dígitos: solutionen rango = 100 - 400

    ¿Te das cuenta de la repetición?

    Puede usar este rango en un enfoque de búsqueda binaria para ver si hay una solutionpara la cual:

    number == solution * solution

    Aqui esta el codigo

    Aquí está mi clase 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;
        }
    
    }

    Y aquí hay un ejemplo sobre cómo usarlo.

    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
    Me encanta el concepto, pero me gustaría señalar cortésmente una falla importante: los números están en base 2 binario. La conversión de la base 2 a la base 10 toStringes una operación increíblemente costosa en comparación con los operadores bit a bit. Por lo tanto, para satisfacer el objetivo de la pregunta (rendimiento), debe utilizar operadores bit a bit en lugar de cadenas de base 10. Nuevamente, me gusta mucho tu concepto. No obstante, su implementación (tal como está ahora) es, con mucho, la más lenta de todas las soluciones posibles publicadas para la pregunta.
    Jack Giffin

    1

    Si la velocidad es una preocupación, ¿por qué no dividir el conjunto de entradas más comúnmente utilizado y sus valores en una tabla de búsqueda y luego hacer cualquier algoritmo mágico optimizado que se le haya ocurrido para los casos excepcionales?


    El problema es que no hay un "conjunto de entradas de uso común"; por lo general, estoy iterando a través de una lista, por lo que no usaré las mismas entradas dos veces.
    Kip

    1

    ¡Debería ser posible empacar el 'no puede ser un cuadrado perfecto si los últimos X dígitos son N' mucho más eficientemente que eso! Usaré ints de Java de 32 bits y produciré suficientes datos para verificar los últimos 16 bits del número, es decir, 2048 valores int hexadecimales.

    ...

    Okay. O me he topado con alguna teoría de números que está un poco más allá de mí, o hay un error en mi código. En cualquier caso, aquí está el código:

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

    Y aquí están los resultados:

    (ed: elidido por bajo rendimiento en prettify.js; ver el historial de revisiones para ver).


    1

    Método de Newton con aritmética de enteros

    Si desea evitar operaciones no enteras, puede utilizar el siguiente método. Básicamente utiliza el método de Newton modificado para la aritmética de enteros.

    /**
     * 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;
    }

    Esta implementación no puede competir con las soluciones que utilizan Math.sqrt. Sin embargo, su rendimiento puede mejorarse utilizando los mecanismos de filtrado descritos en algunas de las otras publicaciones.


    1

    Calcular raíces cuadradas por el método de Newton es terriblemente rápido ... siempre que el valor inicial sea razonable. Sin embargo, no hay un valor inicial razonable, y en la práctica terminamos con la bisección y el comportamiento log (2 ^ 64).
    Para ser realmente rápidos, necesitamos una forma rápida de obtener un valor inicial razonable, y eso significa que debemos descender al lenguaje máquina. Si un procesador proporciona una instrucción como POPCNT en el Pentium, cuenta los ceros iniciales que podemos usar para tener un valor inicial con la mitad de los bits significativos. Con cuidado podemos encontrar un número fijo de pasos de Newton que siempre será suficiente. (Por lo tanto, antes de la necesidad de bucle y tener una ejecución muy rápida).

    Una segunda solución es a través de la instalación de punto flotante, que puede tener un cálculo rápido de sqrt (como el coprocesador i87). Incluso una excursión a través de exp () y log () puede ser más rápida que Newton degenerada en una búsqueda binaria. Hay un aspecto complicado en esto, un análisis dependiente del procesador de qué y si es necesario un refinamiento posterior.

    Una tercera solución resuelve un problema ligeramente diferente, pero vale la pena mencionarlo porque la situación se describe en la pregunta. Si desea calcular una gran cantidad de raíces cuadradas para números que difieren ligeramente, puede usar la iteración de Newton, si nunca reinicializa el valor inicial, pero simplemente déjelo donde quedó el cálculo anterior. He usado esto con éxito en al menos un problema de Euler.


    Obtener una buena estimación no es demasiado difícil. Puede usar el número de dígitos del número para estimar un límite inferior y superior para la solución. Vea también mi respuesta donde propongo una solución de divide y vencerás.
    MWB

    ¿Cuál es la diferencia entre POPCNT y contar el número de dígitos? Excepto que puedes hacer POPCNT en un nanosegundo.
    Albert van der Horst

    1

    Raíz cuadrada de un número, dado que el número es un cuadrado perfecto.

    La complejidad es 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

    Si desea velocidad, dado que sus enteros son de tamaño finito, sospecho que la forma más rápida implicaría (a) dividir los parámetros por tamaño (por ejemplo, en categorías por conjunto de bits más grande), luego verificar el valor contra una matriz de cuadrados perfectos dentro de ese rango.


    2
    Hay 2 ^ 32 cuadrados perfectos en el rango de un largo. Esta mesa sería enorme. Además, la ventaja de calcular el valor sobre el acceso a la memoria podría ser enorme.
    PeterAllenWebb

    Oh no, no hay, hay 2 ^ 16. 2 ^ 32 es 2 ^ 16 al cuadrado. Hay 2 ^ 16.
    Celestial M Weasel

    3
    sí, pero el rango de un largo es de 64 bits, no de 32 bits. sqrt (2 ^ 64) = 2 ^ 32. (Estoy ignorando el bit de signo para facilitar un poco las matemáticas ... en realidad hay (largo) (2 ^ 31.5) = 3037000499 cuadrados perfectos)
    Kip

    0

    Con respecto al método Carmac, parece que sería bastante fácil repetir una vez más, lo que debería duplicar el número de dígitos de precisión. Después de todo, es un método iterativo extremadamente truncado, el de Newton, con una muy buena primera suposición.

    En cuanto a su mejor momento actual, veo dos micro optimizaciones:

    • mover el cheque vs. 0 después del cheque usando mod255
    • reorganice los poderes de división de cuatro para omitir todos los controles para el caso habitual (75%).

    Es decir:

    // 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;
    }

    Aún mejor podría ser un simple

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

    Obviamente, sería interesante saber cuántos números se seleccionan en cada punto de control; dudo mucho que los controles sean realmente independientes, lo que dificulta las cosas.

    Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
    Licensed under cc by-sa 3.0 with attribution required.