Cách nhanh nhất để xác định xem căn bậc hai của số nguyên có phải là số nguyên không


1454

Tôi đang tìm cách nhanh nhất để xác định xem một longgiá trị có phải là một hình vuông hoàn hảo hay không (nghĩa là căn bậc hai của nó là một số nguyên khác):

  1. Tôi đã thực hiện nó một cách dễ dàng, bằng cách sử dụng Math.sqrt() hàm tích hợp, nhưng tôi tự hỏi liệu có cách nào để làm điều đó nhanh hơn bằng cách giới hạn bản thân trong miền chỉ có số nguyên.
  2. Việc duy trì bảng tra cứu là không thực tế (vì có khoảng 2 31,5 số nguyên có bình phương nhỏ hơn 2 63 ).

Đây là cách rất đơn giản và đơn giản mà tôi đang làm bây giờ:

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

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

Lưu ý: Tôi đang sử dụng chức năng này trong nhiều vấn đề của Project Euler . Vì vậy, không ai khác sẽ phải duy trì mã này. Và loại tối ưu hóa vi mô này thực sự có thể tạo ra sự khác biệt, vì một phần của thách thức là thực hiện mọi thuật toán trong vòng chưa đầy một phút và chức năng này sẽ cần phải được gọi hàng triệu lần trong một số vấn đề.


Tôi đã thử các giải pháp khác nhau cho vấn đề:

  • Sau khi thử nghiệm toàn diện, tôi thấy rằng việc thêm 0.5vào kết quả của Math.sqrt () là không cần thiết, ít nhất là không phải trên máy của tôi.
  • Căn bậc hai nghịch đảo nhanh nhanh hơn, nhưng nó cho kết quả không chính xác cho n> = 410881. Tuy nhiên, theo đề xuất của BobbyShaftoe , chúng tôi có thể sử dụng hack FISR cho n <410881.
  • Phương pháp của Newton chậm hơn một chút so với Math.sqrt(). Điều này có lẽ là do Math.sqrt()sử dụng một cái gì đó tương tự như Phương pháp của Newton, nhưng được triển khai trong phần cứng nên nó nhanh hơn nhiều so với Java. Ngoài ra, Phương pháp của Newton vẫn yêu cầu sử dụng gấp đôi.
  • Phương pháp của Newton đã sửa đổi, sử dụng một vài thủ thuật để chỉ có toán học số nguyên tham gia, yêu cầu một số hack để tránh tràn (tôi muốn hàm này hoạt động với tất cả các số nguyên có dấu 64 bit dương), và nó vẫn chậm hơn Math.sqrt().
  • Chặt nhị phân thậm chí còn chậm hơn. Điều này có ý nghĩa bởi vì trung bình nhị phân sẽ yêu cầu 16 lượt đi để tìm căn bậc hai của một số 64 bit.
  • Theo các thử nghiệm của John, sử dụng các orcâu lệnh trong C ++ nhanh hơn so với sử dụng a switch, nhưng trong Java và C # dường như không có sự khác biệt giữa orswitch.
  • Tôi cũng đã thử tạo một bảng tra cứu (như một mảng tĩnh riêng gồm 64 giá trị boolean). Sau đó, thay vì chuyển đổi hoặc ortuyên bố, tôi sẽ chỉ nói if(lookup[(int)(n&0x3F)]) { test } else return false;. Thật ngạc nhiên, điều này là (chỉ một chút) chậm hơn. Điều này là do giới hạn mảng được kiểm tra trong Java .

21
Đây là mã Java, trong đó int == 32 bit và dài == 64 bit và cả hai đều được ký.
Kip

14
@Shreevasta: Tôi đã thực hiện một số thử nghiệm trên các giá trị lớn (lớn hơn 2 ^ 53) và phương pháp của bạn đưa ra một số kết quả dương tính giả. Cái đầu tiên gặp phải là cho n = 9007199326062755, đây không phải là một hình vuông hoàn hảo nhưng được trả về là một.
Kip

37
Xin đừng gọi nó là "John Carmack hack." Anh không nghĩ ra nó.
dùng9282

84
@mamama - Có lẽ, nhưng nó được quy cho anh ta. Henry Ford đã không phát minh ra chiếc xe, Wright Bros. không phát minh ra máy bay và Galleleo không phải là người đầu tiên tìm ra Trái đất xoay quanh mặt trời ... thế giới được tạo thành từ những phát minh bị đánh cắp (và yêu và quý).
Robert Fraser

4
Bạn có thể nhận được một sự gia tăng tốc độ nhỏ trong 'quickfail' bằng cách sử dụng một cái gì đó như ((1<<(n&15))|65004) != 0, thay vì có ba kiểm tra riêng biệt.
Nabb

Câu trả lời:


736

Tôi đã tìm ra một phương pháp hoạt động nhanh hơn ~ 35% so với mã 6 bit + mã của bạn, ít nhất là với CPU (x86) và ngôn ngữ lập trình (C / C ++) của tôi. Kết quả của bạn có thể khác nhau, đặc biệt là vì tôi không biết yếu tố Java sẽ diễn ra như thế nào.

Cách tiếp cận của tôi là ba lần:

  1. Đầu tiên, lọc ra câu trả lời rõ ràng. Điều này bao gồm các số âm và nhìn vào 4 bit cuối cùng. (Tôi thấy việc nhìn vào sáu người cuối cùng không giúp được.) Tôi cũng trả lời có cho 0. (Khi đọc đoạn mã dưới đây, lưu ý rằng đầu vào của tôi là int64 x.)
    if( x < 0 || (x&2) || ((x & 7) == 5) || ((x & 11) == 8) )
        return false;
    if( x == 0 )
        return true;
  2. Tiếp theo, hãy kiểm tra xem đó có phải là modulo vuông 255 = 3 * 5 * 17. Bởi vì đó là sản phẩm của ba số nguyên tố riêng biệt, chỉ có khoảng 1/8 số dư mod 255 là hình vuông. Tuy nhiên, theo kinh nghiệm của tôi, việc gọi toán tử modulo (%) có chi phí cao hơn lợi ích người ta nhận được, vì vậy tôi sử dụng các thủ thuật bit liên quan đến 255 = 2 ^ 8-1 để tính toán dư lượng. (Để tốt hơn hoặc tồi tệ hơn, tôi không sử dụng thủ thuật đọc từng byte riêng lẻ trong một từ, chỉ theo bitwise và và dịch chuyển.)
    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.
    Để thực sự kiểm tra xem phần dư có phải là hình vuông không, tôi tìm câu trả lời trong bảng được tính toán trước.
    if( bad255[y] )
        return false;
    // However, I just use a table of size 512
  3. Cuối cùng, hãy thử tính căn bậc hai bằng một phương pháp tương tự bổ đề của Hensel . (Tôi không nghĩ rằng nó có thể áp dụng trực tiếp, nhưng nó hoạt động với một số sửa đổi.) Trước khi làm điều đó, tôi chia ra tất cả các quyền hạn của 2 với một tìm kiếm nhị phân:
    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;
    Tại thời điểm này, để số của chúng ta là một hình vuông, nó phải là 1 mod 8.
    if((x & 7) != 1)
        return false;
    Cấu trúc cơ bản của bổ đề của Hensel là như sau. (Lưu ý: mã chưa được kiểm tra; nếu nó không hoạt động, hãy thử t = 2 hoặc 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.
    Ý tưởng là ở mỗi lần lặp, bạn thêm một bit vào r, căn bậc hai "hiện tại" của x; mỗi căn bậc hai là modulo chính xác có công suất lớn hơn và lớn hơn bằng 2, cụ thể là t / 2. Cuối cùng, r và t / 2-r sẽ là căn bậc hai của x modulo t / 2. (Lưu ý rằng nếu r là căn bậc hai của x, thì cũng như vậy. ) Bởi vì căn bậc hai thực tế của chúng tôi nhỏ hơn 2 ^ 32, tại thời điểm đó, chúng tôi thực sự có thể kiểm tra xem r hoặc t / 2-r có phải là căn bậc hai thực sự hay không. Trong mã thực tế của tôi, tôi sử dụng vòng lặp sửa đổi sau:
    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) );
    Việc tăng tốc ở đây có được theo ba cách: giá trị bắt đầu được tính toán trước (tương đương với ~ 10 lần lặp của vòng lặp), thoát khỏi vòng lặp trước đó và bỏ qua một số giá trị t. Đối với phần cuối cùng, tôi nhìn vào z = r - x * xvà đặt t là lũy thừa lớn nhất của 2 phép chia z với một mẹo nhỏ. Điều này cho phép tôi bỏ qua các giá trị t sẽ không ảnh hưởng đến giá trị của r. Giá trị bắt đầu được tính toán trước trong trường hợp của tôi chọn ra modulo căn bậc hai "nhỏ nhất dương" 8192.

Ngay cả khi mã này không hoạt động nhanh hơn đối với bạn, tôi hy vọng bạn thích một số ý tưởng mà nó chứa. Hoàn thành, kiểm tra mã sau, bao gồm các bảng được tính toán trước.

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
Ồ Tôi sẽ cố gắng chuyển đổi nó thành Java và so sánh, cũng như kiểm tra độ chính xác về kết quả. Tôi sẽ cho bạn biết những gì tôi tìm thấy.
Kip

79
Wow, nó thật đẹp Tôi đã thấy Hensel nâng trước đó (tính toán gốc đa thức modulo một số nguyên tố) nhưng tôi thậm chí còn không nhận ra bổ đề có thể được hạ xuống một cách cẩn thận trong suốt quá trình tính toán căn bậc hai của số; đây là ... nâng cao :)
ShreevatsaR

3
@nightcracker Không. 9 < 0 => false, 9&2 => 0, 9&7 == 5 => false, 9&11 == 8 => false.
primo

53
Maartinus đăng một giải pháp nhanh hơn gấp 2 lần (và ngắn hơn nhiều) xuống bên dưới, một lát sau, điều đó dường như không nhận được nhiều tình yêu.
Jason C

3
Có vẻ như rất nhiều lợi thế về tốc độ trong các giải pháp khác nhau có được bằng cách lọc ra các ô vuông rõ ràng. Có ai đã chuẩn hóa tình huống lọc ra thông qua giải pháp của Maartinus và sau đó chỉ sử dụng hàm sqrt như đó là một hàm tích hợp?
dùng1914292

377

Tôi đến bữa tiệc khá muộn, nhưng tôi hy vọng sẽ cung cấp một câu trả lời tốt hơn; ngắn hơn và (giả sử điểm chuẩn của tôi là chính xác) cũng nhanh hơn nhiều .

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

Thử nghiệm đầu tiên bắt hầu hết các hình vuông không nhanh chóng. Nó sử dụng một bảng 64 mục được đóng gói trong một thời gian dài, do đó không có chi phí truy cập mảng (kiểm tra giới hạn và kiểm tra giới hạn). Đối với ngẫu nhiên thống nhất long, có xác suất 81,25% kết thúc tại đây.

Thử nghiệm thứ hai bắt tất cả các số có số twos lẻ trong hệ số của chúng. Phương pháp Long.numberOfTrailingZerosnày rất nhanh vì nó được JIT-ed thành một lệnh i86 duy nhất.

Sau khi bỏ các số 0 ở cuối, thử nghiệm thứ ba xử lý các số kết thúc bằng 011, 101 hoặc 111 ở dạng nhị phân, không có hình vuông hoàn hảo. Nó cũng quan tâm đến các số âm và cũng xử lý 0.

Bài kiểm tra cuối cùng rơi trở lại doublesố học. Như doublechỉ có 53 bit mantissa, việc chuyển đổi từ longđể doublebao gồm làm tròn cho các giá trị lớn. Tuy nhiên, thử nghiệm là chính xác (trừ khi bằng chứng là sai).

Cố gắng kết hợp ý tưởng mod255 đã không thành công.


3
Việc che giấu giá trị thay đổi đó là một chút ... xấu xa. Bạn có biết tại sao đó là trong đặc tả Java không?
dfeuer

6
@dfeuer Tôi đoán có hai lý do: 1. Chuyển dịch nhiều hơn không có ý nghĩa. 2. Giống như CTNH hoạt động và bất kỳ ai sử dụng các thao tác bitwise đều quan tâm đến hiệu suất, do đó, làm bất cứ điều gì khác sẽ là sai. - Bài goodMaskkiểm tra thực hiện nó, nhưng nó thực hiện trước khi chuyển đúng. Vì vậy, bạn phải lặp lại nó, nhưng cách này đơn giản hơn và AFAIK nhanh hơn một chút và tốt không kém.
maaartinus

3
@dfeuer Đối với điểm chuẩn, điều quan trọng là phải đưa ra câu trả lời càng sớm càng tốt, và chính số đếm không có dấu không đưa ra câu trả lời; nó chỉ là một bước chuẩn bị. i86 / amd64 làm điều đó. Không có ý tưởng về các CPU nhỏ trong điện thoại di động, nhưng tệ nhất, Java phải tạo ra một lệnh AND cho chúng, điều này chắc chắn đơn giản hơn so với cách khác.
maaartinus

2
@Sebastian Một thử nghiệm có lẽ tốt hơn : if ((x & (7 | Integer.MIN_VALUE)) != 1) return x == 0;.
maaartinus

4
"Vì double chỉ có 56 bit mantissa" -> Tôi có thể nói nó có nhiều khả năng có một bit 53 bit . Ngoài ra
chux - Tái lập Monica

132

Bạn sẽ phải làm một số điểm chuẩn. Thuật toán tốt nhất sẽ phụ thuộc vào việc phân phối đầu vào của bạn.

Thuật toán của bạn có thể gần như tối ưu, nhưng bạn có thể muốn kiểm tra nhanh để loại trừ một số khả năng trước khi gọi thói quen căn bậc hai của bạn. Ví dụ: nhìn vào chữ số cuối cùng của số của bạn bằng hex bằng cách thực hiện một chút "và". Các ô vuông hoàn hảo chỉ có thể kết thúc bằng 0, 1, 4 hoặc 9 trong cơ sở 16, vì vậy, đối với 75% đầu vào của bạn (giả sử chúng được phân phối đồng đều), bạn có thể tránh một cuộc gọi đến căn bậc hai để đổi lấy một số bit rất nhanh.

Kip đã điểm chuẩn mã sau đây thực hiện thủ thuật hex. Khi kiểm tra các số từ 1 đến 100.000.000, mã này chạy nhanh gấp đôi so với ban đầu.

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

Khi tôi kiểm tra mã tương tự trong C ++, nó thực sự chạy chậm hơn bản gốc. Tuy nhiên, khi tôi loại bỏ câu lệnh switch, thủ thuật hex một lần nữa làm cho mã nhanh gấp đôi.

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

Loại bỏ câu lệnh chuyển đổi ít ảnh hưởng đến mã C #.


điều đó khá thông minh ... sẽ không nghĩ về điều đó
warren

Điểm hay về các bit trailing. Tôi sẽ cố gắng kết hợp bài kiểm tra đó với một số nhận xét khác ở đây.
Peter ALLenWebb

3
Giải pháp tuyệt vời. Tự hỏi làm thế nào bạn đến với nó? Là một nguyên tắc khá thành lập hoặc chỉ là một cái gì đó bạn tìm ra? : D
Jeel Shah

3
@LarsH Không cần thêm 0,5, hãy xem giải pháp của tôi để biết liên kết đến bằng chứng.
maaartinus

2
@JerryGidel Nó phụ thuộc vào trình biên dịch và các giá trị của các trường hợp. Trong một trình biên dịch hoàn hảo, một chuyển đổi luôn luôn ít nhất là nhanh như nếu khác. Nhưng trình biên dịch không hoàn hảo, vì vậy tốt nhất bạn nên dùng thử, giống như John đã làm.
câu cá

52

Tôi đã suy nghĩ về những khoảng thời gian khủng khiếp mà tôi đã trải qua trong khóa học Phân tích số.

Và sau đó tôi nhớ, có chức năng này xoay quanh 'mạng từ mã Nguồn 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;
}

Về cơ bản tính toán một căn bậc hai, sử dụng hàm xấp xỉ của Newton (không thể nhớ tên chính xác).

Nó có thể sử dụng được và thậm chí có thể nhanh hơn, đó là từ một trong những trò chơi của phần mềm id phi thường!

Nó được viết bằng C ++ nhưng không quá khó để sử dụng lại kỹ thuật tương tự trong Java một khi bạn có ý tưởng:

Ban đầu tôi tìm thấy nó tại: http://www.codemaestro.com/reviews/9

Phương pháp của Newton được giải thích tại wikipedia: http://en.wikipedia.org/wiki/Newton%27s_method

Bạn có thể theo liên kết để được giải thích thêm về cách thức hoạt động của nó, nhưng nếu bạn không quan tâm nhiều, thì đây là những gì tôi nhớ được khi đọc blog và từ khi tham gia khóa học Phân tích số:

  • các * (long*) &ycơ bản là một chức năng nhanh chóng chuyển đổi-to-dài nên hoạt động số nguyên có thể được áp dụng trên các byte thô.
  • các 0x5f3759df - (i >> 1);dòng là một giá trị giống tính trước cho hàm xấp xỉ.
  • các * (float*) &ichuyển đổi giá trị trở lại điểm nổi.
  • các y = y * ( threehalfs - ( x2 * y * y ) )dòng bascially lặp giá trị so với chức năng một lần nữa.

Hàm xấp xỉ cho các giá trị chính xác hơn khi bạn lặp lại hàm nhiều hơn kết quả. Trong trường hợp của Quake, một lần lặp là "đủ tốt", nhưng nếu nó không dành cho bạn ... thì bạn có thể thêm số lần lặp như bạn cần.

Điều này sẽ nhanh hơn bởi vì nó làm giảm số lượng các hoạt động phân chia được thực hiện trong root vuông ngây thơ xuống một phép chia đơn giản cho 2 (thực tế là một * 0.5Fhoạt động nhân) và thay thế nó bằng một số hoạt động nhân cố định thay thế.


9
Cần lưu ý rằng điều này trả về 1 / sqrt (số), không phải sqrt (số). Tôi đã thực hiện một số thử nghiệm và điều này không bắt đầu từ n = 410881: công thức ma thuật John Carmack trả về 642.00104, khi căn bậc hai thực tế là 641.
Kip

11
Bạn có thể xem bài viết của Chris Lomonts về căn bậc hai nghịch đảo nhanh: lomont.org/Math/Papers/2003/InvSqrt.pdf Nó sử dụng kỹ thuật tương tự như ở đây, nhưng với một số ma thuật khác. Bài viết giải thích tại sao số ma thuật được chọn.

4
Ngoài ra, beyond3d.com/content/articles/8beyond3d.com/content/articles/15 làm sáng tỏ nguồn gốc của phương pháp này. Nó thường được quy cho John Carmack, nhưng có vẻ như mã gốc là (có thể) được viết bởi Gary Tarolli, Greg Walsh và có lẽ những người khác.

3
Ngoài ra, bạn không thể đánh máy float và ints trong Java.
Antimon

10
@Antimony ai nói? FloatToIntBitsIntToFloatBits đã xuất hiện kể từ java 1.0.2.
corsiKa

38

Tôi không chắc liệu nó sẽ nhanh hơn hay thậm chí chính xác hơn, nhưng bạn có thể sử dụng thuật toán Magical Square Root của John Carmack để giải quyết căn bậc hai nhanh hơn. Bạn có thể dễ dàng kiểm tra điều này cho tất cả các số nguyên 32 bit có thể và xác thực rằng bạn thực sự có kết quả chính xác, vì đó chỉ là một sự đánh giá. Tuy nhiên, bây giờ tôi nghĩ về nó, sử dụng gấp đôi cũng gần đúng, vì vậy tôi không chắc điều đó sẽ diễn ra như thế nào.


10
Tôi tin rằng mánh khóe của Carmack ngày nay khá vô nghĩa. Hướng dẫn sqrt tích hợp nhanh hơn rất nhiều so với trước đây, vì vậy bạn có thể tốt hơn là chỉ thực hiện một căn bậc hai thông thường và kiểm tra nếu kết quả là một int. Như mọi khi, điểm chuẩn nó.
jalf

4
Điều này phá vỡ bắt đầu từ n = 410881, công thức ma thuật John Carmack trả về 642.00104, khi căn bậc hai thực tế là 641.
Kip

11
Gần đây tôi đã sử dụng thủ thuật của Carmack trong một trò chơi Java và nó rất hiệu quả, giúp tăng tốc khoảng 40%, vì vậy nó vẫn hữu ích, ít nhất là trong Java.
vây

3
@Robert Fraser Có + 40% trong tốc độ khung hình chung. Trò chơi có một hệ thống vật lý hạt mà chiếm gần như toàn bộ chu kỳ CPU có sẵn, chi phối bởi các chức năng căn bậc hai và hàm round-to-gần-số nguyên (mà tôi cũng đã được tối ưu hóa bằng cách sử dụng tương tự chút twiddling hack.)
finnw

5
Liên kết bị hỏng.
Pixar

36

Nếu bạn thực hiện thao tác nhị phân để cố gắng tìm căn bậc hai "đúng", bạn hoàn toàn có thể dễ dàng phát hiện xem giá trị bạn nhận được có đủ gần để nói không:

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

Vì vậy, đã tính toán n^2, các tùy chọn là:

  • n^2 = target: xong, trả lại đúng
  • n^2 + 2n + 1 > target > n^2 : bạn đang ở gần, nhưng nó không hoàn hảo: trả về false
  • n^2 - 2n + 1 < target < n^2 : ditto
  • target < n^2 - 2n + 1 : nhị phân trên thấp hơn n
  • target > n^2 + 2n + 1 : nhị phân trên cao hơn n

(Xin lỗi, điều này sử dụng nnhư dự đoán hiện tại của bạn và targetcho tham số. Xin lỗi vì sự nhầm lẫn!)

Tôi không biết liệu điều này sẽ nhanh hơn hay không, nhưng nó đáng để thử.

EDIT: Chặt nhị phân cũng không phải mất toàn bộ phạm vi số nguyên, (2^x)^2 = 2^(2x)vì vậy, một khi bạn đã tìm thấy bit set hàng đầu trong mục tiêu của mình (có thể được thực hiện bằng thủ thuật xoay đôi bit; tôi quên chính xác làm thế nào) bạn có thể nhanh chóng nhận được một loạt các câu trả lời tiềm năng. Nhắc bạn, một nhị phân ngây thơ vẫn sẽ chỉ mất tối đa 31 hoặc 32 lần lặp.


Tiền của tôi là về cách tiếp cận này. Tránh gọi sqrt () vì nó đang tính một căn bậc hai đầy đủ và bạn chỉ cần một vài chữ số đầu tiên.
Peter ALLenWebb

3
Mặt khác, nếu điểm nổi đang được thực hiện trong một đơn vị FP chuyên dụng, thì nó có thể đang sử dụng tất cả các loại thủ thuật thú vị. Tôi sẽ không muốn đặt cược vào nó mà không có điểm chuẩn :) (Tôi có thể thử tối nay mặc dù trong C #, chỉ để xem ...)
Jon Skeet

8
Phần cứng sqrts thực sự là khá nhanh những ngày này.
Adam Rosenfield

24

Tôi đã chạy phân tích của riêng tôi về một số thuật toán trong chủ đề này và đưa ra một số kết quả mới. Bạn có thể thấy những kết quả cũ đó trong lịch sử chỉnh sửa của câu trả lời này, nhưng chúng không chính xác, vì tôi đã mắc lỗi và lãng phí thời gian để phân tích một số thuật toán không đóng. Tuy nhiên, rút ​​ra bài học từ một số câu trả lời khác nhau, bây giờ tôi có hai thuật toán đè bẹp "người chiến thắng" của chủ đề này. Đây là điều cốt lõi tôi làm khác với mọi người:

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

Tuy nhiên, dòng đơn giản này, phần lớn thời gian thêm một hoặc hai hướng dẫn rất nhanh, đơn giản hóa rất nhiều switch-casecâu lệnh thành một câu lệnh if. Tuy nhiên, nó có thể thêm vào thời gian chạy nếu nhiều số được kiểm tra có sức mạnh đáng kể của hai yếu tố.

Các thuật toán dưới đây như sau:

  • Internet - Câu trả lời được đăng của Kip
  • Durron - Câu trả lời được sửa đổi của tôi bằng cách sử dụng câu trả lời một lượt làm cơ sở
  • DurronTwo - Câu trả lời được sửa đổi của tôi bằng cách sử dụng câu trả lời hai lượt (của @JohnnyHeggheim), với một số sửa đổi nhỏ khác.

Dưới đây là thời gian chạy mẫu nếu các số được tạo bằng 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

Và đây là thời gian chạy mẫu nếu nó chỉ chạy trong một triệu lần đầu tiên:

 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

Như bạn có thể thấy, DurronTwotốt hơn cho các đầu vào lớn, bởi vì nó được sử dụng trò ảo thuật rất thường xuyên, nhưng bị tắc nghẽn so với thuật toán đầu tiên và Math.sqrtvì các con số nhỏ hơn rất nhiều. Trong khi đó, đơn giản hơn Durronlà một người chiến thắng khổng lồ vì nó không bao giờ phải chia 4 nhiều lần trong một triệu số đầu tiên.

Đây là 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;
}

Và khai thác điểm chuẩn của tôi: (Yêu cầu 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);
    }
}

CẬP NHẬT: Tôi đã tạo một thuật toán mới nhanh hơn trong một số trường hợp, chậm hơn trong các kịch bản khác, tôi đã nhận được các điểm chuẩn khác nhau dựa trên các đầu vào khác nhau. Nếu chúng ta tính toán modulo 0xFFFFFF = 3 x 3 x 5 x 7 x 13 x 17 x 241, chúng ta có thể loại bỏ 97,82% số không thể là bình phương. Điều này có thể (sắp xếp) được thực hiện trong một dòng, với 5 thao tác bitwise:

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

Chỉ số kết quả là 1) dư lượng, 2) dư lượng + 0xFFFFFFhoặc 3) dư lượng + 0x1FFFFFE. Tất nhiên, chúng ta cần phải có một bảng tra cứu dư lượng modulo 0xFFFFFF, đó là khoảng một tệp 3mb (trong trường hợp này được lưu dưới dạng số thập phân văn bản ascii, không tối ưu nhưng rõ ràng có thể ứng biến được với một ByteBuffercái gì đó. Vấn đề rất nhiều. Bạn có thể tìm thấy tệp ở đây (hoặc tự tạo tệp):

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

Tôi tải nó vào một booleanmảng như thế này:

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

Ví dụ thời gian chạy. Nó đánh bại Durron(phiên bản một) trong mọi thử nghiệm tôi chạy.

 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
Một bảng tra cứu khổng lồ dường như không phải là một ý tưởng tốt. Lỗi bộ nhớ cache chậm hơn (~ 100 đến 150 chu kỳ) so với hướng dẫn sqrt phần cứng x86 (~ 20 chu kỳ). Thông qua thông tin, bạn có thể duy trì rất nhiều lỗi nhớ cache nổi bật, nhưng bạn vẫn đang đuổi dữ liệu hữu ích khác. Một bảng tra cứu khổng lồ sẽ chỉ có giá trị nếu nó nhanh hơn rất nhiều so với bất kỳ tùy chọn nào khác và chức năng này là yếu tố chính trong hiệu suất của toàn bộ chương trình của bạn.
Peter Cordes

1
@SwissFrank: kiểm tra hình vuông hoàn hảo là điều duy nhất chương trình của bạn làm? Một bảng tra cứu có thể trông tốt trong một microbenchmark gọi nó liên tục trong một vòng lặp chặt chẽ, nhưng trong một chương trình thực sự có dữ liệu khác trong tập làm việc của nó, nó không tốt.
Peter Cordes

1
Một bitmap gồm các bit 0x1FFFFFE mất 4 mega byte nếu được lưu trữ dưới dạng bitmap đóng gói. Một L3 Cache hit trên một máy tính để bàn của Intel hiện đại có> 40 chu kỳ của độ trễ, và tồi tệ hơn trên Xeon lớn; dài hơn phần cứng sqrt + độ trễ mul. Nếu được lưu dưới dạng byte -map với 1 byte cho mỗi giá trị, thì khoảng 32 MB; lớn hơn bộ đệm L3 của bất cứ thứ gì ngoại trừ Xeon nhiều lõi trong đó tất cả lõi đều chia sẻ một bộ đệm lớn. Vì vậy, nếu dữ liệu đầu vào của bạn có phân phối ngẫu nhiên thống nhất trên một phạm vi đầu vào đủ lớn, bạn sẽ nhận được rất nhiều lỗi nhớ cache L2 ngay cả trong một vòng lặp chặt chẽ. (L2 riêng cho mỗi lõi trên Intel chỉ 256k, với độ trễ ~ 12 chu kỳ.)
Peter Cordes

1
@SwissFrank: Ồ, nếu tất cả những gì bạn đang làm là kiểm tra root, thì có khả năng điều này với một bitmap để có được lượt truy cập L3. Tôi đã xem xét độ trễ, nhưng nhiều lần bỏ lỡ có thể trong chuyến bay cùng một lúc, vì vậy thông lượng có khả năng tốt. sqrtpsThông lượng OTOH, SIMD hoặc thậm chí sqrtpd(độ chính xác kép) không quá tệ trên Skylake, nhưng không tốt hơn nhiều so với độ trễ trên các CPU cũ. Dù sao 7-cpu.com/cpu/Haswell.html có một số số thử nghiệm đẹp và các trang cho các CPU khác. Hướng dẫn microarch của Agner Fog pdf có một số số độ trễ bộ đệm cho các uarch của Intel và AMD: agner.org/optizes
Peter Cordes

1
Sử dụng x86 SIMD từ Java là một vấn đề và vào thời điểm bạn thêm chi phí chuyển đổi int-> fp và fp-> int, thật hợp lý khi một bitmap có thể tốt hơn. Bạn cần doubleđộ chính xác để tránh làm tròn một số nguyên bên ngoài phạm vi + -2 ^ 24 (do đó, một số nguyên 32 bit có thể nằm ngoài số đó) và sqrtpdchậm hơn sqrtpscũng như chỉ xử lý một nửa số phần tử trên mỗi lệnh (trên mỗi vectơ SIMD) .
Peter Cordes

18

Sẽ nhanh hơn nhiều khi sử dụng phương pháp của Newton để tính toán Căn bậc hai số nguyên , sau đó bình phương số này và kiểm tra, như bạn làm trong giải pháp hiện tại của mình. Phương pháp của Newton là cơ sở cho giải pháp Carmack được đề cập trong một số câu trả lời khác. Bạn sẽ có thể nhận được câu trả lời nhanh hơn vì bạn chỉ quan tâm đến phần nguyên của gốc, cho phép bạn dừng thuật toán gần đúng sớm hơn.

Một tối ưu hóa khác mà bạn có thể thử: Nếu Root kỹ thuật số của một số không kết thúc bằng 1, 4, 7 hoặc 9 thì số đó không phải là một hình vuông hoàn hảo. Điều này có thể được sử dụng như một cách nhanh chóng để loại bỏ 60% đầu vào của bạn trước khi áp dụng thuật toán căn bậc hai chậm hơn.


1
Rễ kỹ thuật số hoàn toàn tính toán tương đương với modulo, vì vậy nên được xem xét cùng với các phương pháp modulo khác ở đây, chẳng hạn như mod 16 và mod 255.
Christian Oudard

1
Bạn có chắc rằng root kỹ thuật số tương đương với modulo? Nó dường như là một cái gì đó hoàn toàn khác nhau như được giải thích bởi liên kết. Lưu ý danh sách là 1,4,7,9 chứ không phải 1,4,5,9.
Fractaly

1
Root kỹ thuật số trong hệ thập phân tương đương với việc sử dụng modulo 9 (well dr (n) = 1 + ((n-1) mod 9); do đó cũng có một sự thay đổi nhỏ). Các số 0,1,4,5,9 dành cho modulo 16 và 0, 1, 4, 7 dành cho modulo 9 - tương ứng với 1, 4, 7, 9 cho gốc kỹ thuật số.
Hans Olsson

16

Tôi muốn chức năng này hoạt động với tất cả các số nguyên có dấu 64 bit tích cực

Math.sqrt()hoạt động với gấp đôi làm tham số đầu vào, do đó bạn sẽ không nhận được kết quả chính xác cho số nguyên lớn hơn 2 ^ 53 .


5
Tôi thực sự đã kiểm tra câu trả lời trên tất cả các ô vuông hoàn hảo lớn hơn 2 ^ 53, cũng như tất cả các số từ 5 bên dưới mỗi ô vuông hoàn hảo đến 5 trên mỗi ô vuông hoàn hảo và tôi nhận được kết quả chính xác. (lỗi vòng được sửa khi tôi làm tròn câu trả lời sqrt thành dài, sau đó bình phương giá trị đó và so sánh)
Kip

2
@Kip: Tôi đoán tôi đã chứng minh rằng nó hoạt động .
maaartinus

Kết quả không hoàn toàn chính xác, nhưng chính xác hơn bạn nghĩ. Nếu chúng ta giả sử ít nhất 15 chữ số chính xác sau khi chuyển đổi thành gấp đôi và sau căn bậc hai thì đó là rất nhiều, vì chúng ta cần không quá 11: 10 chữ số cho căn bậc hai 32 bit và nhỏ hơn 1 cho một số thập phân, bởi vì các vòng +0,5 đến gần nhất.
mwfearnley

3
Math.sqrt () không hoàn toàn chính xác, nhưng nó không phải. Trong bài đầu tiên, tst là một số nguyên gần với sqrt (N). Nếu N không phải là một hình vuông, thì tst * tst! = N, bất kể giá trị của tst là gì. Nếu N là một hình vuông hoàn hảo, thì sqrt (N) <2 ^ 32 và miễn là sqrt (N) được tính toán với sai số <0,5, chúng tôi sẽ ổn.
gnasher729

13

Chỉ cần cho hồ sơ, một cách tiếp cận khác là sử dụng phân tách chính. Nếu mọi yếu tố của sự phân rã là chẵn thì số đó là một hình vuông hoàn hảo. Vì vậy, những gì bạn muốn là để xem nếu một số có thể được phân tách như là một sản phẩm của bình phương của số nguyên tố. Tất nhiên, bạn không cần phải có được sự phân tách như vậy, chỉ để xem liệu nó có tồn tại không.

Đầu tiên xây dựng một bảng bình phương các số nguyên tố thấp hơn 2 ^ 32. Giá trị này nhỏ hơn nhiều so với bảng của tất cả các số nguyên cho đến giới hạn này.

Một giải pháp sau đó sẽ như thế này:

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

Tôi đoán đó là một chút khó hiểu. Những gì nó làm là kiểm tra trong mỗi bước mà bình phương của một số nguyên tố chia số đầu vào. Nếu có thì nó chia số cho hình vuông miễn là có thể, để loại bỏ hình vuông này khỏi phân tách nguyên tố. Nếu theo quy trình này, chúng tôi đã đến 1, thì số đầu vào là một phép phân tích bình phương của các số nguyên tố. Nếu hình vuông trở nên lớn hơn chính số đó, thì không có cách nào hình vuông này, hoặc bất kỳ hình vuông nào lớn hơn, có thể chia nó, vì vậy số đó không thể là một phép phân tích bình phương của số nguyên tố.

Ngày nay, sqrt được thực hiện trong phần cứng và cần phải tính các số nguyên tố ở đây, tôi đoán giải pháp này chậm hơn nhiều. Nhưng nó sẽ cho kết quả tốt hơn giải pháp với sqrt sẽ không hoạt động trên 2 ^ 54, như mrzl đã trả lời.


1
phân chia số nguyên chậm hơn so với FP sqrt trên phần cứng hiện tại. Ý tưởng này không có cơ hội. >. <Ngay cả trong năm 2008, sqrtsdthông lượng của Core2 là một trên 6-58c. Nó idivlà một trên 12-36 xe máy. (độ trễ tương tự như thông lượng: không đơn vị nào là đường ống).
Peter Cordes

sqrt không cần phải hoàn toàn chính xác. Đó là lý do tại sao bạn kiểm tra bằng cách bình phương số nguyên kết quả và thực hiện so sánh số nguyên để quyết định xem số nguyên đầu vào có một số nguyên chính xác không.
Peter Cordes

11

Nó đã được chỉ ra rằng các dchữ số cuối cùng của một hình vuông hoàn hảo chỉ có thể có các giá trị nhất định. Các dchữ số cuối cùng (tính theo cơ sở b) của một số ngiống với phần còn lại khi nđược chia cho bd, nghĩa là. trong ký hiệu C n % pow(b, d).

Điều này có thể được khái quát cho bất kỳ mô-đun m, tức là. n % mcó thể được sử dụng để loại trừ một số phần trăm số là hình vuông hoàn hảo. Mô-đun bạn hiện đang sử dụng là 64, cho phép 12, tức là. 19% số dư, càng nhiều hình vuông càng tốt. Với một chút mã hóa, tôi đã tìm thấy mô-đun 110880, chỉ cho phép năm 2016, tức là. 1,8% còn lại là hình vuông có thể. Vì vậy, tùy thuộc vào chi phí của một hoạt động mô đun (ví dụ: phân chia) và tra cứu bảng so với căn bậc hai trên máy của bạn, sử dụng mô đun này có thể nhanh hơn.

Nhân tiện, nếu Java có cách lưu trữ một mảng bit được đóng gói cho bảng tra cứu, thì đừng sử dụng nó. 110880 từ 32 bit không có nhiều RAM trong những ngày này và việc tìm nạp một từ máy sẽ nhanh hơn so với tìm nạp một bit.


Đẹp. Bạn đã làm việc này ra đại số hay bằng cách thử và sai? Tôi có thể thấy lý do tại sao nó hiệu quả đến vậy - rất nhiều va chạm giữa các ô vuông hoàn hảo, ví dụ: 333 ^ 2% 110880 == 3 ^ 2, 334 ^ 2% 110880 == 26 ^ 2, 338 ^ 2% 110880 == 58 ^ 2 .. .
Finnw

IIRC đó là lực lượng vũ phu, nhưng lưu ý rằng 110880 = 2 ^ 5 * 3 ^ 2 * 5 * 7 * 11, cung cấp 6 * 3 * 2 * 2 * 2 - 1 = 143 ước số thích hợp.
Hugh Allen

Tôi thấy rằng vì những hạn chế của việc tra cứu, 44352 hoạt động tốt hơn, với tỷ lệ vượt qua 2,6%. Ít nhất là trong việc thực hiện của tôi.
Fractaly

1
Phân chia số nguyên ( idiv) bằng hoặc tệ hơn về chi phí so với FP sqrt ( sqrtsd) trên phần cứng x86 hiện tại. Ngoài ra, hoàn toàn không đồng ý với việc tránh bitfield. Tốc độ truy cập bộ đệm sẽ tốt hơn rất nhiều với bitfield và kiểm tra bit trong bitfield chỉ là một hoặc hai hướng dẫn đơn giản hơn so với kiểm tra toàn bộ byte. (Đối với các bảng nhỏ phù hợp với bộ đệm ngay cả khi không phải là bitfield, một mảng byte sẽ là tốt nhất, không phải là 32 bit int x86 có quyền truy cập một byte với tốc độ tương đương với từ khóa 32 bit.)
Peter Cordes

11

Một vấn đề số nguyên xứng đáng là một giải pháp số nguyên. Như vậy

Thực hiện tìm kiếm nhị phân trên các số nguyên (không âm) để tìm số nguyên t lớn nhất sao cho t**2 <= n. Sau đó kiểm tra xem r**2 = nchính xác. Điều này cần có thời gian O (log n).

Nếu bạn không biết cách tìm kiếm nhị phân các số nguyên dương vì tập hợp này không bị ràng buộc, thật dễ dàng. Bạn bắt đầu bằng cách tính toán hàm tăng f của mình (ở trên f(t) = t**2 - n) dựa trên quyền hạn của hai. Khi bạn thấy nó chuyển biến tích cực, bạn đã tìm thấy một giới hạn trên. Sau đó, bạn có thể làm tìm kiếm nhị phân tiêu chuẩn.


Trên thực tế, thời gian sẽ ít nhất là O((log n)^2)vì phép nhân không phải là thời gian không đổi nhưng thực tế có giới hạn thấp hơn O(log n), điều này trở nên rõ ràng khi làm việc với các số đa chính xác lớn. Nhưng phạm vi của wiki này dường như là 64 bit, vì vậy có lẽ đó là nbd.

10

Việc đơn giản hóa giải pháp maaartinus sau đây dường như giúp loại bỏ một vài điểm phần trăm khỏi thời gian chạy, nhưng tôi không đủ giỏi trong việc đo điểm chuẩn để tạo ra một điểm chuẩn mà tôi có thể tin tưởng:

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

Sẽ đáng để kiểm tra cách bỏ qua bài kiểm tra đầu tiên,

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

sẽ ảnh hưởng đến hiệu suất.


2
Kết quả là đây . Loại bỏ thử nghiệm đầu tiên là xấu vì nó giải quyết hầu hết các trường hợp khá rẻ. Nguồn là trong câu trả lời của tôi (cập nhật).
maaartinus

9

Để thực hiện, bạn rất thường xuyên phải thực hiện một số thỏa hiệp. Tuy nhiên, các phương pháp khác đã thể hiện nhiều phương thức khác nhau, tuy nhiên, bạn lưu ý rằng hack của Carmack nhanh hơn với các giá trị nhất định của N. Sau đó, bạn nên kiểm tra "n" và nếu nó nhỏ hơn số N, hãy sử dụng hack của Carmack, nếu không, hãy sử dụng một số phương pháp khác được mô tả trong các câu trả lời ở đây.


Tôi cũng đã kết hợp đề xuất của bạn vào giải pháp. Ngoài ra, xử lý tốt đẹp. :)
Kip

8

Đây là triển khai Java nhanh nhất tôi có thể nghĩ ra, sử dụng kết hợp các kỹ thuật được đề xuất bởi những người khác trong luồng này.

  • Kiểm tra Mod-256
  • Thử nghiệm mod-3465 không chính xác (tránh phân chia số nguyên với chi phí của một số dương tính giả)
  • Căn bậc hai dấu phẩy động, làm tròn và so sánh với giá trị đầu vào

Tôi cũng đã thử nghiệm những sửa đổi này nhưng chúng không giúp hiệu suất:

  • Kiểm tra mod-255 bổ sung
  • Chia giá trị đầu vào theo lũy thừa 4
  • Fast Inverse Square Root (để hoạt động với giá trị cao của N, nó cần 3 lần lặp, đủ để làm cho nó chậm hơn hàm căn bậc hai phần cứng.)

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

Bạn nên loại bỏ phần 2 năng lượng của N ngay từ đầu.

Chỉnh sửa lần 2 Biểu thức ma thuật cho m dưới đây nên là

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

và không được viết

Kết thúc chỉnh sửa lần 2

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

Chỉnh sửa lần 1:

Cải thiện nhỏ:

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

Kết thúc chỉnh sửa lần 1

Bây giờ tiếp tục như bình thường. Bằng cách này, vào thời điểm bạn đến phần dấu phẩy động, bạn đã loại bỏ tất cả các số có phần 2 lũy thừa là số lẻ (khoảng một nửa), và sau đó bạn chỉ xem xét 1/8 số còn lại. Tức là bạn chạy phần dấu phẩy động trên 6% số.


7

Project Euler được đề cập trong các thẻ và nhiều vấn đề trong đó yêu cầu kiểm tra số >> 2^64. Hầu hết các tối ưu hóa được đề cập ở trên không hoạt động dễ dàng khi bạn đang làm việc với bộ đệm 80 byte.

Tôi đã sử dụng java BigInteger và một phiên bản sửa đổi một chút của phương pháp Newton, một phiên bản hoạt động tốt hơn với các số nguyên. Vấn đề là các ô vuông chính xác được n^2hội tụ (n-1)thay nvì bởi vì n^2-1 = (n-1)(n+1)và lỗi cuối cùng chỉ là một bước dưới ước số cuối cùng và thuật toán chấm dứt. Thật dễ dàng để sửa bằng cách thêm một vào đối số ban đầu trước khi tính toán lỗi. (Thêm hai cho khối lập phương, vv)

Một thuộc tính hay của thuật toán này là bạn có thể biết ngay số đó có phải là một hình vuông hoàn hảo hay không - lỗi cuối cùng (không phải hiệu chỉnh) trong phương pháp của Newton sẽ bằng không. Một sửa đổi đơn giản cũng cho phép bạn nhanh chóng tính toán floor(sqrt(x))thay vì số nguyên gần nhất. Điều này rất hữu ích với một số vấn đề Euler.


1
Tôi đã suy nghĩ điều tương tự về các thuật toán này không chuyển dịch tốt sang bộ đệm đa độ chính xác. Vì vậy, tôi nghĩ rằng tôi sẽ thực hiện điều này ở đây ... Tôi thực sự đã tìm thấy một bài kiểm tra bình phương xác suất với độ phức tạp tiệm cận tốt hơn cho số lượng lớn ..... trong đó các ứng dụng lý thuyết số không tự tìm thấy. Không quen thuộc với Project Euler mặc dù ... có vẻ thú vị.

6

Đây là một bản làm lại từ thập phân đến nhị phân của thuật toán máy tính Marchant cũ (xin lỗi, tôi không có tài liệu tham khảo), trong Ruby, được điều chỉnh cụ thể cho câu hỏi này:

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

Đây là một công việc tương tự (xin đừng bỏ phiếu cho tôi về phong cách mã hóa / mùi hoặc O / O lộn xộn - đó là thuật toán được tính và C ++ không phải là ngôn ngữ nhà của tôi). Trong trường hợp này, chúng tôi đang tìm kiếm dư lượng == 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;
};

Số lần lặp trông O (ln n), trong đó n là độ dài bit của v, vì vậy tôi nghi ngờ điều này sẽ tiết kiệm nhiều cho v. Dấu phẩy động nổi chậm, có thể là 100-200 chu kỳ, nhưng toán học số nguyên thì không miễn phí Một chục lần lặp với 15 chu kỳ mỗi lần, và đó sẽ là một lần rửa. Tuy nhiên, +1 vì thú vị.
Tadmas

Trên thực tế, tôi tin rằng các phép cộng và phép trừ có thể được thực hiện bởi XOR.
Brent.Longborough

Đó là một nhận xét daft - chỉ có thể thực hiện việc bổ sung bằng XOR; phép trừ là số học.
Brent.Longborough

1
Có thực sự có sự khác biệt đáng kể nào giữa thời gian chạy XOR và bổ sung không?
Tadmas

1
@Tadmas: có lẽ không đủ để phá vỡ quy tắc "tối ưu hóa sau". (:-)
Brent.Longborough

6

Cuộc gọi sqrt không hoàn toàn chính xác, như đã được đề cập, nhưng thật thú vị và mang tính hướng dẫn rằng nó không thổi bay các câu trả lời khác về tốc độ. Rốt cuộc, chuỗi các hướng dẫn ngôn ngữ lắp ráp cho một sqrt là rất nhỏ. Tôi tin rằng Intel có một hướng dẫn về phần cứng, không được Java sử dụng vì nó không phù hợp với IEEE.

Vậy tại sao nó chậm? Bởi vì Java thực sự đang gọi một thói quen C thông qua JNI và thực tế làm điều đó chậm hơn so với việc gọi một chương trình con Java, bản thân nó chậm hơn so với thực hiện nội tuyến. Điều này rất khó chịu và Java nên đưa ra một giải pháp tốt hơn, tức là xây dựng các cuộc gọi thư viện dấu phẩy động nếu cần thiết. Ồ tốt

Trong C ++, tôi nghi ngờ tất cả các lựa chọn thay thế phức tạp sẽ mất tốc độ, nhưng tôi đã không kiểm tra tất cả. Những gì tôi đã làm và những gì người Java sẽ thấy hữu ích, là một cách hack đơn giản, một phần mở rộng của thử nghiệm trường hợp đặc biệt được đề xuất bởi A. Rex. Sử dụng một giá trị dài duy nhất dưới dạng một mảng bit, không được kiểm tra giới hạn. Bằng cách đó, bạn có tra cứu boolean 64 bit.

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

Thói quen isPerinfSapes5 chạy trong khoảng 1/3 thời gian trên máy bộ đôi core2 của tôi. Tôi nghi ngờ rằng các chỉnh sửa tiếp theo trên cùng một dòng có thể giảm thời gian trung bình hơn nữa, nhưng mỗi khi bạn kiểm tra, bạn đang giao dịch thử nghiệm nhiều hơn để loại bỏ nhiều hơn, vì vậy bạn không thể đi quá xa trên con đường đó.

Chắc chắn, thay vì có một thử nghiệm riêng cho âm tính, bạn có thể kiểm tra 6 bit cao theo cùng một cách.

Lưu ý rằng tất cả những gì tôi đang làm là loại bỏ các ô vuông có thể, nhưng khi tôi gặp trường hợp tiềm năng, tôi phải gọi bản gốc là isPerinfSapes.

Thường trình init2 được gọi một lần để khởi tạo các giá trị tĩnh của pp1 và pp2. Lưu ý rằng trong quá trình triển khai C ++ của tôi, tôi sử dụng dài không dấu, vì vậy kể từ khi bạn đăng nhập, bạn phải sử dụng toán tử >>>.

Không có nhu cầu nội tại để kiểm tra mảng, nhưng trình tối ưu hóa của Java phải tìm ra công cụ này khá nhanh, vì vậy tôi không đổ lỗi cho chúng vì điều đó.


3
Tôi cá là bạn đã sai hai lần. 1. Intel sqrt phù hợp với IEEE. Các hướng dẫn không tuân thủ duy nhất là các hướng dẫn đo lường cho các đối số lange. 2. Java sử dụng nội tại cho Math.sqrt, không có JNI .
maaartinus

1
Bạn không quên sử dụng pp2? Tôi hiểu rằng nó pp1được sử dụng để kiểm tra sáu bit có ý nghĩa nhỏ nhất, nhưng tôi không tin rằng việc kiểm tra sáu bit tiếp theo có ý nghĩa gì.
maaartinus

6

Tôi thích ý tưởng sử dụng một phương pháp gần như đúng trên một số đầu vào. Đây là một phiên bản có "bù" cao hơn. Mã dường như hoạt động và vượt qua trường hợp thử nghiệm đơn giản của tôi.

Chỉ cần thay thế của bạn:

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

mã với cái này:

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

Xem xét về độ dài bit chung (mặc dù tôi đã sử dụng loại cụ thể ở đây), tôi đã cố gắng thiết kế thuật toán đơn giản như dưới đây. Kiểm tra đơn giản và rõ ràng cho 0,1,2 hoặc <0 là bắt buộc ban đầu. Theo sau là đơn giản theo nghĩa là nó không cố gắng sử dụng bất kỳ hàm toán học hiện có nào. Hầu hết các toán tử có thể được thay thế bằng các toán tử bit-khôn ngoan. Tôi đã không thử nghiệm với bất kỳ dữ liệu đánh dấu băng ghế mặc dù. Tôi không phải là chuyên gia về toán học hay thiết kế thuật toán máy tính nói riêng, tôi rất thích thấy bạn chỉ ra vấn đề. Tôi biết có rất nhiều cơ hội cải thiện ở đó.

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: Một số vấn đề với trình duyệt của tôi.
nabam serbang

1
Bạn cần một số thụt lề.
Steve Kuo

5

Tôi đã kiểm tra tất cả các kết quả có thể khi n bit cuối cùng của một hình vuông được quan sát. Bằng cách kiểm tra liên tiếp nhiều bit hơn, có thể loại bỏ tối đa 5/6 đầu vào. Tôi thực sự đã thiết kế nó để thực hiện thuật toán Factorization của Fermat và nó rất nhanh ở đó.

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

Bit cuối cùng của mã giả có thể được sử dụng để mở rộng các thử nghiệm để loại bỏ nhiều giá trị hơn. Các bài kiểm tra trên dành cho k = 0, 1, 2, 3

  • a có dạng (3 << 2k) - 1
  • b có dạng (2 << 2k)
  • c có dạng (2 << 2k + 2) - 1
  • d có dạng (2 << 2k - 1) * 10

    Đầu tiên, nó kiểm tra xem nó có phần dư vuông với mô đun công suất bằng hai hay không, sau đó nó kiểm tra dựa trên mô đun cuối cùng, sau đó nó sử dụng Math.sqrt để thực hiện bài kiểm tra cuối cùng. Tôi đã đưa ra ý tưởng từ bài viết hàng đầu, và cố gắng mở rộng nó. Tôi đánh giá cao bất kỳ ý kiến ​​hoặc đề xuất.

    Cập nhật: Sử dụng thử nghiệm theo mô-đun, (modSq) và cơ sở mô-đun 44352, thử nghiệm của tôi chạy trong 96% thời gian của thử nghiệm trong bản cập nhật của OP cho số lượng lên tới 1.000.000.000.


  • 2

    Đây là một giải pháp phân chia và chinh phục.

    Nếu căn bậc hai của số tự nhiên ( number) là số tự nhiên ( solution), bạn có thể dễ dàng xác định phạm vi solutiondựa trên số chữ số của number:

    • numbercó 1 chữ số: solutiontrong phạm vi = 1 - 4
    • numbercó 2 chữ số: solutiontrong phạm vi = 3 - 10
    • numbercó 3 chữ số: solutiontrong phạm vi = 10 - 40
    • numbercó 4 chữ số: solutiontrong phạm vi = 30 - 100
    • number có 5 chữ số: solution trong phạm vi = 100 - 400

    Thông báo sự lặp lại?

    Bạn có thể sử dụng phạm vi này trong cách tiếp cận tìm kiếm nhị phân để xem liệu có một solutionmục nào:

    number == solution * solution

    Đây là mã

    Đây là lớp SquareRootChecker của tôi

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

    Và đây là một ví dụ về cách sử dụng nó.

    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
    Tôi thích khái niệm này, nhưng tôi muốn chỉ ra một cách lịch sự một lỗ hổng lớn: các số nằm trong cơ sở nhị phân 2. Chuyển đổi cơ sở 2 sang cơ sở 10 thông qua toStringlà một hoạt động cực kỳ tốn kém so với các nhà khai thác bitwise. Do đó, để thỏa mãn mục tiêu của câu hỏi - hiệu suất - bạn phải sử dụng các toán tử bit thay vì chuỗi 10 cơ sở. Một lần nữa, tôi thực sự thích khái niệm của bạn. Mặc dù vậy, việc triển khai của bạn (như hiện tại) là chậm nhất trong số các giải pháp có thể được đăng cho câu hỏi.
    Jack Giffin

    1

    Nếu tốc độ là một mối quan tâm, tại sao không phân vùng tập hợp đầu vào được sử dụng phổ biến nhất và các giá trị của chúng vào bảng tra cứu và sau đó thực hiện bất kỳ thuật toán ma thuật tối ưu hóa nào bạn đã đưa ra cho các trường hợp đặc biệt?


    Vấn đề là không có "bộ đầu vào thường được sử dụng" - thông thường tôi đang lặp qua một danh sách, vì vậy tôi sẽ không sử dụng cùng một đầu vào hai lần.
    Kip

    1

    Nên có thể đóng gói 'không thể là một hình vuông hoàn hảo nếu các chữ số X cuối cùng là N' hiệu quả hơn thế nhiều! Tôi sẽ sử dụng java 32 bit int và tạo ra đủ dữ liệu để kiểm tra 16 bit cuối của số - đó là giá trị int thập lục phân 2048.

    ...

    Đồng ý. Hoặc là tôi đã gặp phải một số lý thuyết số vượt xa tôi hoặc có một lỗi trong mã của tôi. Trong mọi trường hợp, đây là mã:

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

    và đây là kết quả:

    (ed: được hỗ trợ cho hiệu suất kém trong prettify.js; xem lịch sử sửa đổi để xem.)


    1

    Phương pháp của Newton với số học số nguyên

    Nếu bạn muốn tránh các hoạt động không nguyên, bạn có thể sử dụng phương pháp bên dưới. Về cơ bản, nó sử dụng Phương pháp của Newton được sửa đổi cho số học số nguyên.

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

    Việc thực hiện này không thể cạnh tranh với các giải pháp sử dụng Math.sqrt. Tuy nhiên, hiệu suất của nó có thể được cải thiện bằng cách sử dụng các cơ chế lọc được mô tả trong một số bài đăng khác.


    1

    Tính toán căn bậc hai theo phương pháp của Newton rất nhanh ... với điều kiện giá trị bắt đầu là hợp lý. Tuy nhiên, không có giá trị bắt đầu hợp lý và trong thực tế, chúng tôi kết thúc bằng hành vi chia đôi và ghi nhật ký (2 ^ 64).
    Để thực sự nhanh chóng, chúng ta cần một cách nhanh chóng để có được giá trị khởi đầu hợp lý, và điều đó có nghĩa là chúng ta cần phải đi vào ngôn ngữ máy. Nếu bộ xử lý cung cấp một lệnh như POPCNT trong Pentium, thì tính các số 0 đứng đầu, chúng ta có thể sử dụng số đó để có giá trị bắt đầu với một nửa số bit đáng kể. Với sự cẩn thận, chúng ta có thể tìm thấy một số bước cố định của Newton sẽ luôn luôn đủ. (Do đó, cần phải lặp lại và thực hiện rất nhanh.)

    Một giải pháp thứ hai là thông qua cơ sở dấu phẩy động, có thể có phép tính sqrt nhanh (như bộ đồng xử lý i87.) Ngay cả một chuyến tham quan qua exp () và log () có thể nhanh hơn Newton bị thoái hóa thành tìm kiếm nhị phân. Có một khía cạnh khó khăn cho vấn đề này, một phân tích phụ thuộc vào bộ xử lý về những gì và nếu tinh chỉnh sau đó là cần thiết.

    Một giải pháp thứ ba giải quyết một vấn đề hơi khác, nhưng cũng đáng được đề cập vì tình huống được mô tả trong câu hỏi. Nếu bạn muốn tính toán nhiều căn bậc hai cho các số khác nhau một chút, bạn có thể sử dụng phép lặp Newton, nếu bạn không bao giờ xác định lại giá trị bắt đầu, nhưng chỉ để nó ở nơi tính toán trước đó. Tôi đã sử dụng thành công trong ít nhất một vấn đề Euler.


    Ước tính tốt không quá khó. Bạn có thể sử dụng số chữ số của số để ước tính giới hạn dưới và trên cho giải pháp. Xem thêm câu trả lời của tôi, nơi tôi đề xuất một giải pháp phân chia và chinh phục.
    MWB

    Sự khác biệt giữa POPCNT và đếm số chữ số là gì? Ngoại trừ việc bạn có thể thực hiện POPCNT trong một nano giây.
    Albert van der Horst

    1

    Căn bậc hai của một số, cho rằng số đó là một hình vuông hoàn hảo.

    Sự phức tạp là 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

    Nếu bạn muốn tốc độ, do các số nguyên của bạn có kích thước hữu hạn, tôi nghi ngờ rằng cách nhanh nhất sẽ liên quan đến (a) phân vùng các tham số theo kích thước (ví dụ: thành các loại theo tập bit lớn nhất), sau đó kiểm tra giá trị theo một mảng các ô vuông hoàn hảo trong phạm vi đó.


    2
    Có 2 ^ 32 ô vuông hoàn hảo trong phạm vi dài. Bảng này sẽ rất lớn. Ngoài ra, lợi thế của việc tính toán giá trị so với truy cập bộ nhớ có thể rất lớn.
    Peter ALLenWebb

    Ồ không, không có, có 2 ^ 16. 2 ^ 32 là 2 ^ 16 bình phương. Có 2 ^ 16.
    Celestial M Weasel

    3
    có, nhưng phạm vi dài là 64 bit, không phải 32 bit. sqrt (2 ^ 64) = 2 ^ 32. (tôi đang bỏ qua bit dấu để làm cho phép toán dễ dàng hơn một chút ... thực sự có (dài) (2 ^ 31,5) = 3037000499 hình vuông hoàn hảo)
    Kip

    0

    Về phương pháp Carmac, có vẻ như sẽ khá dễ dàng chỉ cần lặp lại một lần nữa, điều này sẽ tăng gấp đôi số chữ số chính xác. Rốt cuộc, đó là một phương pháp lặp cực kỳ rút gọn - Newton, với dự đoán đầu tiên rất hay.

    Về điều tốt nhất hiện tại của bạn, tôi thấy hai tối ưu hóa vi mô:

    • di chuyển kiểm tra so với 0 sau khi kiểm tra bằng mod255
    • sắp xếp lại các quyền hạn phân chia của bốn để bỏ qua tất cả các kiểm tra cho trường hợp thông thường (75%).

    I E:

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

    Thậm chí tốt hơn có thể là một đơn giản

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

    Rõ ràng, sẽ rất thú vị khi biết có bao nhiêu con số bị loại bỏ ở mỗi điểm kiểm tra - tôi khá nghi ngờ việc kiểm tra là thực sự độc lập, điều này khiến mọi thứ trở nên khó khăn.

    Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
    Licensed under cc by-sa 3.0 with attribution required.