วิธีที่เร็วที่สุดในการตรวจสอบว่าสแควร์รูทของจำนวนเต็มเป็นจำนวนเต็มหรือไม่


1453

ฉันกำลังมองหาวิธีที่เร็วที่สุดในการพิจารณาว่าlongค่าเป็นจตุรัสที่สมบูรณ์แบบหรือไม่ (เช่นสแควร์รูทเป็นจำนวนเต็มอีกตัว):

  1. ฉันได้ทำมันเป็นวิธีที่ง่ายโดยใช้ในตัว Math.sqrt() ฟังก์ชั่นแต่ฉันสงสัยว่ามีวิธีที่จะทำได้เร็วขึ้นหรือไม่โดยการ จำกัด ตัวเองเป็นโดเมนจำนวนเต็มเท่านั้น
  2. การบำรุงรักษาตารางการค้นหาไม่สามารถใช้งานได้ (เนื่องจากมีจำนวนเต็มประมาณ31.5 2 ซึ่งมีค่าน้อยกว่า 2 63 )

นี่เป็นวิธีที่ง่ายและตรงไปตรงมาที่ฉันทำตอนนี้:

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

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

หมายเหตุ: ฉันกำลังใช้ฟังก์ชั่นนี้ในหลายปัญหาของProject Euler ดังนั้นไม่มีใครจะต้องรักษารหัสนี้ และการเพิ่มประสิทธิภาพขนาดเล็กแบบนี้สามารถสร้างความแตกต่างได้จริงเนื่องจากส่วนหนึ่งของความท้าทายคือการทำอัลกอริทึมทุกอย่างในเวลาน้อยกว่าหนึ่งนาทีและฟังก์ชั่นนี้จะต้องถูกเรียกว่าหลายล้านครั้งในปัญหาบางอย่าง


ฉันได้ลองวิธีแก้ไขปัญหาต่าง ๆ แล้ว:

  • หลังจากการทดสอบอย่างละเอียดถี่ถ้วนฉันพบว่า0.5ไม่จำเป็นต้องเพิ่มผลลัพธ์ของ Math.sqrt () อย่างน้อยก็ไม่ได้อยู่ในเครื่องของฉัน
  • แควร์รูทแบบเร็วนั้นเร็วขึ้น แต่ให้ผลลัพธ์ที่ไม่ถูกต้องสำหรับ n> = 410881 อย่างไรก็ตามตามที่แนะนำโดยBobbyShaftoeเราสามารถใช้ FISR Hack สำหรับ n <410881 ได้
  • วิธีการของนิวตันช้ากว่าMath.sqrt()เล็กน้อย อาจเป็นเพราะMath.sqrt()ใช้สิ่งที่คล้ายกับวิธีการของนิวตัน แต่ใช้งานในฮาร์ดแวร์จึงเร็วกว่าใน Java นอกจากนี้วิธีการของนิวตันยังคงต้องใช้คู่ผสม
  • แก้ไขวิธีการของนิวตันซึ่งใช้เทคนิคเล็กน้อยเพื่อให้เฉพาะจำนวนเต็มคณิตศาสตร์มีส่วนเกี่ยวข้องต้อง hacks บางอย่างเพื่อหลีกเลี่ยงการล้น (ฉันต้องการฟังก์ชั่นการทำงานที่มีจำนวนเต็มบวก 64 บิตลงนามทั้งหมด) Math.sqrt()และมันก็ยังคงช้ากว่า
  • ไบนารีสับก็ยิ่งช้า เรื่องนี้สมเหตุสมผลเพราะสับไบนารีจะต้องผ่าน 16 เพื่อหาสแควร์รูทของจำนวน 64 บิต
  • ตามที่การทดสอบของจอห์นใช้orงบได้เร็วขึ้นใน C ++ กว่าการใช้switchแต่ใน Java และ C # มีปรากฏเป็นความแตกต่างระหว่างไม่มีและorswitch
  • ฉันยังพยายามสร้างตารางการค้นหา (เป็นอาร์เรย์แบบสแตติกส่วนตัวที่มีค่าบูลีน 64 ค่า) จากนั้นแทนการอย่างใดอย่างหนึ่งหรือสวิทช์คำสั่งฉันเพียงแค่จะบอกว่าor if(lookup[(int)(n&0x3F)]) { test } else return false;สำหรับความประหลาดใจของฉันนี่คือ (เพียงเล็กน้อย) ช้าลง เพราะนี่คือขอบเขตอาร์เรย์จะถูกตรวจสอบในชวา

21
นี่คือรหัส Java โดยที่ int == 32 บิตและยาว == 64 บิตและทั้งสองลงนาม
กี

14
@Shreevasta: ฉันทำการทดสอบค่าขนาดใหญ่ (มากกว่า 2 ^ 53) และวิธีการของคุณให้ผลบวกที่ผิดพลาด สิ่งแรกที่พบคือสำหรับ n = 9007199326062755 ซึ่งไม่ใช่สี่เหลี่ยมจัตุรัสที่สมบูรณ์แบบ แต่ถูกส่งคืนเป็นหนึ่ง
Kip

37
โปรดอย่าเรียกมันว่า "John Carmack hack" เขาไม่ได้คิดขึ้นมา
user9282

84
@amama - บางที แต่เป็นเพราะเขา Henry Ford ไม่ได้ประดิษฐ์รถยนต์ไรท์ Bros. ไม่ประดิษฐ์เครื่องบินและและ Galleleo ไม่ใช่คนแรกที่คิดว่าโลกหมุนรอบดวงอาทิตย์ ... โลกนี้ประกอบด้วยสิ่งประดิษฐ์ที่ถูกขโมย (และ รัก).
Robert Fraser

4
คุณอาจได้รับความเร็วเพิ่มขึ้นเล็กน้อยใน 'quickfail' โดยใช้สิ่งที่ต้องการ((1<<(n&15))|65004) != 0แทนที่จะมีเช็คสามชุดแยกกัน
Nabb

คำตอบ:


735

ฉันหาวิธีที่ทำงานได้เร็วกว่า ~ 35% 6bits + Carmack + sqrt code ของคุณอย่างน้อยก็มี CPU (x86) และภาษาโปรแกรม (C / C ++) ของฉัน ผลลัพธ์ของคุณอาจแตกต่างกันไปโดยเฉพาะอย่างยิ่งเพราะฉันไม่ทราบว่าปัจจัย Java จะเล่นได้อย่างไร

แนวทางของฉันคือสามเท่า:

  1. ก่อนอื่นให้กรองคำตอบที่ชัดเจน ซึ่งรวมถึงตัวเลขติดลบและดู 4 บิตสุดท้าย (ฉันพบว่าการดูหกครั้งล่าสุดไม่ได้ช่วยด้วย) ฉันยังตอบใช่สำหรับ 0 (ในการอ่านรหัสด้านล่างโปรดทราบว่าการป้อนข้อมูลของฉันคือint64 x)
    if( x < 0 || (x&2) || ((x & 7) == 5) || ((x & 11) == 8) )
        return false;
    if( x == 0 )
        return true;
  2. ถัดไปตรวจสอบว่ามันเป็นรูปสี่เหลี่ยมจัตุรัส 255 = 3 * 5 * 17 เพราะนั่นคือผลคูณของสามช่วงเวลาที่เหลืออยู่เพียงประมาณ 1/8 ของส่วนที่เหลือ mod 255 นั้นคือกำลังสอง อย่างไรก็ตามจากประสบการณ์ของฉันการเรียกโมเดอเรเตอร์ของโมดูโล่ (%) มีค่าใช้จ่ายมากกว่าผลประโยชน์ที่ได้รับดังนั้นฉันจึงใช้บิตเทคนิคที่เกี่ยวข้องกับ 255 = 2 ^ 8-1 เพื่อคำนวณสิ่งตกค้าง (สำหรับดีขึ้นหรือแย่ลงฉันไม่ได้ใช้เคล็ดลับในการอ่านแต่ละไบต์จากคำเพียงบิตและ - และกะ)
    int64 y = x;
    y = (y & 4294967295LL) + (y >> 32); 
    y = (y & 65535) + (y >> 16);
    y = (y & 255) + ((y >> 8) & 255) + (y >> 16);
    // At this point, y is between 0 and 511.  More code can reduce it farther.
    หากต้องการตรวจสอบว่าสารตกค้างเป็นรูปสี่เหลี่ยมจริงหรือไม่ฉันค้นหาคำตอบในตารางที่คำนวณล่วงหน้าแล้ว
    if( bad255[y] )
        return false;
    // However, I just use a table of size 512
  3. สุดท้ายให้ลองคำนวณรากที่สองโดยใช้วิธีการที่คล้ายกัน แทรก Hensel ของ (ฉันไม่คิดว่ามันใช้ได้โดยตรง แต่ใช้ได้กับการดัดแปลงบางอย่าง) ก่อนที่จะทำเช่นนั้นฉันแบ่งพลังทั้งหมดของ 2 ด้วยการค้นหาไบนารี
    if((x & 4294967295LL) == 0)
        x >>= 32;
    if((x & 65535) == 0)
        x >>= 16;
    if((x & 255) == 0)
        x >>= 8;
    if((x & 15) == 0)
        x >>= 4;
    if((x & 3) == 0)
        x >>= 2;
    ณ จุดนี้เพื่อให้หมายเลขของเราเป็นจตุรัสจะต้องเป็น 1 mod 8
    if((x & 7) != 1)
        return false;
    โครงสร้างพื้นฐานของบทแทรกของ Hensel มีดังต่อไปนี้ (หมายเหตุ: รหัสที่ยังไม่ทดลองถ้าไม่ได้ผลให้ลอง t = 2 หรือ 8)
    int64 t = 4, r = 1;
    t <<= 1; r += ((x - r * r) & t) >> 1;
    t <<= 1; r += ((x - r * r) & t) >> 1;
    t <<= 1; r += ((x - r * r) & t) >> 1;
    // Repeat until t is 2^33 or so.  Use a loop if you want.
    แนวคิดก็คือในการวนซ้ำแต่ละครั้งคุณเพิ่มหนึ่งบิตลงบน r รากที่สองของ "ปัจจุบัน" ของ x; รากที่สองแต่ละตัวเป็นโมดูโลที่แม่นยำพลังงานที่มากขึ้นและใหญ่ขึ้นของ 2 คือ t / 2 ในตอนท้าย r และ t / 2-r จะเป็นสแควร์รูทของ x modulo t / 2 (โปรดสังเกตว่าถ้า r เป็นสแควร์รูทของ x ดังนั้นก็คือ -r นี่เป็นความจริงแม้ตัวเลขโมดูโล แต่ระวังโมดูโลบางจำนวน ) เนื่องจากสแควร์รูทจริงของเราน้อยกว่า 2 ^ 32 ณ จุดนั้นเราสามารถตรวจสอบได้ว่า r หรือ t / 2-r เป็นรากที่สองจริง ในรหัสจริงของฉันฉันใช้วนรอบที่ปรับเปลี่ยนต่อไปนี้:
    int64 r, t, z;
    r = start[(x >> 3) & 1023];
    do {
        z = x - r * r;
        if( z == 0 )
            return true;
        if( z < 0 )
            return false;
        t = z & (-z);
        r += (z & t) >> 1;
        if( r > (t >> 1) )
            r = t - r;
    } while( t <= (1LL << 33) );
    การเร่งความเร็วที่นี่สามารถทำได้สามวิธี: ค่าเริ่มต้นที่คำนวณล่วงหน้าแล้ว (เท่ากับ ~ 10 การวนซ้ำของการวนซ้ำ), การออกก่อนหน้าของลูปและการข้ามค่า t บางค่า สำหรับส่วนสุดท้ายฉันดูz = r - x * xและตั้งค่า t ให้เป็นพลังที่ใหญ่ที่สุดของ 2 หาร z ด้วยเคล็ดลับเล็กน้อย สิ่งนี้ทำให้ฉันสามารถข้ามค่า t ที่ไม่ส่งผลกระทบต่อค่า r ได้ ค่าเริ่มต้นที่คำนวณล่วงหน้าในกรณีของฉันเลือกโมดัลรูต "บวกที่เล็กที่สุด" สแควร์รูท 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 และทำการเปรียบเทียบรวมถึงตรวจสอบความถูกต้องของผลลัพธ์ ฉันจะให้คุณรู้ว่าสิ่งที่ฉันพบ
กีบ

79
ว้าวนี่สวยงาม ฉันเคยเห็น Hensel ยกมาก่อน (การคำนวณรากของ polynomials modulo a prime) แต่ฉันไม่เคยรู้เลยว่าบทแทรกสามารถลดลงอย่างระมัดระวังตลอดทางสำหรับการคำนวณรากที่สองของตัวเลข นี้คือ ... สูง :)
ShreevatsaR

3
@ nightcracker มันไม่ได้ 9 < 0 => false, 9&2 => 0, ,9&7 == 5 => false 9&11 == 8 => false
โม่

53
Maartinus โพสต์โซลูชั่นที่เร็วกว่า 2x (และสั้นกว่ามาก) ด้านล่างอีกเล็กน้อยในภายหลังซึ่งดูเหมือนจะไม่ได้รับความรักมากนัก
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% ที่นี่

การทดสอบครั้งที่สองจับตัวเลขทั้งหมดที่มีเลขคี่สองตัวในการแยกตัวประกอบ วิธีการLong.numberOfTrailingZerosนี้เร็วมากเมื่อได้รับ JIT-ed เป็นคำสั่ง i86 เดี่ยว

หลังจากปล่อยเลขศูนย์ต่อท้ายการทดสอบครั้งที่สามจะจัดการกับตัวเลขที่ลงท้ายด้วย 011, 101 หรือ 111 ในรูปแบบไบนารีซึ่งไม่ใช่รูปสี่เหลี่ยมที่สมบูรณ์แบบ นอกจากนี้ยังใส่ใจเกี่ยวกับจำนวนลบและจัดการ 0

การทดสอบขั้นสุดท้ายกลับไปเป็นdoubleเลขคณิต เนื่องจากdoubleมี mantissa เพียง 53 บิตการแปลงจากlongเป็นdoubleรวมการปัดเศษสำหรับค่าขนาดใหญ่ อย่างไรก็ตามการทดสอบนั้นถูกต้อง (เว้นแต่จะพิสูจน์ได้ว่าผิด)

พยายามรวมแนวคิด mod255 ไม่สำเร็จ


3
การปิดบังโดยนัยของค่าการเปลี่ยนแปลงนั้นค่อนข้างเลวร้าย คุณมีความคิดใด ๆ ว่าทำไมถึงอยู่ในสเป็คของ Java?
dfeuer

6
@ pdfeuer ฉันคิดว่ามีสองเหตุผล: 1. การขยับโดยไม่มีเหตุผลเพิ่มเติม 2. มันก็เหมือนกับการทำงานของ HW และทุกคนที่ใช้การดำเนินการระดับบิตที่สนใจในการปฏิบัติงานดังนั้นการทำสิ่งอื่นจะผิด -การgoodMaskทดสอบทำ แต่จะทำก่อนการเปลี่ยนที่ถูกต้อง ดังนั้นคุณต้องทำซ้ำ แต่วิธีนี้ง่ายกว่าและ AFAIK จะเร็วขึ้นเล็กน้อยและดีเท่า ๆ กัน
maaartinus

3
@dfeuer สำหรับมาตรฐานเป็นสิ่งสำคัญที่จะต้องให้คำตอบโดยเร็วและจำนวนศูนย์ต่อท้ายตัวเองไม่ให้คำตอบ มันเป็นเพียงขั้นตอนเตรียมการ i86 / amd64 ทำได้ ไม่มีความคิดเกี่ยวกับซีพียูขนาดเล็กในโทรศัพท์มือถือ แต่ที่แย่ที่สุดคือ Java ต้องสร้างคำสั่งและสำหรับพวกเขาซึ่งแน่นอนง่ายกว่าวิธีอื่น ๆ
maaartinus

2
@Sebastian if ((x & (7 | Integer.MIN_VALUE)) != 1) return x == 0;ทดสอบอาจจะดีกว่า:
maaartinus

4
"เนื่องจาก double มีเพียง 56 bits mantissa" -> ฉันจะบอกว่ามันน่าจะมี53 บิตมากกว่าหนึ่ง นอกจากนี้
chux - Reinstate Monica

132

คุณจะต้องทำการเปรียบเทียบ อัลกอริทึมที่ดีที่สุดจะขึ้นอยู่กับการกระจายอินพุตของคุณ

อัลกอริทึมของคุณอาจจะเกือบจะดีที่สุด แต่คุณอาจต้องการตรวจสอบอย่างรวดเร็วเพื่อแยกแยะความเป็นไปได้ก่อนที่จะเรียกรูทีนรูทของคุณ ตัวอย่างเช่นดูตัวเลขสุดท้ายของตัวเลขของคุณเป็นเลขฐานสิบหกด้วยการทำ bit-wise "และ" สี่เหลี่ยมจัตุรัสที่สมบูรณ์แบบสามารถลงท้ายด้วย 0, 1, 4 หรือ 9 ในฐาน 16 ดังนั้นสำหรับ 75% ของอินพุตของคุณ (สมมติว่าพวกมันกระจายแบบสม่ำเสมอ) คุณสามารถหลีกเลี่ยงการเรียกรูทสแควร์เพื่อแลกกับการบิดนิด ๆ หน่อย ๆ

Kip benchmarked รหัสต่อไปนี้การใช้เคล็ดลับ hex เมื่อทำการทดสอบหมายเลข 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 เคล็ดลับ hex ทำให้รหัสสองครั้งเร็วขึ้นอีกครั้ง

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 #


มันช่างฉลาดเหลือเกิน ... คงไม่คิดอย่างนั้น
warren

จุดดีเกี่ยวกับบิตต่อท้าย ฉันจะพยายามรวมการทดสอบนั้นกับข้อสังเกตอื่น ๆ ที่นี่
PeterAllenWebb

3
สุดยอดทางออก สงสัยว่าคุณมากับมันได้อย่างไร เป็นหลักการที่จัดตั้งขึ้นอย่างเป็นธรรมหรือเป็นเพียงสิ่งที่คุณคิดออกมา? : D
Jeel Shah

3
@ LarsH ไม่จำเป็นต้องเพิ่ม 0.5 ดูโซลูชันของฉันสำหรับลิงก์ไปยังหลักฐาน
maaartinus

2
@JerryGoyal มันขึ้นอยู่กับคอมไพเลอร์และค่าของเคส ในคอมไพเลอร์ที่สมบูรณ์แบบสวิตช์จะเร็วอย่างน้อยเท่ากับ if-else แต่คอมไพเลอร์ไม่สมบูรณ์แบบดังนั้นจึงเป็นการดีที่สุดที่จะลองใช้เช่นเดียวกับจอห์น
fishinear

52

ฉันกำลังคิดเกี่ยวกับเวลาที่น่ากลัวที่ฉันใช้ในหลักสูตรการวิเคราะห์เชิงตัวเลข

และจากนั้นฉันก็จำได้ว่ามีฟังก์ชั่นนี้วนรอบ 'เน็ตจากรหัสที่มาของ 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;
}

ซึ่งโดยทั่วไปจะคำนวณสแควร์รูทโดยใช้ฟังก์ชันการประมาณของนิวตัน (จำชื่อไม่ได้)

มันควรจะใช้งานได้และอาจจะเร็วกว่านั้นมันมาจากเกม id ซอฟต์แวร์มหัศจรรย์!

มันเขียนด้วยภาษา C ++ แต่ไม่ควรยากเกินกว่าจะใช้เทคนิคเดียวกันใน Java เมื่อคุณได้รับแนวคิด:

ฉันพบมันในตอนแรก: http://www.codemaestro.com/reviews/9

วิธีการของนิวตันอธิบายที่วิกิพีเดีย: http://en.wikipedia.org/wiki/Newton%27s_method

คุณสามารถไปที่ลิงก์เพื่อดูคำอธิบายเพิ่มเติมเกี่ยวกับวิธีการใช้งาน แต่ถ้าคุณไม่สนใจมากสิ่งนี้เป็นสิ่งที่ฉันจำได้จากการอ่านบล็อกและจากการเรียนรู้การวิเคราะห์เชิงตัวเลข:

  • * (long*) &yเป็นพื้นอย่างรวดเร็วแปลงไปนานฟังก์ชั่นเพื่อให้การดำเนินงานจำนวนเต็มสามารถนำมาใช้ในไบต์ดิบ
  • 0x5f3759df - (i >> 1);บรรทัดเป็นค่าเมล็ดพันธุ์ก่อนการคำนวณสำหรับฟังก์ชั่นการประมาณ
  • การ* (float*) &iแปลงค่ากลับเป็นทศนิยม
  • y = y * ( threehalfs - ( x2 * y * y ) )บรรทัด bascially iterates ค่ามากกว่าฟังก์ชั่นอีกครั้ง

ฟังก์ชั่นการประมาณให้ค่าที่แม่นยำมากขึ้นยิ่งคุณทำซ้ำฟังก์ชันมากกว่าผลลัพธ์ ในกรณีของ Quake การวนซ้ำหนึ่งครั้งคือ "ดีพอ" แต่ถ้าไม่ใช่สำหรับคุณ ... จากนั้นคุณสามารถเพิ่มการวนซ้ำได้มากเท่าที่คุณต้องการ

สิ่งนี้ควรจะเร็วกว่าเพราะจะช่วยลดจำนวนการดำเนินการหารในการลบสแควร์ที่ไร้เดียงสาให้แบ่งง่าย ๆ ด้วย 2 (อันที่จริงเป็นการ* 0.5Fดำเนินการคูณ) และแทนที่ด้วยจำนวนการดำเนินการคูณจำนวนคงที่แทน


9
ควรสังเกตว่าสิ่งนี้คืนค่า 1 / sqrt (number) ไม่ใช่ sqrt (number) ฉันได้ทำการทดสอบแล้วและสิ่งนี้ล้มเหลวโดยเริ่มต้นที่ n = 410881: สูตรเวทย์มนตร์ของ John Carmack ส่งคืน 642.00104 เมื่อรากที่สองที่แท้จริงคือ 641
Kip

11
คุณสามารถดูกระดาษของ Chris Lomonts บนสแควร์รูทแบบผกผันได้อย่างรวดเร็ว: lomont.org/Math/Papers/2003/InvSqrt.pdfมันใช้เทคนิคเดียวกับที่นี่ แต่มีหมายเลขเวทย์มนตร์แตกต่างกัน กระดาษอธิบายว่าทำไมเลือกหมายเลขเวทมนตร์

4
นอกจากนี้Beyond3d.com/content/articles/8และBeyond3d.com/content/articles/15ฉายแสงบางส่วนเกี่ยวกับต้นกำเนิดของวิธีนี้ มันมักจะมาจาก John Carmack แต่ดูเหมือนว่ารหัสดั้งเดิม (อาจ) เขียนโดย Gary Tarolli, Greg Walsh และคนอื่น ๆ

3
นอกจากนี้คุณไม่สามารถพิมพ์ลอยและ ints ใน Java
พลวง

10
@Antimony ใครบอกว่า? FloatToIntBitsและIntToFloatBitsมีมาตั้งแต่จาวา 1.0.2
corsiKa

38

ฉันไม่แน่ใจว่ามันจะเร็วกว่าหรือแม่นยำกว่านี้ แต่คุณสามารถใช้Magical Square Root ของ John Carmackอัลกอริทึมเพื่อแก้ปัญหารากที่สองได้เร็วขึ้น คุณอาจทดสอบสิ่งนี้ได้อย่างง่ายดายสำหรับจำนวนเต็ม 32 บิตที่เป็นไปได้ทั้งหมดและตรวจสอบว่าคุณได้รับผลลัพธ์ที่ถูกต้องจริง ๆ เพราะเป็นเพียงการทดสอบเท่านั้น อย่างไรก็ตามตอนนี้ที่ฉันคิดเกี่ยวกับมันการใช้ doubles ก็ใกล้เคียงกันดังนั้นฉันจึงไม่แน่ใจว่าสิ่งนั้นจะเกิดขึ้นได้อย่างไร


10
ฉันเชื่อว่าเคล็ดลับของ Carmack นั้นไร้ประโยชน์ในทุกวันนี้ คำสั่ง sqrt ในตัวนั้นเร็วกว่าที่เคยเป็นมาดังนั้นคุณอาจจะดีกว่าเพียงแค่ทำการสแควร์รูทปกติและทดสอบว่าผลลัพธ์นั้นเป็น int หรือไม่ เช่นเคยเกณฑ์มาตรฐานมัน
jalf

4
ตัวแบ่งนี้เริ่มต้นที่ n = 410881 สูตรเวทย์มนตร์ของ John Carmack จะคืนค่า 642.00104 เมื่อรากที่สองที่แท้จริงคือ 641
Kip

11
ฉันเพิ่งใช้เคล็ดลับของ Carmack ในเกม Java และมันก็มีประสิทธิภาพมากทำให้ความเร็วเพิ่มขึ้นประมาณ 40% ดังนั้นมันจึงมีประโยชน์อย่างน้อยใน Java
finnw

3
@Robert Fraser ใช่ + 40% ในอัตราเฟรมโดยรวม เกมที่มีระบบฟิสิกส์ของอนุภาคที่เกิดขึ้นเกือบทุกรอบ CPU ที่มีโดดเด่นด้วยฟังก์ชั่นรากที่สองและฟังก์ชั่นรอบเพื่อที่ใกล้ที่สุดจำนวนเต็ม (ซึ่งผมยังได้เพิ่มประสิทธิภาพการใช้คล้ายสับบิต twiddling.)
finnw

5
ลิงก์เสีย
Pixar

36

หากคุณทำการสับเลขฐานสองเพื่อพยายามหาสแควร์รูท "ถูกต้อง" คุณสามารถตรวจสอบได้อย่างง่ายดายว่าค่าที่คุณได้รับอยู่ใกล้พอที่จะบอกได้หรือไม่:

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

ดังนั้นเมื่อคำนวณn^2แล้วตัวเลือกคือ:

  • n^2 = target: เสร็จแล้วส่งคืนจริง
  • 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)ดังนั้นเมื่อคุณพบบิตเซ็ตบนสุดในเป้าหมายของคุณ (ซึ่งสามารถทำได้ด้วยเคล็ดลับ bit-twiddling; ฉันลืมวิธี) คุณสามารถรับคำตอบที่เป็นไปได้อย่างรวดเร็ว โปรดทราบว่าการสับไบนารีไร้เดียงสายังคงใช้การวนซ้ำได้ถึง 31 หรือ 32 ครั้งเท่านั้น


เงินของฉันอยู่ในแนวทางนี้ หลีกเลี่ยงการเรียก sqrt () เนื่องจากมันคำนวณหาสแควร์รูทเต็มรูปแบบและคุณต้องการเพียงตัวเลขสองสามตัวแรกเท่านั้น
PeterAllenWebb

3
ในทางตรงกันข้ามหากมีการดำเนินการจุดลอยในหน่วย FP โดยเฉพาะมันอาจจะใช้เทคนิคสนุก ๆ ทุกชนิด ฉันไม่ต้องการที่จะเดิมพันโดยไม่มีเกณฑ์มาตรฐาน :) (ฉันอาจลองคืนนี้แม้ว่าใน C # เพียงเพื่อดู ... )
Jon Skeet

8
sqrts ฮาร์ดแวร์จริง ๆ แล้วเร็ววันนี้
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คำสั่งง่ายขึ้นอย่างมากในหนึ่งถ้าคำสั่ง อย่างไรก็ตามสามารถเพิ่มรันไทม์ได้หากตัวเลขที่ทดสอบจำนวนมากมีปัจจัยด้านกำลังสองอย่าง

อัลกอริทึมด้านล่างมีดังนี้:

  • อินเทอร์เน็ต - คำตอบที่โพสต์ของ Kip
  • Durron - คำตอบที่ฉันแก้ไขโดยใช้คำตอบเดียวเป็นฐาน
  • DurronTwo - คำตอบที่ฉันแก้ไขโดยใช้คำตอบแบบสองรอบ (โดย @JohnnyHeggheim) พร้อมการแก้ไขเล็กน้อยอื่น ๆ

นี่คือตัวอย่างรันไทม์หากสร้างตัวเลขโดยใช้ Math.abs(java.util.Random.nextLong())

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

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

vm: java
trial: 0

และนี่คือตัวอย่างรันไทม์หากมันทำงานในช่วงแรกล้านยาวเท่านั้น:

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

UPDATE:ฉันได้สร้างอัลกอริทึมใหม่ที่เร็วกว่าในบางสถานการณ์ช้ากว่าในบางสถานการณ์ฉันได้รับการวัดประสิทธิภาพที่แตกต่างกันตามอินพุตที่แตกต่างกัน หากเราคำนวณโมดูโล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, ไม่ดีที่สุด แต่ชัดเจนดีขึ้นด้วยByteBufferและอื่น ๆ . แต่เนื่องจากว่าเป็น precalculation มัน doesn' ไม่สำคัญมากคุณสามารถค้นหาไฟล์ได้ที่นี่ (หรือสร้างด้วยตัวเอง):

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

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

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

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

ฉันโหลดมันลงในbooleanอาร์เรย์ดังนี้:

private static boolean[] goodLookupSquares = null;

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

    goodLookupSquares = new boolean[0x1FFFFFE];

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

    s.close();
}

ตัวอย่างรันไทม์ มันเอาชนะDurron(เวอร์ชั่นหนึ่ง) ในทุกการทดลองที่ฉันวิ่ง

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

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

vm: java
trial: 0

3
ตารางการค้นหายักษ์ดูเหมือนจะไม่เป็นความคิดที่ดี แคชมิสช้ากว่า (~ 100 ถึง 150 รอบ) กว่าคำสั่งฮาร์ดแวร์ x86 sqrt (~ 20 รอบ) คุณสามารถจัดการกับการพลาดแคชที่โดดเด่นจำนวนมาก แต่คุณยังคงหลีกเลี่ยงข้อมูลที่มีประโยชน์อื่น ๆ ตารางการค้นหาขนาดใหญ่จะคุ้มค่าหากมันเร็วกว่าตัวเลือกอื่น ๆ และฟังก์ชั่นนี้เป็นปัจจัยสำคัญในประสิทธิภาพของโปรแกรมทั้งหมดของคุณ
Peter Cordes

1
@SwissFrank: การตรวจสอบตารางที่สมบูรณ์แบบเป็นสิ่งเดียวที่โปรแกรมของคุณทำได้หรือไม่ ตารางการค้นหาสามารถดูดีใน microbenchmark ที่เรียกมันซ้ำ ๆ ในการวนรอบที่แน่นหนา แต่ในโปรแกรมจริงที่มีข้อมูลอื่นในชุดการทำงานมันไม่ดี
Peter Cordes

1
บิตแมปของ 0x1FFFFFE บิตใช้เวลา 4 mega- ไบต์ถ้าเก็บไว้เป็นบิตแมปที่บรรจุ L3 cache ที่ได้รับความนิยมบนเดสก์ท็อปของ Intel นั้นมีเวลาแฝงมากกว่า 40 รอบและแย่กว่าใน Xeon ขนาดใหญ่ ยาวกว่าเวลาแฝงของฮาร์ดแวร์ sqrt + mul หากเก็บเป็นไบต์ -แผนที่ที่มี 1 ไบต์ต่อค่ามันจะอยู่ที่ประมาณ 32 MB ใหญ่กว่าแคช L3 ของอะไร แต่ Xeon แบบคอร์หลาย ๆ ที่ที่คอร์ทั้งหมดแชร์แคชขนาดใหญ่หนึ่งอัน ดังนั้นหากข้อมูลอินพุตของคุณมีการกระจายแบบสุ่มสม่ำเสมอในช่วงอินพุตที่มีขนาดใหญ่พอคุณจะได้รับแคช L2 จำนวนมากที่พลาดไปแม้อยู่ในลูปที่แคบ (L2 ต่อคอร์ส่วนตัวใน Intel เพียง 256k โดยมีความล่าช้ารอบ ~ 12)
Peter Cordes

1
@SwissFrank: โอ้ถ้าทุกอย่างที่คุณทำคือการตรวจสอบรากแล้วก็มีความเป็นไปได้ด้วยบิตแมปที่จะได้รับความนิยม L3 ฉันดูที่ความหน่วง แต่การพลาดหลายครั้งสามารถบินได้ในครั้งเดียวดังนั้นปริมาณงานจึงอาจดี OTOH, SIMD ทรูพุตsqrtpsหรือแม้กระทั่งsqrtpd(ความแม่นยำสองเท่า) นั้นไม่ได้เลวร้ายใน Skylake แต่ก็ไม่ได้ดีไปกว่า latency ในซีพียูเก่า อย่างไรก็ตาม7-cpu.com/cpu/Haswell.htmlมีหมายเลขทดสอบที่ดีและหน้าสำหรับ CPU อื่น ๆ คู่มือ microarch pdf ของ Agner Fog มีตัวเลขแฝงแคชสำหรับ Intel และ AMD uarches: agner.org/optimize
Peter Cordes

1
การใช้ x86 SIMD จาก Java เป็นปัญหาและเมื่อคุณเพิ่มค่าใช้จ่ายในการแปลง int-> fp และ fp-> int เป็นไปได้ที่บิตแมปจะดีกว่า คุณต้องการdoubleความแม่นยำในการหลีกเลี่ยงการปัดเศษจำนวนเต็มบางส่วนนอกช่วง + -2 ^ 24 (ดังนั้นจำนวนเต็ม 32 บิตอาจอยู่นอกนั้น) และsqrtpdช้ากว่าsqrtpsเช่นเดียวกับการประมวลผลองค์ประกอบครึ่งหนึ่งตามคำแนะนำ (ต่อ SIMD เวกเตอร์) .
Peter Cordes

18

มันควรจะเร็วกว่ามากในการใช้วิธีการของนิวตันในการคำนวณจำนวนเต็มของสแควร์รูทจากนั้นจึงเพิ่มจำนวนนี้และตรวจสอบตามที่คุณทำในโซลูชันปัจจุบันของคุณ วิธีการของนิวตันเป็นพื้นฐานสำหรับการแก้ปัญหา Carmack ที่กล่าวถึงในคำตอบอื่น ๆ คุณควรจะได้รับคำตอบที่เร็วขึ้นเพราะคุณเพียง แต่สนใจในส่วนจำนวนเต็มของรูททำให้คุณสามารถหยุดอัลกอริทึมการประมาณได้เร็วขึ้น

การปรับให้เหมาะสมอื่นที่คุณสามารถลองได้: หากDigital Rootของตัวเลขไม่ได้ลงท้ายด้วย 1, 4, 7 หรือ 9 ตัวเลขนั้นไม่ใช่จัตุรัสที่สมบูรณ์แบบ วิธีนี้สามารถใช้เป็นวิธีที่รวดเร็วในการกำจัดอินพุตของคุณ 60% ก่อนที่จะใช้อัลกอริทึมรากที่สองที่ช้ากว่า


1
รูตดิจิทัลนั้นคำนวณอย่างเข้มงวดเทียบเท่ากับโมดูโลดังนั้นจึงควรพิจารณาพร้อมกับวิธีการโมดูโลอื่น ๆ ที่นี่เช่น mod 16 และ mod 255
Christian Oudard

1
คุณแน่ใจหรือไม่ว่ารูทดิจิทัลนั้นเทียบเท่าโมดูโล ดูเหมือนจะเป็นสิ่งที่แตกต่างอย่างสิ้นเชิงตามที่อธิบายไว้ในลิงค์ สังเกตว่ารายการคือ 1,4,7,9 ไม่ใช่ 1,4,5,9
Fractaly

1
รากดิจิตอลในระบบทศนิยมเทียบเท่ากับการใช้ modulo 9 (well dr (n) = 1 + ((n-1) mod 9) ดังนั้นการเปลี่ยนแปลงเล็กน้อยเช่นกัน) ตัวเลข 0,1,4,5,9 สำหรับโมดูโล 16 และ 0, 1, 4, 7 สำหรับโมดูโล 9 - ซึ่งสอดคล้องกับ 1, 4, 7, 9 สำหรับรูทดิจิทัล
Hans Olsson

16

ฉันต้องการให้ฟังก์ชันนี้ทำงานกับจำนวนเต็ม 64 บิตที่เป็นบวกทั้งหมด

Math.sqrt()ทำงานร่วมกับคู่เป็นพารามิเตอร์การป้อนข้อมูลเพื่อให้คุณจะไม่ได้รับผลลัพธ์ที่ถูกต้องสำหรับจำนวนเต็มขนาดใหญ่กว่า2 ^ 53


5
ฉันทดสอบคำตอบสำหรับสี่เหลี่ยมจัตุรัสที่สมบูรณ์แบบทั้งหมดที่มีขนาดใหญ่กว่า 2 ^ 53 รวมถึงตัวเลขทั้งหมดจาก 5 ด้านล่างของแต่ละสี่เหลี่ยมจัตุรัสถึง 5 ข้างบนสี่เหลี่ยมจัตุรัสที่สมบูรณ์และฉันได้ผลลัพธ์ที่ถูกต้อง (ข้อผิดพลาด roundoff แก้ไขเมื่อฉันรอบคำตอบ sqrt ไปนานแล้วตารางที่คุ้มค่าและเปรียบเทียบ)
กีบ

2
@ คิป: ฉันคิดว่าฉันได้พิสูจน์แล้วว่ามันใช้งานได้
maaartinus

ผลลัพธ์ไม่ถูกต้องสมบูรณ์แบบ แต่แม่นยำกว่าที่คุณคิด หากเราสมมติว่าตัวเลขที่ถูกต้องอย่างน้อย 15 หลักหลังจากการแปลงเป็นสองเท่าและหลังรากที่สองนั่นก็เพียงพอแล้วเพราะเราต้องการไม่เกิน 11: 10 หลักสำหรับรากที่สองแบบ 32 บิตและน้อยกว่า 1 สำหรับตำแหน่งทศนิยมเพราะ +0.5 ปัดเศษให้ใกล้ที่สุด
mwfearnley

3
Math.sqrt () ไม่ถูกต้องทั้งหมด แต่ไม่จำเป็นต้อง ในการโพสต์แรก tst เป็นจำนวนเต็มใกล้กับ sqrt (N) ถ้า N ไม่ใช่ตารางดังนั้น tst * tst! = N ไม่ว่าค่าของ tst คืออะไร ถ้า 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 ทำทุกวันนี้ในฮาร์ดแวร์และความต้องการในการคำนวณจำนวนเฉพาะที่นี่ฉันคิดว่าวิธีนี้จะช้าลง แต่ควรให้ผลลัพธ์ที่ดีกว่าโซลูชันด้วย sqrt ซึ่งจะไม่ทำงาน 2 ^ 54 ดังที่ mrzl พูดในคำตอบของเขา


1
การแบ่งจำนวนเต็มช้ากว่า FP sqrt บนฮาร์ดแวร์ปัจจุบัน ความคิดนี้ไม่มีโอกาส >. <แม้กระทั่งในปี 2008 sqrtsdอัตราความเร็วของ Core2 คือหนึ่งต่อ 6-58c มันidivคือหนึ่งต่อ 12-36 ครั้ง (เวลาแฝงที่คล้ายกับปริมาณงาน: ไม่มีหน่วยเป็น pipelined)
Peter Cordes

sqrt ไม่จำเป็นต้องแม่นยำอย่างสมบูรณ์ นั่นเป็นเหตุผลที่คุณตรวจสอบด้วยผลคูณจำนวนเต็มและเปรียบเทียบจำนวนเต็มเพื่อตัดสินใจว่าจำนวนเต็มเข้านั้นมีจำนวนเต็ม sqrt ที่แน่นอนหรือไม่
Peter Cordes

11

มีการชี้ให้เห็นว่าdตัวเลขสุดท้ายของสี่เหลี่ยมจัตุรัสที่สมบูรณ์สามารถใช้กับค่าบางค่าเท่านั้น dตัวเลขสุดท้าย(เป็นฐานb) ของตัวเลขnจะเหมือนกับส่วนที่เหลือเมื่อnถูกหารด้วยbdเช่น n % pow(b, d)ในสัญกรณ์ C

สิ่งนี้สามารถสรุปได้ทั่วไปกับโมดูลัสใด ๆmเช่น n % mสามารถใช้ในการแยกแยะตัวเลขบางส่วนจากการเป็นสี่เหลี่ยมที่สมบูรณ์แบบ โมดูลัสที่คุณกำลังใช้คือ 64 ซึ่งอนุญาตให้ 12 คือ 19% ของส่วนที่เหลือเป็นสี่เหลี่ยมที่เป็นไปได้ ด้วยการเข้ารหัสเล็กน้อยฉันพบโมดูลัส 110880 ซึ่งอนุญาตเฉพาะ 2016 คือ 1.8% ของเศษที่เหลือเป็นสี่เหลี่ยมที่เป็นไปได้ ดังนั้นขึ้นอยู่กับค่าใช้จ่ายของการดำเนินการโมดูลัส (เช่น. การหาร) และการค้นหาตารางเทียบกับรากที่สองในเครื่องของคุณการใช้โมดูลัสนี้อาจเร็วกว่า

โดยวิธีถ้า Java มีวิธีการเก็บอาร์เรย์บิตบรรจุสำหรับตารางการค้นหาอย่าใช้มัน 110880 คำ 32- บิตไม่ใช่ RAM มากในปัจจุบันและการดึงคำของเครื่องจักรจะเร็วกว่าการดึงข้อมูลเพียงเล็กน้อย


ดี คุณทำการคำนวณเชิงพีชคณิตหรือโดยการลองผิดลองถูกไหม? ฉันเห็นว่าทำไมมันถึงได้ผล - มีการชนกันมากมายระหว่างสี่เหลี่ยมที่สมบูรณ์แบบเช่น 333 ^ 2% 110880 == 3 ^ 2, 334 ^ 2% 110880 == 26 ^ 2, 338 ^ 2% 110880 == 58 ^ 2 .. .
finnw

IIRC เป็นกำลังดุร้าย แต่โปรดทราบว่า 110880 = 2 ^ 5 * 3 ^ 2 * 5 * 7 * 11 ซึ่งให้ 6 * 3 * 2 * 2 * 2 - 1 = 143 ตัวหารที่เหมาะสม
Hugh Allen

ฉันพบว่าเนื่องจากข้อ จำกัด ของการค้นหา 44352 ทำงานได้ดีขึ้นด้วยอัตราการส่งผ่าน 2.6% อย่างน้อยในการดำเนินการของฉัน
Fractaly

1
การแบ่งจำนวนเต็ม ( idiv) เท่ากับหรือแย่กว่าในราคาของ FP sqrt ( sqrtsd) สำหรับฮาร์ดแวร์ x86 ปัจจุบัน ยังไม่เห็นด้วยอย่างยิ่งกับการหลีกเลี่ยงบิตฟิลด์ อัตราการใช้แคชจะดีขึ้นมากเมื่อเทียบกับบิตฟิลด์และการทดสอบบิตในบิตฟิลด์เป็นเพียงคำแนะนำง่าย ๆ เพียงหนึ่งหรือสองวิธีมากกว่าการทดสอบไบต์ทั้งหมด (สำหรับตารางเล็ก ๆ ที่พอดีกับแคชแม้ในขณะที่ไม่ใช่บิตฟิลด์อาร์เรย์ไบต์จะดีที่สุดไม่ใช่ 32 บิต ints x86 มีการเข้าถึงไบต์เดียวด้วยความเร็วเท่ากับ 32 บิต dword.)
Peter Cordes

11

ปัญหาจำนวนเต็มควรได้รับการแก้ปัญหาจำนวนเต็ม ดังนั้น

จะค้นหาไบนารีบน (ที่ไม่ใช่เชิงลบ) t**2 <= nจำนวนเต็มเพื่อหาสิ่งที่ยิ่งใหญ่ที่สุดจำนวนเต็มเสื้อดังกล่าวว่า การทดสอบแล้วว่าr**2 = nว่า ใช้เวลา O (log n)

หากคุณไม่ทราบวิธีการค้นหาเลขจำนวนเต็มบวกของไบนารีเนื่องจากชุดนี้ไม่มีขอบเขตมันเป็นเรื่องง่าย คุณเริ่มต้นด้วยการคำนวณฟังก์ชั่นที่เพิ่มขึ้นของคุณ f (ด้านบนf(t) = t**2 - n) กับพลังของทั้งสอง เมื่อคุณเห็นว่ามันเป็นบวกคุณจะพบขอบเขตบน จากนั้นคุณสามารถทำการค้นหาแบบไบนารีมาตรฐาน


จริงๆแล้วเวลาจะเป็นอย่างน้อย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

นี่เป็นการใช้งานจาวาที่เร็วที่สุดที่ฉันสามารถทำได้โดยใช้เทคนิคที่ผู้อื่นแนะนำไว้ในชุดนี้

  • การทดสอบ Mod-256
  • การทดสอบที่ไม่แน่นอน mod-3465 (หลีกเลี่ยงการหารจำนวนเต็มในราคาที่เป็นค่าบวกเท็จ)
  • รากที่สองของทศนิยมจุดกลมและเปรียบเทียบกับค่าอินพุต

ฉันยังทดลองกับการดัดแปลงเหล่านี้ แต่พวกเขาไม่ได้ช่วยประสิทธิภาพ:

  • การทดสอบ mod-255 เพิ่มเติม
  • การหารค่าอินพุตด้วยกำลังของ 4
  • Fast Inverse Square Root (เพื่อทำงานสำหรับค่า 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

คุณควรกำจัดส่วนที่ 2 กำลังของ N ตั้งแต่เริ่มต้น

2nd Edit การแสดงออกที่น่าอัศจรรย์สำหรับ m ด้านล่างควรเป็น

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

และไม่เป็นลายลักษณ์อักษร

สิ้นสุดการแก้ไขครั้งที่ 2

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)และข้อผิดพลาดสุดท้ายเป็นเพียงขั้นตอนเดียวด้านล่างตัวหารสุดท้ายและอัลกอริทึมถูกยกเลิก มันง่ายในการแก้ไขโดยการเพิ่มเข้ากับอาร์กิวเมนต์ดั้งเดิมก่อนคำนวณข้อผิดพลาด (เพิ่มสองอันสำหรับรูทคิวบ์ ฯลฯ )

คุณลักษณะที่ดีอย่างหนึ่งของอัลกอริธึมนี้คือคุณสามารถบอกได้ทันทีว่าตัวเลขเป็นสี่เหลี่ยมจัตุรัสที่สมบูรณ์แบบหรือไม่ข้อผิดพลาดสุดท้าย (ไม่ใช่การแก้ไข) ในวิธีการของนิวตันจะเป็นศูนย์ การดัดแปลงอย่างง่ายยังช่วยให้คุณคำนวณอย่างรวดเร็วfloor(sqrt(x))แทนจำนวนเต็มที่ใกล้เคียงที่สุด สิ่งนี้มีประโยชน์กับปัญหาหลายอย่างของออยเลอร์


1
ฉันคิดในสิ่งเดียวกันเกี่ยวกับอัลกอริทึมเหล่านี้ที่แปลได้ดีกับบัฟเฟอร์ที่มีความแม่นยำสูง ดังนั้นคิดว่าฉันติดที่นี่ ... จริง ๆ แล้วฉันพบการทดสอบความน่าจะเป็นกำลังสองที่มีความซับซ้อนเชิงซ้อนที่ดีขึ้นสำหรับคนจำนวนมาก ..... ซึ่งการประยุกต์ใช้ทฤษฎีจำนวนไม่พบตัวเองอย่างผิดปกติ ไม่คุ้นเคยกับ Project Euler แม้ว่า ... ดูน่าสนใจ

6

นี่คือการทำใหม่จากทศนิยมถึงไบนารีของอัลกอริทึมเครื่องคิดเลข Marchant เก่า (ขออภัยฉันไม่มีข้อมูลอ้างอิง) ใน Ruby ที่ดัดแปลงมาสำหรับคำถามนี้โดยเฉพาะ:

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 clunky - เป็นอัลกอริทึมที่นับและ C ++ ไม่ใช่ภาษาบ้านของฉัน) ในกรณีนี้เรากำลังมองหาสารตกค้าง == 0:

#include <iostream>  

using namespace std;  
typedef unsigned long long int llint;

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

public:

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

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

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

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

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

จำนวนการวนซ้ำมีลักษณะเป็น O (ln n) โดยที่ n คือความยาวบิตของ v ดังนั้นฉันสงสัยว่าสิ่งนี้จะประหยัดได้มากสำหรับ v ที่ใหญ่กว่าจุดลอยตัว sqrt ช้าอาจจะประมาณ 100-200 รอบ แต่คณิตศาสตร์เลขจำนวนเต็มไม่ใช่ ฟรีทั้ง การวนซ้ำสิบรอบรอบละ 15 รอบและมันเป็นการล้าง ถึงกระนั้น +1 ก็น่าสนใจ
Tadmas

ที่จริงฉันเชื่อว่าการเพิ่มและการลบสามารถทำได้โดย XOR
Brent.Longborough

นั่นคือความคิดเห็นที่บ้าคลั่ง - การเพิ่มเท่านั้นที่สามารถทำได้โดย XOR; การลบคือเลขคณิต
Brent.Longborough

1
มีความแตกต่างที่สำคัญระหว่างเวลารันของ XOR และการเพิ่มต่อไปหรือไม่?
Tadmas

1
@Tadmas: อาจไม่เพียงพอที่จะทำลายกฎ "เพิ่มประสิทธิภาพในภายหลัง" (:-)
Brent.Longborough

6

การเรียกใช้ sqrt นั้นไม่ถูกต้องอย่างสมบูรณ์ดังที่ได้กล่าวมาแล้ว แต่มันก็น่าสนใจและให้คำแนะนำว่ามันจะไม่ตอบคำถามอื่น ๆ ในแง่ของความเร็ว หลังจากทั้งหมดลำดับของคำแนะนำภาษาแอสเซมบลีสำหรับ sqrt มีขนาดเล็ก Intel มีคำสั่งฮาร์ดแวร์ซึ่ง Java ไม่ได้ใช้เพราะฉันเชื่อว่าไม่เป็นไปตาม IEEE

แล้วทำไมมันช้า เนื่องจาก Java กำลังเรียกรูทีน C ผ่าน JNI และจริงๆแล้วช้ากว่าการเรียกรูทีนย่อย Java ซึ่งตัวมันเองช้ากว่าการทำอินไลน์ สิ่งนี้น่ารำคาญมากและ Java ควรมีวิธีแก้ปัญหาที่ดีกว่าเช่นการสร้างในการเรียกไลบรารี่แบบ floating point ถ้าจำเป็น โอ้ดี

ใน 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 ทำงานในเวลาประมาณ 1/3 ของเวลาบนเครื่อง core2 duo ของฉัน ฉันสงสัยว่าการปรับแต่งเพิ่มเติมในบรรทัดเดียวกันนั้นสามารถลดเวลาโดยเฉลี่ยต่อไปได้ แต่ทุกครั้งที่คุณตรวจสอบคุณกำลังทำการทดสอบเพิ่มเติมเพื่อกำจัดให้หมดไปดังนั้นคุณจะไม่สามารถไปไกลกว่านั้นได้

แน่นอนว่าแทนที่จะมีการทดสอบแยกต่างหากสำหรับลบคุณสามารถตรวจสอบ 6 บิตสูงเช่นเดียวกัน

โปรดทราบว่าสิ่งที่ฉันทำคือกำจัดสี่เหลี่ยมที่เป็นไปได้ แต่เมื่อฉันมีกรณีที่เป็นไปได้ฉันต้องโทรหาต้นฉบับที่ถูกขีดเส้นใต้ isPerfectSquare

รูทีน init2 ถูกเรียกหนึ่งครั้งเพื่อเริ่มต้นค่าคงที่ของ pp1 และ pp2 โปรดทราบว่าในการใช้งานของฉันใน C ++ ฉันกำลังใช้เวลาที่ไม่ได้ลงชื่อนานดังนั้นเมื่อคุณลงชื่อคุณจะต้องใช้โอเปอเรเตอร์ >>>

ไม่มีความจำเป็นที่แท้จริงที่จะ จำกัด ขอบเขตการตรวจสอบอาร์เรย์ แต่เครื่องมือเพิ่มประสิทธิภาพของ Java ต้องคิดสิ่งนี้ออกมาอย่างรวดเร็วดังนั้นฉันจึงไม่โทษพวกเขาสำหรับสิ่งนั้น


3
ฉันพนันว่าคุณผิดสองครั้ง 1. Intel sqrt เป็นไปตาม IEEE คำแนะนำที่ไม่เป็นไปตามคำสั่งเพียงอย่างเดียวคือคำแนะนำ goniometric สำหรับข้อโต้แย้งแบบมีเหตุมีผล 2. Java ใช้ intrinsics สำหรับ Math.sqrt, ไม่มี JNI
maaartinus

1
คุณไม่ลืมที่จะใช้pp2? ฉันเข้าใจว่าpp1ใช้สำหรับการทดสอบบิตที่มีนัยสำคัญน้อยที่สุดหกบิต แต่ฉันไม่เชื่อว่าการทดสอบหกบิตถัดไปจะสมเหตุสมผล
maaartinus

6

ฉันชอบความคิดที่จะใช้วิธีการที่ถูกต้องในอินพุตบางส่วน นี่คือรุ่นที่มี "ออฟเซ็ต" ที่สูงกว่า รหัสดูเหมือนว่าจะทำงานและผ่านกรณีทดสอบง่ายๆของฉัน

เพียงแทนที่ของคุณ:

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

รหัสกับอันนี้:

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

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

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

6

เมื่อพิจารณาถึงความยาวบิตทั่วไป (แม้ว่าฉันได้ใช้ชนิดเฉพาะที่นี่) ฉันพยายามออกแบบ algo แบบง่าย ๆ ดังต่อไปนี้ ตรวจสอบง่ายและชัดเจนสำหรับ 0,1,2 หรือ <0 เป็นสิ่งจำเป็นในตอนแรก การติดตามเป็นเรื่องง่ายที่จะไม่พยายามใช้ฟังก์ชันคณิตศาสตร์ที่มีอยู่ ตัวดำเนินการส่วนใหญ่สามารถแทนที่ด้วยตัวดำเนินการที่ชาญฉลาดบิต ฉันยังไม่ได้ทดสอบกับข้อมูลเครื่องหมายเปรียบเทียบใด ๆ ฉันไม่ใช่ผู้เชี่ยวชาญในวิชาคณิตศาสตร์หรือการออกแบบอัลกอริทึมคอมพิวเตอร์โดยเฉพาะฉันชอบที่จะเห็นคุณชี้ปัญหา ฉันรู้ว่ามีโอกาสในการปรับปรุงมากมายที่นั่น

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

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

@Kip: ปัญหาบางอย่างกับเบราว์เซอร์ของฉัน
nabam serbang

1
คุณต้องเยื้อง
Steve Kuo

5

ฉันตรวจสอบผลลัพธ์ที่เป็นไปได้ทั้งหมดเมื่อตรวจสอบบิตสุดท้ายของสี่เหลี่ยม โดยการตรวจสอบบิตเพิ่มเติมอย่างต่อเนื่องสามารถลดอินพุตได้สูงสุดถึง 5 / 6th จริง ๆ แล้วฉันออกแบบสิ่งนี้เพื่อใช้อัลกอริธึมการแยกตัวประกอบของแฟร์มาต์และรวดเร็วมาก

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

pseudocode บิตสุดท้ายสามารถใช้เพื่อขยายการทดสอบเพื่อกำจัดค่าเพิ่มเติม การทดสอบข้างต้นสำหรับ k = 0, 1, 2, 3

  • a เป็นรูปแบบ (3 << 2k) - 1
  • b เป็นรูปแบบ (2 << 2k)
  • c เป็นรูปแบบ (2 << 2k + 2) - 1
  • d เป็นรูปแบบ (2 << 2k - 1) * 10

    มันทดสอบก่อนว่ามันมีสแควร์ส่วนที่เหลือกับโมดูลัสของกำลังสองหรือไม่จากนั้นก็ทดสอบตามโมดูลัสสุดท้ายจากนั้นใช้ Math.sqrt เพื่อทำการทดสอบขั้นสุดท้าย ฉันคิดด้วยไอเดียจากโพสต์ยอดนิยมและพยายามที่จะทำต่อไป ฉันขอขอบคุณที่แสดงความคิดเห็นหรือข้อเสนอแนะ

    อัปเดต:การใช้การทดสอบโดยโมดูลัส, (modSq) และโมดูลัสฐานของ 44352 การทดสอบของฉันทำงานใน 96% ของเวลาที่หนึ่งในการอัพเดทของ OP สำหรับตัวเลขสูงถึง 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
    • numberมี 5 หลัก: solutionอยู่ในช่วง = 100 - 400

    สังเกตเห็นการทำซ้ำหรือไม่

    คุณสามารถใช้ช่วงนี้ในวิธีการค้นหาแบบไบนารีเพื่อดูว่ามีสิ่งsolutionใดบ้าง:

    number == solution * solution

    นี่คือรหัส

    นี่คือคลาสของฉัน SquareRootChecker

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

    และนี่คือตัวอย่างวิธีการใช้งาน

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

    2
    ฉันชอบแนวคิดนี้ แต่ฉันอยากจะชี้ให้เห็นข้อบกพร่องที่สำคัญอย่างสุภาพ: ตัวเลขอยู่ในฐานสองฐานสอง การแปลงฐาน 2 ถึงฐาน 10 ผ่านtoStringเป็นงานที่มีราคาแพงอย่างเหลือเชื่อเมื่อเทียบกับตัวดำเนินการระดับบิต ดังนั้นเพื่อตอบสนองวัตถุประสงค์ของคำถาม - ประสิทธิภาพ - คุณจะต้องใช้ตัวดำเนินการระดับบิตแทนฐาน 10 สตริง อีกครั้งฉันชอบแนวคิดของคุณ อย่างไรก็ตามการติดตั้งของคุณ (ตามที่เป็นอยู่ในปัจจุบัน) นั้นเป็นวิธีที่ช้าที่สุดในบรรดาวิธีแก้ปัญหาที่เป็นไปได้ทั้งหมดที่โพสต์ไว้สำหรับคำถาม
    Jack Giffin

    1

    หากความกังวลเกี่ยวกับความเร็วทำไมไม่แบ่งพาร์ติชันออกจากชุดอินพุตและค่าที่ใช้บ่อยที่สุดไปยังตารางการค้นหา


    ปัญหาคือไม่มี "ชุดอินพุตที่ใช้กันทั่วไป" - ปกติฉันจะวนซ้ำในรายการดังนั้นฉันจะไม่ใช้อินพุตเดียวกันสองครั้ง
    หนังลูกวัว

    1

    มันควรจะเป็นไปได้ที่จะแพ็ค 'ไม่สามารถเป็นสแควร์ที่สมบูรณ์แบบได้หากตัวเลข X สุดท้ายเป็น N' มีประสิทธิภาพมากกว่านั้น! ฉันจะใช้จาวา int 32 บิตและสร้างข้อมูลมากพอที่จะตรวจสอบจำนวน 16 บิตสุดท้าย - นั่นคือ 2048 ค่าเลขฐานสิบหก

    ...

    ตกลง. ไม่ว่าฉันจะได้เจอทฤษฎีจำนวนที่น้อยกว่าฉันหรือมีข้อผิดพลาดในรหัสของฉัน ในกรณีใด ๆ นี่คือรหัส:

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

    และนี่คือผลลัพธ์:

    (ed: elided สำหรับประสิทธิภาพที่ไม่ดีใน prettify.js ดูประวัติการแก้ไขเพื่อดู)


    1

    วิธีการของนิวตันพร้อมเลขคณิตจำนวนเต็ม

    หากคุณต้องการหลีกเลี่ยงการดำเนินการที่ไม่ใช่จำนวนเต็มคุณสามารถใช้วิธีการด้านล่าง โดยทั่วไปจะใช้วิธีของนิวตันที่แก้ไขสำหรับเลขจำนวนเต็ม

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

    Math.sqrtการดำเนินการนี้ไม่สามารถแข่งขันกับโซลูชั่นที่ใช้งาน อย่างไรก็ตามสามารถปรับปรุงประสิทธิภาพโดยใช้กลไกการกรองที่อธิบายไว้ในโพสต์อื่น ๆ


    1

    การคำนวณรากที่สองด้วยวิธีของนิวตันนั้นน่ากลัวอย่างรวดเร็ว ... โดยที่ค่าเริ่มต้นนั้นสมเหตุสมผล อย่างไรก็ตามไม่มีค่าเริ่มต้นที่สมเหตุสมผลและในทางปฏิบัติเราสิ้นสุดด้วยพฤติกรรมแบบทวิภาคและบันทึก (2 ^ 64)
    เพื่อความรวดเร็วเราต้องใช้วิธีที่รวดเร็วเพื่อให้ได้ค่าเริ่มต้นที่เหมาะสมและนั่นหมายความว่าเราต้องสืบหาภาษาเครื่อง หากหน่วยประมวลผลมีคำสั่งเช่น POPCNT ใน Pentium ซึ่งนับเป็นศูนย์นำหน้าเราสามารถใช้มันเพื่อให้มีค่าเริ่มต้นโดยมีครึ่งหนึ่งของบิตที่สำคัญ ด้วยความระมัดระวังเราสามารถหาจำนวนนิวตันที่แน่นอนซึ่งจะเพียงพอ (ดังนั้นจึงจำเป็นที่จะต้องวนซ้ำและมีการประมวลผลที่รวดเร็วมาก)

    วิธีแก้ปัญหาที่สองกำลังดำเนินการผ่านจุดลอยตัวซึ่งอาจมีการคำนวณ sqrt ที่รวดเร็ว (เช่นตัวประมวลผลร่วม i87) แม้แต่การสำรวจผ่าน exp () และ log () อาจเร็วกว่านิวตันที่ถดถอยลงในการค้นหาแบบไบนารี มีความยุ่งยากในด้านนี้การวิเคราะห์ขึ้นอยู่กับหน่วยประมวลผลของสิ่งที่และถ้าจำเป็นต้องมีการปรับแต่งภายหลัง

    วิธีที่สามแก้ปัญหาที่แตกต่างกันเล็กน้อย แต่ก็คุ้มค่าที่จะกล่าวถึงเพราะสถานการณ์ได้อธิบายไว้ในคำถาม หากคุณต้องการคำนวณรากที่สองจำนวนมากสำหรับตัวเลขที่แตกต่างกันเล็กน้อยคุณสามารถใช้การคำนวณซ้ำของ Newton หากคุณไม่เคยกำหนดค่าเริ่มต้นใหม่ แต่ให้ปล่อยไว้ที่การคำนวณก่อนหน้านี้ค้างไว้ ฉันใช้มันกับความสำเร็จในปัญหาของออยเลอร์อย่างน้อยหนึ่งปัญหา


    การประเมินที่ดีนั้นไม่ยากเกินไป คุณสามารถใช้จำนวนหลักของตัวเลขเพื่อประมาณขอบเขตล่างและบนสำหรับการแก้ปัญหา ดูคำตอบของฉันที่ซึ่งฉันขอเสนอวิธีหารและพิชิต
    MWB

    ความแตกต่างระหว่าง POPCNT และการนับจำนวนหลักคืออะไร ยกเว้นว่าคุณสามารถทำ 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
    สวรรค์ M Weasel

    3
    ใช่ แต่ช่วงของความยาวคือ 64 บิตไม่ใช่ 32 บิต sqrt (2 ^ 64) = 2 ^ 32 (ฉันเพิกเฉยเครื่องหมายบิตเพื่อทำให้คณิตศาสตร์ง่ายขึ้นเล็กน้อย ... มีอยู่จริง (ยาว) (2 ^ 31.5) = 3037000499 สี่เหลี่ยมที่สมบูรณ์แบบ)
    Kip

    0

    เกี่ยวกับวิธีการ Carmac ดูเหมือนว่ามันจะค่อนข้างง่ายเพียงแค่วนซ้ำอีกครั้งซึ่งควรเพิ่มจำนวนของความแม่นยำเป็นสองเท่า มันเป็นวิธีการวนซ้ำที่ถูกตัดทอนอย่างมาก - ของนิวตันด้วยการเดาครั้งแรกที่ดีมาก

    เกี่ยวกับปัจจุบันที่ดีที่สุดของคุณฉันเห็นการเพิ่มประสิทธิภาพขนาดเล็กสองประการ:

    • ย้ายเช็คกับ 0 หลังจากเช็คโดยใช้ mod255
    • จัดเรียงกำลังที่แบ่งออกเป็นสี่ส่วนเพื่อข้ามการตรวจสอบทั้งหมดสำหรับกรณีปกติ (75%)

    เช่น:

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

    ดียิ่งขึ้นอาจเป็นเรื่องง่าย

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

    เห็นได้ชัดว่ามันน่าสนใจที่จะรู้ว่ามีตัวเลขจำนวนเท่าใดที่ถูกเลือกในแต่ละด่าน - ฉันค่อนข้างสงสัยว่าการตรวจสอบนั้นเป็นอิสระอย่างแท้จริงซึ่งทำให้สิ่งต่าง ๆ ยุ่งยาก

    โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
    Licensed under cc by-sa 3.0 with attribution required.