정수의 제곱근이 정수인지 확인하는 가장 빠른 방법


1453

long값이 완벽한 제곱 인지 여부를 결정하는 가장 빠른 방법을 찾고 있습니다 (즉, 제곱근이 다른 정수임).

  1. 내장 Math.sqrt() 함수 를 사용하여 쉬운 방법을 수행 했지만 정수 전용 도메인으로 제한하여 더 빨리 수행 할 수있는 방법이 있는지 궁금합니다.
  2. 조회 테이블을 유지 관리하는 것은 실용적이지 않습니다 ( 제곱이 2 63 미만인 약 2 개의 31.5 정수가 있으므로 ).

내가 지금하고있는 매우 간단하고 간단한 방법은 다음과 같습니다.

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 문제 에서이 기능을 사용하고 있습니다. 따라서 아무도이 코드를 유지할 필요가 없습니다. 이런 종류의 마이크로 최적화는 실제로 차이를 만들 수 있습니다. 도전 과제의 일부는 모든 알고리즘을 1 분 이내에 수행하는 것이므로이 기능은 일부 문제에서 수백만 번 호출되어야합니다.


나는 문제에 대한 다른 해결책을 시도했다.

  • 철저한 테스트 0.5결과, 적어도 내 컴퓨터에는 Math.sqrt () 결과 를 추가 할 필요가 없습니다.
  • 제곱근 역 고속은 빨리했지만, 그것은 잘못된 결과를 준 N> = 410881. 그러나,에 의해 제안 BobbyShaftoe , 우리는 N <410881에 대한 FISR 해킹을 사용할 수 있습니다.
  • 뉴턴의 방법은보다 약간 느 렸습니다 Math.sqrt(). 아마도 Math.sqrt()Newton의 Method와 비슷한 것을 사용 하기 때문일 수 있지만 하드웨어에서 구현되어 Java보다 훨씬 빠릅니다. 또한, 뉴턴의 방법은 여전히 ​​복식을 사용해야했습니다.
  • 정수 수학 만 관련되도록 몇 가지 트릭을 사용하는 수정 된 뉴턴의 방법은 오버플로를 피하기 위해 일부 해킹이 필요했습니다 (이 함수는 모든 양의 64 비트 부호있는 정수와 함께 작동하기를 원합니다) Math.sqrt().
  • 이진 절단은 더 느렸다. 이진 절단은 64 비트 숫자의 제곱근을 찾기 위해 평균 16 번의 패스가 필요하기 때문에 이치에 맞습니다.
  • 요한의 테스트에 따르면, 사용하는 or문은 빠른 C ++에서을 사용하는 것보다 switch,하지만 자바와 C #의 사이에 차이가없는 것으로 나타납니다 or하고 switch.
  • 또한 조회 테이블 (64 부울 값의 개인 정적 배열)을 만들려고했습니다. 그런 다음 switch 나 orstatement 대신 그냥 말합니다 if(lookup[(int)(n&0x3F)]) { test } else return false;. 놀랍게도, 이것은 (약간) 느려졌습니다. 배열 범위는 Java에서 확인 되기 때문 입니다.

21
이것은 int == 32 비트 및 long == 64 비트 인 Java 코드이며 둘 다 서명됩니다.
Kip

14
@ Shreevasta : 큰 값 (2 ^ 53보다 큼)에 대한 테스트를 수행했으며 귀하의 방법은 잘못된 긍정을 제공합니다. 첫 번째로 발생하는 것은 n = 9007199326062755입니다. 이는 완벽한 정사각형이 아니지만 하나로 반환됩니다.
Kip

37
"John Carmack hack"이라고 부르지 마십시오. 그는 그것을 내놓지 않았다.
user9282

84
@mamama-아마도 그 때문입니다. Henry Ford는 차를 발명하지 않았고 Wright Bros.는 비행기를 발명하지 않았으며 Galleleo는 지구가 태양을 중심으로 회전 한 것을 처음으로 발견 한 것은 아니 었습니다 ... 세계는 도난당한 발명품으로 구성되어 있습니다. 사랑).
Robert Fraser

4
((1<<(n&15))|65004) != 0세 가지 별도의 검사를 수행하는 대신 과 같은 것을 사용하여 'quickfail'의 속도를 약간 높일 수 있습니다 .
Nabb

답변:


735

적어도 내 CPU (x86) 및 프로그래밍 언어 (C / C ++)에서 6 비트 + Carmack + sqrt 코드보다 ~ 35 % 더 빠른 방법을 알아 냈습니다. Java 요소가 어떻게 작동하는지 모르기 때문에 결과가 다를 수 있습니다.

내 접근 방식은 세 가지입니다.

  1. 먼저 명확한 답변을 걸러냅니다. 여기에는 음수와 마지막 4 비트를 보는 것이 포함됩니다. (마지막 6 개를 보는 것이 도움이되지 않는다는 것을 알았습니다.) 또한 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인지 확인하십시오. 이는 3 개의 고유 한 소수의 곱이므로 잔차 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 's lemma 와 유사한 방법을 사용하여 제곱근을 계산하십시오 . (나는 그것이 직접 적용 할 수 없다고 생각하지만 약간의 수정으로 작동합니다.) 그렇게하기 전에 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 mod 8이어야합니다.
    if((x & 7) != 1)
        return false;
    헨젤의 기본 정리의 기본 구조는 다음과 같습니다. (참고 : 테스트되지 않은 코드. 작동하지 않으면 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.
    각 반복마다 r에 하나의 비트, 즉 x의 "현재"제곱근을 추가하는 것이 좋습니다. 각 제곱근은 정확한 모듈로 2의 더 큰 거듭 제곱, 즉 t / 2입니다. 마지막에, r과 t / 2-r은 x 모듈로 t / 2의 제곱근이됩니다. (r이 x의 제곱근이면 -r도 마찬가지입니다.) 이것은 모듈로 수조차도 사실이지만 모듈로 일부 숫자를 조심하면 2 제곱근 이상을 가질 수 있습니다. 특히 2의 제곱을 포함합니다. ) 실제 제곱근이 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 * xt를 비트 트릭으로 나누는 2의 최대 거듭 제곱으로 t를 설정했습니다. 이것은 어쨌든 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 => false, 9&2 => 0, 9&7 == 5 => false, 9&11 == 8 => false.
primo

53
Maartinus는 조금 후에 2 배 더 빠른 솔루션 (그리고 훨씬 더 짧은)을 게시 했습니다 .
Jason C

3
명백한 사각형을 필터링하여 다른 솔루션에서 많은 속도 이점을 얻는 것처럼 보입니다. 누구든지 Maartinus의 솔루션을 통해 필터링 한 다음 sqrt 함수를 내장 함수로 사용하는 상황을 벤치마킹 했습니까?
user1914292

377

나는 파티에 꽤 늦었지만 더 나은 답변을 제공하기를 희망합니다. 더 짧고 (나의 벤치 마크 가 정확 하다고 가정 ) 훨씬 빠릅니다 .

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 %입니다.

두 번째 테스트는 인수 분해에서 홀수 2를 갖는 모든 숫자를 포착합니다. 방법Long.numberOfTrailingZeros 은 단일 i86 명령어로 JIT-ed되므로 매우 빠릅니다.

후행 0을 삭제 한 후 세 번째 테스트는 011, 101 또는 111로 끝나는 숫자를 이진수로 처리합니다. 이는 완전한 제곱이 아닙니다. 또한 음수를 고려하고 0도 처리합니다.

최종 테스트는 double산술로 돌아갑니다 . AS는 double단지 53 비트의 가수 부로부터 변환 갖는다 long으로는 double큰 값을 라운딩하는 단계를 포함한다. 그럼에도 불구하고 테스트는 정확합니다 ( 증거 가 잘못 되지 않은 경우 ).

mod255 아이디어를 통합하려는 시도는 성공하지 못했습니다.


3
시프트 값의 암시 적 마스킹은 약간 ... 악합니다. 왜 이것이 Java 사양에 속하는지 아십니까?
dfeuer

6
@dfeuer 나는 두 가지 이유가 있다고 생각한다. 2. 그것은 HW 작업과 같으며 비트 단위 작업을 사용하는 사람은 성능에 관심이 있으므로 다른 일을하는 것은 잘못된 것입니다. -goodMask 테스트를 수행하지만 그것을 수행 하기 전에 올바른 변화. 따라서 반복해야하지만이 방법은 더 간단하고 AFAIK는 조금 더 빠르고 똑같이 좋습니다.
maaartinus

3
@dfeuer 벤치 마크의 경우 최대한 빨리 답변을하는 것이 중요하며 후행 제로 자체는 답변이 없습니다. 그것은 단지 준비 단계입니다. i86 / amd64는 그렇게합니다. 모바일의 소형 CPU에 대해서는 전혀 모르지만 최악의 경우 Java는 AND 명령어를 생성해야합니다. 이는 다른 방법보다 훨씬 간단합니다.
maaartinus

2
@Sebastian 아마 더 나은 테스트 : if ((x & (7 | Integer.MIN_VALUE)) != 1) return x == 0;.
maaartinus

4
"더블은 56 비트 가수만을가집니다"-> 53 비트를 가질 가능성이 더 높습니다 . 또한
chux-복원 모니카

132

벤치마킹을 수행해야합니다. 최상의 알고리즘은 입력 분포에 따라 다릅니다.

알고리즘이 거의 최적 일 수 있지만, 제곱근 루틴을 호출하기 전에 몇 가지 가능성을 배제하기 위해 빠른 검사를 수행 할 수 있습니다. 예를 들어, 비트 단위 "and"를 수행하여 16 진수로 된 숫자의 마지막 숫자를보십시오. 완벽한 제곱은 16 진에서 0, 1, 4 또는 9로만 끝날 수 있으므로 입력의 75 % (균일하게 분포되어 있다고 가정)에 대해 매우 빠른 비트 트위들 링 대신에 제곱근에 대한 호출을 피할 수 있습니다.

Kip은 16 진 트릭을 구현하는 다음 코드를 벤치마킹했습니다. 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 문을 제거하면 16 진수 트릭으로 코드가 두 배 빠릅니다.

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

3
@LarsH 0.5를 추가 할 필요가 없습니다. 증명에 대한 링크는 내 솔루션을 참조하십시오.
maaartinus

2
@JerryGoyal 컴파일러와 케이스 값에 따라 다릅니다. 완벽한 컴파일러에서 스위치는 항상 if-else만큼 빠릅니다. 그러나 컴파일러는 완벽하지 않으므로 John이 시도한 것처럼 시도해 보는 것이 가장 좋습니다.
fishinear

52

나는 수치 분석 과정에서 보낸 끔찍한 시간에 대해 생각하고있었습니다.

그리고 나는 Quake Source 코드에서 'net'주위를 돌고있는이 기능이 있다는 것을 기억합니다.

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에 설명 된 Newton의 방법 : http://en.wikipedia.org/wiki/Newton%27s_method

작동 방식에 대한 자세한 설명을 보려면 링크를 따라갈 수 있지만, 신경 쓰지 않으면 블로그를 읽고 수치 분석 과정을 수강 할 때 대략적으로 기억합니다.

  • 그만큼 * (long*) &y 기본적으로 정수 연산이 생의 바이트에 적용 할 수 있도록 빠른 변환 - 투 - 긴 기능입니다.
  • 0x5f3759df - (i >> 1);라인 근사 함수에 대한 미리 계산 된 시드 값이다.
  • * (float*) &i값을 부동 소수점으로 다시 변환합니다.
  • y = y * ( threehalfs - ( x2 * y * y ) )라인 bascially 다시 기능 이상 값으로 반복.

근사 함수는 결과에 대해 함수를 반복할수록 더 정확한 값을 제공합니다. Quake의 경우 하나의 반복이 "충분히"충분하지만 그렇지 않은 경우 필요한만큼 반복을 추가 할 수 있습니다.

순진한 제곱근에서 수행되는 나눗셈 연산의 수를 단순 나누기 2 (실제로 * 0.5F곱하기 연산) 로 줄이고 대신 고정 된 곱셈 연산으로 대체 하기 때문에 더 빠릅니다 .


9
이것은 sqrt (number)가 아니라 1 / sqrt (number)를 리턴한다는 점에 유의해야합니다. 몇 가지 테스트를 수행했는데 n = 410881에서 시작하지 못합니다. 실제 제곱근이 641 일 때 John Carmack 마술 공식은 642.00104를 반환합니다.
Kip

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에서 float 및 int를 입력 할 수 없습니다.
안티몬

10
@ 앤티 모니 누가? FloatToIntBitsIntToFloatBits 는 java 1.0.2부터 사용되었습니다.
corsiKa

38

그것이 더 빠르거나 정확한지 확실하지 않지만 John Carmack의 Magical Square Root 알고리즘을 사용하여 제곱근을 더 빨리 풀 수 있습니다. 가능한 모든 32 비트 정수에 대해 이것을 쉽게 테스트하고 appoximation 일뿐이므로 실제로 올바른 결과를 얻었는지 확인할 수 있습니다. 그러나 이제는 더블을 사용하는 것도 근사치이므로 어떻게 작동하는지 잘 모르겠습니다.


10
요즘 카맥의 트릭은 무의미하다고 생각합니다. 내장 sqrt 명령어는 예전보다 훨씬 빠르기 때문에 정규 제곱근을 수행하고 결과가 int인지 테스트하는 것이 더 나을 수 있습니다. 항상 그렇듯이 벤치마킹하십시오.
jalf

4
실제 제곱근이 641 일 때 John Carmack의 마법 공식은 642.00104를 반환합니다.
Kip

11
나는 최근에 Java 게임에서 Carmack의 트릭을 사용했으며 매우 효과적이어서 약 40 %의 속도를 제공하므로 적어도 Java에서는 여전히 유용합니다.
finnw

3
@Robert Fraser 예 전체 프레임 속도에서 + 40 %입니다. (. 나는 또한 비슷한 비트 만지작 해킹을 사용하여 최적화 한) 게임은 제곱근 함수와 원 - 투 - 가장 가까운 정수 기능에 의해 지배 거의 모든 사용 가능한 CPU 사이클을 차지 입자 물리 시스템, 한
finnw

5
링크가 끊어졌습니다.
Pixar

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 : 당신은 가까이 있지만 완벽하지는 않습니다.
  • 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 ()는 전체 제곱근을 계산하므로 처음 몇 자리 숫자 만 필요하므로 sqrt ()를 호출하지 마십시오.
PeterAllenWebb

3
반면에 부동 소수점이 전용 FP 장치에서 수행되는 경우 모든 종류의 재미있는 트릭을 사용하고있을 수 있습니다. 나는 벤치 마크없이 거기에 내기로 :) (난 그냥 ...보고, C #으로 오늘 밤을하지만 시도 할 수 있습니다) 않을 것 같은
존 소총

8
요즘 하드웨어 sqrt는 실제로 매우 빠릅니다.
Adam Rosenfield

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의 거듭 제곱을 갖는 경우 런타임에 추가 할 수 있습니다.

아래 알고리즘은 다음과 같습니다.

  • 인터넷 -킵의 답변 게시
  • Durron- 원 패스 응답을 기본으로 사용하는 수정 된 답변
  • DurronTwo- 약간의 수정을 거쳐 2 패스 답변 (@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

그리고 처음 백만 번만 실행되는 샘플 런타임은 다음과 같습니다.

 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것은 첫 번째 수의 ​​숫자로 4 번 여러 번 나눌 필요가 없기 때문에 큰 승자입니다.

여기 있습니다 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 캘리퍼스 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 텍스트 10 진수로 저장되지만 최적은 아니지만 명확하게 개선 할 수는 없습니다 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내가 한 모든 시행에서 (버전 1)을 이겼 습니다.

 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 사이클). 처리량 측면에서 많은 캐시 미스를 유지할 수 있지만 여전히 다른 유용한 데이터를 제거하고 있습니다. 거대한 조회 테이블은 다른 옵션보다 훨씬 빠르면 가치가 있으며이 기능은 전체 프로그램 성능의 주요 요인이었습니다.
Peter Cordes

1
@SwissFrank : 프로그램이 하는 유일한 일을 완벽하게 검사 합니까? 룩업 테이블은 타이트한 루프에서 반복적으로 호출하는 마이크로 벤치 마크에서는 좋지만 작업 세트에 다른 데이터가있는 실제 프로그램에서는 좋지 않습니다.
Peter Cordes

1
0x1FFFFFE 비트의 비트 맵이 얻어 4 메가 바이트 꽉 찬 비트 맵으로 저장되어 있다면. 최신 인텔 데스크탑에서 L3 캐시 히트 는> 40주기의 대기 시간을 가지며 큰 제온에서는 더 나쁩니다. 하드웨어 sqrt + mul 대기 시간보다 깁니다. 값당 1 바이트 의 바이트 맵으로 저장된 경우 약 32MB입니다. 모든 코어가 하나의 거대한 캐시를 공유하는 많은 코어 Xeon 이외의 L3 캐시보다 큽니다. 따라서 입력 데이터가 충분히 넓은 범위의 입력에 대해 균일 한 무작위 분포를 갖는 경우 엄격한 루프에서도 많은 L2 캐시 미스가 발생합니다. (인텔의 개인 코어 당 L2는 ~ 12 사이클 대기 시간 만 256K입니다.)
피터 코르

1
@SwissFrank : 아, 만약 당신이하고있는 모든 것이 루트 점검이라면, L3 적중을 얻을 수있는 비트 맵이있을 가능성이 있습니다. 대기 시간을보고 있었지만 한 번에 많은 미스가 발생할 수 있으므로 처리량이 잠재적으로 좋습니다. OTOH, SIMD sqrtps처리량 또는 짝수 sqrtpd(배정도)는 Skylake에서 그리 나쁘지는 않지만 이전 CPU의 대기 시간보다 훨씬 좋지 않습니다. 어쨌든 7-cpu.com/cpu/Haswell.html 에는 멋진 실험 번호와 다른 CPU 용 페이지가 있습니다. Agner 안개의 microarch 가이드 PDF는 인텔에 대한 몇 가지 캐시 대기 번호를 가지고 있으며, AMD는 uarches : agner.org/optimize
피터 코르

1
Java에서 x86 SIMD를 사용하는 것은 문제이며 int-> fp 및 fp-> int 변환 비용을 추가 할 때 비트 맵이 더 나을 수 있습니다. double+ -2 ^ 24 범위를 벗어나는 일부 정수의 반올림을 피하기 위해 정밀도 가 필요 하므로 (32 비트 정수는이 범위를 벗어날 수 있음) 명령 당 절반의 요소 만 처리 할뿐 아니라 sqrtpd속도가 느립니다 sqrtps(SIMD 벡터 당). .
Peter Cordes

18

Newton의 방법 을 사용하여 정수 제곱근 을 계산 한 다음 현재 솔루션에서와 같이이 숫자를 제곱하고 확인하는 것이 훨씬 빠릅니다 . 뉴턴의 방법은 다른 답변에서 언급 된 Carmack 솔루션의 기초입니다. 근음의 정수 부분에만 관심이 있으므로 근사 알고리즘을 더 빨리 중지 할 수 있으므로 더 빠른 답변을 얻을 수 있습니다.

시도 할 수있는 또 다른 최적화 : 숫자 의 디지털 루트 가 1, 4, 7 또는 9로 끝나지 않으면 숫자가 완벽한 제곱 이 아닙니다 . 느린 제곱근 알고리즘을 적용하기 전에 입력의 60 %를 제거하는 빠른 방법으로 사용할 수 있습니다.


1
디지털 루트는 계산적으로 모듈로와 동일하므로 mod 16 및 mod 255와 같은 다른 모듈로 방법과 함께 고려해야합니다.
Christian Oudard

1
디지털 루트가 모듈로와 동일합니까? 링크에서 설명한 것처럼 완전히 다른 것처럼 보입니다. 목록은 1,4,5,9가 아닌 1,4,7,9입니다.
Fractaly

1
십진법의 디지털 근은 모듈로 9를 사용하는 것과 같습니다 (well dr (n) = 1 + ((n-1) mod 9); 따라서 약간의 이동도 있습니다). 숫자 0,1,4,5,9는 모듈로 16에 대한 것이고 0, 1, 4, 7은 모듈로 9에 대한 것입니다. 이는 디지털 루트의 경우 1, 4, 7, 9에 해당합니다.
한스 올슨 12

16

이 함수가 모든 양의 64 비트 부호있는 정수와 함께 작동하기를 원합니다

Math.sqrt()double을 입력 매개 변수로 사용하므로 2 ^ 53 보다 큰 정수에 대한 정확한 결과를 얻을 수 없습니다 .


5
실제로 2 ^ 53보다 큰 모든 정사각형에 대한 답을 테스트했으며 각 정사각형 아래 5에서 각 정사각형 위 5까지의 모든 숫자를 테스트했으며 올바른 결과를 얻습니다. (반올림 오류는 sqrt 답변을 long으로 반올림 한 다음 그 값을 제곱하고 비교할 때 수정됩니다)
Kip

2
@ 킵 : 나는 그것이 작동 한다는 것을 증명했다고 생각합니다 .
maaartinus

결과는 완벽하지는 않지만 생각보다 정확합니다. 두 배로 변환 한 후 제곱근으로 변환 한 후 15 자릿수 이상의 정확한 자릿수를 가정하면 32 비트 제곱근의 경우 11 자리 이하, 소수점 이하 자릿수의 경우 1 자리 미만이 필요하기 때문에 충분합니다. +0.5는 가장 가까운 반올림합니다.
mwfearnley

3
Math.sqrt ()는 완전히 정확하지는 않지만 반드시 그렇지는 않습니다. 첫 번째 포스트에서 tst는 sqrt (N)에 가까운 정수입니다. N이 제곱이 아닌 경우 tst의 값이 무엇이든 관계없이 tst * tst! = N입니다. N이 완전한 제곱이면 sqrt (N) <2 ^ 32이고 sqrt (N)이 0.5보다 작은 오류로 계산되는 한 괜찮습니다.
gnasher729

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를 사용하는 솔루션보다 2 ^ 54 이상 작동하지 않는 솔루션보다 더 나은 결과를 제공해야합니다.


1
정수 분할은 현재 하드웨어에서 FP sqrt보다 느립니다. 이 아이디어는 기회가 없습니다. >. <2008 년에도 Core2의 sqrtsd처리량은 6-58c 당 1입니다. 그것은 idiv12-36cycles 당 하나입니다. (처리량과 유사한 대기 시간 : 두 장치 모두 파이프 라인되지 않음).
Peter Cordes

sqrt는 완벽하게 정확할 필요는 없습니다. 따라서 정수-제곱 결과를 확인하고 정수 비교를 수행하여 입력 정수에 정확한 정수 sqrt가 있는지 확인하십시오.
Peter Cordes

11

d완벽한 정사각형 의 마지막 숫자는 특정 값만 취할 수 있다고 지적되었습니다 . d숫자 의 마지막 자릿수 (기준 b) 는로 나눈 n나머지와 동일합니다 . C 표기법으로 .nbdn % pow(b, d)

이것은 임의의 계수 m, 즉 n % m완벽한 제곱에서 숫자의 일부 비율을 배제하는 데 사용될 수 있습니다. 현재 사용하고있는 계수는 64이므로 12를 사용할 수 있습니다. 가능한 사각형의 나머지 19 %. 약간의 코딩으로 2016 년 만 허용하는 계수 110880을 발견했습니다. 가능한 사각형의 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

1
정수 나누기 ( idiv)는 sqrtsd현재 x86 하드웨어에서 FP sqrt ( )와 같거나 더 저렴합니다 . 또한 비트 필드를 피하는 데 완전히 동의하지 않습니다. 캐시 적중률은 비트 필드에서 훨씬 나을 것이며 비트 필드에서 비트를 테스트하는 것은 전체 바이트를 테스트하는 것보다 하나 또는 두 개의 간단한 명령 일뿐입니다. (비트 필드가 아닌 경우에도 캐시에 적합한 작은 테이블의 경우 32 비트 정수가 아닌 바이트 배열이 가장 좋습니다. x86은 32 비트 dword와 동일한 속도로 단일 바이트 액세스를 제공합니다.)
Peter Cordes

11

정수 문제는 정수 솔루션이 필요합니다. 그러므로

음수가 아닌 정수에 대해 이진 검색을 수행하여 가장 큰 정수 t를 찾습니다 t**2 <= n. 그런 다음 r**2 = n정확하게 테스트하십시오 . 시간 O (log n)가 걸립니다.

세트가 무제한이기 때문에 양의 정수를 이진 검색하는 방법을 모른다면 쉽습니다. f(t) = t**2 - n2의 거듭 제곱에서 증가하는 함수 f (위 )를 계산하여 시작합니다 . 긍정적으로 바뀌면 상한을 찾았습니다. 그런 다음 표준 이진 검색을 수행 할 수 있습니다.


실제로는 O((log n)^2)곱셈이 상수 시간이 아니지만 실제로는 하한이 있기 때문에 시간은 최소가 될 것 입니다 O(log n). 이는 큰 배정 밀도 숫자로 작업 할 때 분명해집니다. 그러나이 위키의 범위는 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

9

성능을 위해서는 종종 일부 절충을해야합니다. 다른 사람들은 다양한 방법을 표현했지만 Carmack의 핵은 N의 특정 값보다 빠르다는 것을 알았습니다. 그런 다음 "n"을 확인하고 그 수가 N보다 작은 경우 Carmack의 핵을 사용하십시오. 그렇지 않으면 다른 방법을 사용하십시오 여기에 대한 답변.


귀하의 제안을 솔루션에도 통합했습니다. 또한 좋은 핸들. :)
Kip

8

이것은이 스레드에서 다른 사람들이 제안한 기술 조합을 사용하여 얻을 수있는 가장 빠른 Java 구현입니다.

  • Mod-256 테스트
  • 부정확 한 모드 -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 제곱 부분을 제거해야합니다.

2 번째 편집 아래 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;

1 차 편집 :

사소한 개선 :

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

1 차 편집 종료

이제 평소대로 계속하십시오. 이런 식으로, 부동 소수점 부분에 도달 할 때까지 2 전력 부분이 홀수 (약 절반) 인 모든 숫자를 이미 제거한 다음 남은 것의 1/8 만 고려합니다. 즉, 숫자의 6 %에서 부동 소수점 부분을 실행합니다.


7

프로젝트 오일러는 태그에 언급되어 있으며 많은 문제가 숫자를 확인해야합니다 >> 2^64 . 위에서 언급 한 대부분의 최적화는 80 바이트 버퍼로 작업 할 때 쉽게 작동하지 않습니다.

Java BigInteger와 약간 수정 된 Newton의 메소드 버전을 사용했는데 정수로 더 잘 작동합니다. 문제는 정확한 사각형이었다 n^2수렴 (n-1)대신 n하기 때문에 n^2-1 = (n-1)(n+1)최종 오류가 하나의 최종 제수 아래 단계 및 종료 알고리즘이었다. 오류를 계산하기 전에 원래 인수에 하나를 추가하여 쉽게 해결할 수있었습니다. (입방근 등에 2 개 추가)

이 알고리즘의 좋은 특성 중 하나는 숫자가 완벽한 제곱인지 즉시 알 수 있다는 것입니다. 뉴턴 방법의 최종 오류 (수정 아님)는 0입니다. 간단한 수정을 통해 floor(sqrt(x))가장 가까운 정수 대신 빠르게 계산할 수 있습니다 . 이것은 몇 가지 오일러 문제에 유용합니다.


1
다중 알고리즘 버퍼로 잘 변환되지 않는 알고리즘에 대해서도 같은 생각을했습니다. 그래서 나는 이것을 여기에 붙일 것이라고 생각했습니다 ... 실제로 실제로 는 숫자 이론 응용 프로그램이 드물게 발견되지 않는 수많은 숫자에 대해 더 나은 점근 적 복잡성을 가진 확률 제곱 테스트 를 발견했습니다. 그래도 Project Euler에 익숙하지 않은 ... 흥미로워 보입니다.

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 사이클로 12 번 반복되며 세척됩니다. 흥미롭게도 +1.
Tadmas

실제로, 덧셈과 뺄셈은 XOR에 의해 수행 될 수 있다고 생각합니다.
브렌트 롱 버러

그것은 멍청한 의견이었습니다. XOR은 추가 만 할 수 있습니다. 빼기는 산술입니다.
Brent. Longborough

1
XOR의 런타임과 추가 사이에 실질적인 차이가 있습니까?
Tadmas

1
@Tadmas : "나중에 최적화"규칙을 위반하기에 충분하지 않을 수 있습니다. (:-)
브렌트 롱보로

6

sqrt 호출은 언급 한 바와 같이 완벽하게 정확하지는 않지만 속도 측면에서 다른 답변을 날려 버리지 않는 것은 흥미롭고 유익합니다. 결국 sqrt에 대한 일련의 어셈블리 언어 명령어는 작습니다. 인텔은 하드웨어 명령을 가지고 있는데, 이는 IEEE가 준수하지 않기 때문에 Java에서 사용되지 않습니다.

왜 느린가요? Java는 실제로 JNI를 통해 C 루틴을 호출하기 때문에 실제로는 인라인을 수행하는 것보다 느린 Java 서브 루틴을 호출하는 것보다 느립니다. 이것은 매우 성가 시며 Java는 더 나은 솔루션, 즉 필요한 경우 부동 소수점 라이브러리 호출을 빌드하는 방법을 생각해 냈습니다. 오 잘

C ++에서는 모든 복잡한 대안이 속도를 잃을 것이라고 생각하지만 모두 확인하지는 않았습니다. 내가 한 일과 Java 사람들이 유용하게 생각하는 것은 A. Rex가 제안한 특수 사례 테스트의 확장 인 간단한 해킹입니다. 하나의 긴 값을 비트 배열로 사용하십시오. 그렇게하면 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 루틴은 코어 2 듀오 머신에서 약 1/3의 시간으로 실행됩니다. 동일한 라인을 따라 추가 조정하면 평균 시간이 더 단축 될 수 있지만 의심 할 때마다 더 많은 제거를 위해 더 많은 테스트를 수행하므로 해당 도로에서 더 멀리 갈 수는 없습니다.

확실히 음수에 대한 별도의 테스트를 수행하는 대신 높은 6 비트를 동일한 방식으로 확인할 수 있습니다.

내가하고있는 모든 것은 가능한 사각형을 제거하는 것이지만 잠재적 인 사례가있을 때 원래의 인라인 isPerfectSquare를 호출해야합니다.

pp1 및 pp2의 정적 값을 초기화하기 위해 init2 루틴이 한 번 호출됩니다. C ++로 구현할 때 unsigned long long을 사용하고 있으므로 서명했기 때문에 >>> 연산자를 사용해야합니다.

본질적으로 배열을 검사 할 필요는 없지만 Java의 옵티마이 저는이 물건을 매우 빨리 파악해야하므로 그것을 비난하지 않습니다.


3
네가 틀렸다는 게 틀림 없어 1. Intel sqrt는 IEEE를 준수합니다. 적합하지 않은 명령어는 범위 인수에 대한 선형 지시어입니다. 2. Java는 JNI가없는 Math.sqrt에 내장 함수를 사용합니다 .
maaartinus

1
사용하는 것을 잊지 않았습니까 pp2? 나는 pp1이것이 6 개의 최하위 비트를 테스트하는 데 사용 된다는 것을 이해 하지만 다음 6 비트를 테스트하는 것이 의미가 있다고 생각하지 않습니다.
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;  
}  

@ 키트 : 브라우저에 문제가 있습니다.
nabam serbang

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
    • number 3 자리 숫자 : solution 범위 = 10-40
    • number 4 자리 숫자 : solution 범위 = 30-100
    • number5 자리 숫자 : 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 진법이다. 베이스 2를베이스 10으로 변환 toString하는 것은 비트 연산자에 비해 엄청나게 비싼 작업입니다. 따라서 문제의 성능 (성능)을 만족 시키려면 기본 10 문자열 대신 비트 연산자를 사용해야합니다. 다시 한번, 나는 당신의 개념을 정말로 좋아합니다. 그럼에도 불구하고 (현재와 같이) 구현은 질문에 대해 게시 된 모든 가능한 솔루션 중에서 가장 느립니다.
    잭 지핀

    1

    속도가 중요한 경우 가장 일반적으로 사용되는 입력 세트와 해당 값을 룩업 테이블로 분할 한 다음 예외적 인 경우를 위해 최적화 된 매직 알고리즘을 수행하십시오.


    문제는 "일반적으로 사용되는 입력 세트"가 없다는 것입니다. 일반적으로 목록을 반복하므로 동일한 입력을 두 번 사용하지 않습니다.
    Kip

    1

    '마지막 X 자리가 N이면 완벽 사각형이 될 수 없습니다'를 그보다 훨씬 효율적으로 포장 할 수 있어야합니다! Java 32 비트 int를 사용하고 숫자의 마지막 16 비트를 확인하기에 충분한 데이터를 생성합니다. 즉, 2048 16 진수 int 값입니다.

    ...

    확인. 저를 약간 넘어서는 몇 가지 이론에 부딪 쳤거나 코드에 버그가 있습니다. 어쨌든 다음은 코드입니다.

    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

    정수 산술을 가진 뉴턴의 방법

    정수가 아닌 작업을 피하려면 아래 방법을 사용할 수 있습니다. 기본적으로 정수 연산을 위해 수정 된 Newton의 방법을 사용합니다.

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

    시작 값이 합리적이라면 Newton의 방법으로 제곱근을 계산하는 것은 엄청나게 빠릅니다. 그러나 합리적인 시작 가치는 없으며 실제로 우리는 이분법과 log (2 ^ 64) 동작으로 끝납니다.
    정말 빠르려면 합리적인 시작 값을 얻는 빠른 방법이 필요합니다. 즉, 기계 언어로 내려 가야합니다. 프로세서가 Pentium에서 POPCNT와 같은 명령을 제공하는 경우 선행 0을 계산하여 유효 비트의 절반으로 시작 값을 갖는 데 사용할 수 있습니다. 주의를 기울이면 항상 충분한 수의 뉴턴 단계를 찾을 수 있습니다. (따라서 루프를 반복해야하고 실행 속도가 매우 빠릅니다.)

    두 번째 솔루션은 부동 소수점 기능을 통해 진행되는데, i87 코 프로세서와 같이 빠른 sqrt 계산이 가능합니다. exp () 및 log ()를 통한 여행조차도 Newton이 이진 검색으로 생성 된 것보다 빠를 수 있습니다. 이것에 대한 까다로운 측면이 있습니다. 프로세서 개선은 나중에 그리고 구체화가 필요한지 여부에 대한 분석입니다.

    세 번째 해결책은 약간 다른 문제를 해결하지만 상황이 문제에 설명되어 있기 때문에 언급 할 가치가 있습니다. 약간 다른 숫자에 대해 많은 제곱근을 계산하려면 시작 값을 다시 초기화하지 않고 이전 계산이 중단 된 곳에 그대로두면 Newton 반복을 사용할 수 있습니다. 나는 적어도 하나의 오일러 문제에서 이것을 성공으로 사용했습니다.


    좋은 견적을 얻는 것은 그리 어렵지 않습니다. 숫자의 자릿수를 사용하여 솔루션의 하한과 상한을 추정 할 수 있습니다. 또한 분할 및 정복 솔루션을 제안하는 답변을 참조하십시오.
    MWB

    POPCNT와 자릿수 계산의 차이점은 무엇입니까? 단 1 초 안에 POPCNT를 수행 할 수 있습니다.
    Albert van der Horst

    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이 있습니다.
    Celestial M 족제비

    3
    예, 그러나 long의 범위는 32 비트가 아닌 64 비트입니다. sqrt (2 ^ 64) = 2 ^ 32 (나는 수학을 조금 더 쉽게하기 위해 부호 비트를 무시하고 있습니다 ... 실제로 (긴) (2 ^ 31.5) = 3037000499 완벽한 제곱이 있습니다)
    Kip

    0

    Carmac 방법과 관련하여 한 번 더 반복하는 것이 매우 쉬운 것처럼 보이며 정확도의 두 배가되어야합니다. 결국 그것은 매우 잘린 반복 방법입니다-뉴턴 (Newton), 아주 좋은 첫 추측.

    현재 최고와 관련하여 두 가지 미세 최적화가 있습니다.

    • mod255를 사용하여 확인 후 확인 대 0을 이동하십시오.
    • 일반적인 (75 %) 경우에 대한 모든 점검을 건너 뛰려면 4의 제곱 거듭 제곱을 재정렬하십시오.

    즉 :

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

    분명히, 각 체크 포인트에서 몇 개의 숫자가 제거되는지를 아는 것은 흥미로울 것입니다.

    당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
    Licensed under cc by-sa 3.0 with attribution required.