整数分解:令人畏惧的素数列表
使用大型素数列表和试除法算法,
下载
目录
背景
本文是一系列文章的一部分。每篇文章都着重于整数分解的一个方面。
- 整数分解:试除法算法[^]:关注试除法算法的变体及其各自的效率。
- 整数分解:令人头疼的素数列表[^]:关注通过压缩处理大量素数列表的方法。
- 整数分解:优化小因子检查[^]:关注比除法更快地检查小因子的方法。
- 整数分解:逆转乘法[^]:关注优化试除法算法的另一种方法。
引言
试除法算法是通过检查所有可能的因子来分解一个整数,而所有有趣的因子都是素数。问题是如何处理大量的素数列表。
第一种方法:简单的素数列表
当使用素数列表时,第一种方法是使用一个包含素数的简单整数数组。
在 64K 以下,一个素数可以存储在 int16
(2 字节)中,超过这个范围,一个素数则更适合存储在 int32
(4 字节)中。
简单素数列表的数字
上限 | 素数数量 | 数组大小 |
65,536 | 6,542 | 13,084 |
100,000 | 9,592 | 38,368 |
500,000 | 41,538 | 166,152 |
1,000,000 | 78,498 | 313,992 |
5,000,000 | 348,513 | 1,394,052 |
10,000,000 | 664,579 | 2,658,316 |
100,000,000 | 5,761,455 | 23,045,820, |
2^32-1 4,294,967,295 | 203,280,221 | 813,120,884 |
正如你所见,当列表非常庞大时,情况会变得非常糟糕。
示例代码
// Trial Division: Square Root + Prime list + Wheel
// Check all numbers until Square Root, start with a list of primes
// and Skip useless factors with a wheel
long long TD_SRPW(long long Cand) {
long long SPrimes[] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43,
47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113,
127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191,
193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263,
269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347,
349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421,
431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499,
503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593,
599, 601, 0 };
long long Wheel[] = { 6, 4, 2, 4, 2, 4, 6, 2, 0 };
long long Div;
Count = 0;
long long Top = sqrt(Cand);
// Check small primes
for (long long Ind = 0; SPrimes[Ind] != 0; Ind++) {
Div = SPrimes[Ind]; // for debug purpose
if (SPrimes[Ind] > Top) {
return Cand;
}
Count++;
if (Cand % SPrimes[Ind] == 0) {
return SPrimes[Ind];
}
}
// Start the Wheel
Div = 601;
while (Div <= Top) {
for (long long Ind = 0; Wheel[Ind] != 0; Ind++) {
if (Div > Top) {
return Cand;
}
Count++;
if (Cand % Div == 0) {
return Div;
}
Div += Wheel[Ind];
}
}
return Cand;
}
TrialDivLP.cpp 在 TrialDivLP.zip 中.
第二种方法:通过位字段数组编码压缩素数列表
与其使用素数列表,不如采用编码一个数字是否为素数的方法,这是布尔信息,可以存储在一个位中。
采用这种方法,可以应用经典试除法算法变体中的相同优化,并且节省巨大。
所有数字的位字段
非常简单的想法,每个数字 1 位,或者一个字节编码 8 个数字。
数字如下
上限 | 素数数量 | 数组大小 |
65,536 | 6,542 | 8,192 |
100,000 | 9,592 | 12,500 |
500,000 | 41,538 | 62,500 |
1,000,000 | 78,498 | 125,000 |
5,000,000 | 348,513 | 625,000 |
10,000,000 | 664,579 | 1,250,000 |
100,000,000 | 5,761,455 | 12,500,000, |
2^32-1 4,294,967,295 | 203,280,221 | 536,870,912 |
所有奇数的位字段
稍微优化一下,每个奇数 1 位,或者一个字节编码 16 个数字(8 个奇数)。
数字如下所示
上限 | 素数数量 | 数组大小 |
65,536 | 6,542 | 4,096 |
100,000 | 9,592 | 6,250 |
500,000 | 41,538 | 31,250 |
1,000,000 | 78,498 | 62,500 |
5,000,000 | 348,513 | 312,500 |
10,000,000 | 664,579 | ,625,000 |
100,000,000 | 5,761,455 | 6,250,000, |
2^32-1 4,294,967,295 | 203,280,221 | 268,435,456 |
使用“30轮”筛选的所有数字的位字段
然后更加优化,使用“轮”(wheel),每 30 个数字只有 8 个可能的素数,或者一个字节编码 30 个数字(8 个可能的素数)。
数字是
上限 | 素数数量 | 数组大小 |
65,536 | 6,542 | 2,188 |
100,000 | 9,592 | 3,333 |
500,000 | 41,538 | 16,667 |
1,000,000 | 78,498 | 33,333 |
5,000,000 | 348,513 | 166,667 |
10,000,000 | 664,579 | 333,334 |
100,000,000 | 5,761,455 | 3,333,334, |
2^32-1 4,294,967,295 | 203,280,221 | 143,165,577 |
利用素数列表的代码(存储在头文件中)。
// Trial Division: Square Root + Long Prime list + Wheel
// Check all numbers until Square Root, start with a list of primes
// and Skip useless factors with a wheel
long long TD_SRLPW(long long Cand) {
long long WPrimes[] = { 2, 3, 5, 0 };
long long Wheel[] = { 6, 4, 2, 4, 2, 4, 6, 2, 0 };
// unsigned char EPrimes[] = { 0xfe, 0xdf, 0xef, 0x7e, 0xb6, 0xdb, 0x3d, ...
// Encoded list moved to header file
long long Div;
Count = 0;
long long Top = sqrt(Cand);
// Check small primes
for (long long Ind = 0; WPrimes[Ind] != 0; Ind++) {
Div = WPrimes[Ind]; // for debug purpose
if (WPrimes[Ind] > Top) {
return Cand;
}
Count++;
if (Cand % WPrimes[Ind] == 0) {
return WPrimes[Ind];
}
}
// start the wheel with encoded primes
Div = 1;
int EInd = 0;
while (Div <= Top) {
unsigned char Flag = EPrimes[EInd];
unsigned char Mask = 1;
// if (Flag == 0) {
if (EInd >= EPrimesLen) {
break;
}
for (long long Ind = 0; Wheel[Ind] != 0; Ind++) {
if (Div > Top) {
break;
}
if (Flag & Mask) {
Count++;
if (Cand % Div == 0) {
return Div;
}
}
Div += Wheel[Ind];
Mask = Mask << 1;
}
EInd++;
}
// Start the Wheel after encoded primes
while (Div <= Top) {
for (long long Ind = 0; Wheel[Ind] != 0; Ind++) {
if (Div > Top) {
return Cand;
}
Count++;
if (Cand % Div == 0) {
return Div;
}
Div += Wheel[Ind];
}
}
return Cand;
}
TrialDivLP.cpp 在 TrialDivLP.zip 中.
编码素数列表
30轮(Wheel 30)有 8 个槽,一个字节有 8 位,所以两者是同步的,这简化了它的使用。每个槽分配一个位。一个 char
的值是这些槽中是素数的槽位的总和。
列表的编码方式如下
0x01 0x02 0x04 0x08 0x10 0x20 0x40 0x80 code
7 11 13 17 19 23 29 0xfe
31 37 41 43 47 53 59 0xdf
61 67 71 73 79 83 89 0xef
97 101 103 107 109 113 0x7e
127 131 137 139 149 0xb6
151 157 163 167 173 179 0xdb
181 191 193 197 199 0x3d
211 223 227 229 233 239 0xf9
241 251 257 263 269 0xd5
271 277 281 283 293 0x4f
307 311 313 317 0x1e
331 337 347 349 353 359 0xf3
367 373 379 383 389 0xea
397 401 409 419 0xa6
421 431 433 439 443 449 0xed
457 461 463 467 479 0x9e
487 491 499 503 509 0xe6
521 523 0x0c
541 547 557 563 569 0xd3
571 577 587 593 599 0xd3
显示列表编码方式的代码如下
void TD_EncodeDtl(int Max) {
long long Wheel[] = { 6, 4, 2, 4, 2, 4, 6, 2, 0 };
long long Cand = 1;
cout << "0x01\t0x02\t0x04\t0x08\t0x10\t0x20\t0x40\t0x80\tcode" << endl;
while (Cand < Max) {
int Code = 0;
int Index = 1;
int Ind = 0;
do {
if ((TD_SRW(Cand) == Cand) && (Cand != 1)) {
Code |= Index;
cout << dec << Cand;
}
cout << "\t";
Cand += Wheel[Ind];
Index = Index << 1;
Ind++;
} while (Wheel[Ind] != 0);
cout << "0x" << setfill('0') << setw(2) << hex << Code << dec << endl;
}
}
TrialDivLP.cpp 在 TrialDivLP.zip 中.
生成数组的代码如下
// encode Prime Numbers as C array
void TD_EncodeArray(int Max) {
long long Wheel[] = { 6, 4, 2, 4, 2, 4, 6, 2, 0 };
long long Cand = 1;
cout << "Encodage liste nombres premiers compressée" << endl;
cout << "{";
while (Cand < Max) {
int Code = 0;
int Index = 1;
int Ind = 0;
do {
if ((TD_SRW(Cand) == Cand) && (Cand != 1)) {
Code |= Index;
}
Cand += Wheel[Ind];
Index = Index << 1;
Ind++;
} while (Wheel[Ind] != 0);
cout << "0x";
cout << setfill('0') << setw(2) << hex << Code << ",";
}
cout << "0}" << dec << endl;
}
TrialDivLP.cpp 在 TrialDivLP.zip 中.
基准测试
我们计算除法/取模次数来衡量变体的效率。编码的素数列表高达 1,000,000,存储在一个 33kB 的数组中。
Number TD_SR TD_SREF TD_SRW TD_SRLPW Delta
1,009 30 16 11 11 0
2,003 43 22 14 14 0
3,001 53 27 17 16 1
4,001 62 32 19 18 1
5,003 69 35 20 19 1
6,007 76 39 23 21 2
7,001 82 42 25 23 2
8,009 88 45 26 24 2
9,001 93 47 27 24 3
10,007 99 50 28 25 3
20,011 140 71 40 34 6
30,011 172 87 49 40 9
40,009 199 100 56 46 10
49,999 222 112 62 48 14
1,000,003 999 500 268 168 100
4,000,021 1,800 901 483 279 204
9,000,011 2,999 1,500 802 430 372
16,000,057 3,999 2,000 1,068 550 518
25,000,009 4,999 2,500 1,336 669 667
36,000,007 5,999 3,000 1,602 783 819
49,000,027 6,999 3,500 1,868 900 968
64,000,031 7,999 4,000 2,136 1,007 1,129
81,000,001 8,999 4,500 2,402 1,117 1,285
100,000,007 9,999 5,000 2,668 1,229 1,439
121,000,003 10,999 5,500 2,936 1,335 1,601
144,000,001 11,999 6,000 3,202 1,438 1,764
169,000,033 12,999 6,500 3,468 1,547 1,921
196,000,001 13,999 7,000 3,736 1,652 2,084
225,000,011 14,999 7,500 4,002 1,754 2,248
256,000,001 15,999 8,000 4,268 1,862 2,406
289,000,001 16,999 8,500 4,536 1,960 2,576
10,000,000,019 99,999 50,000 26,668 9,592 17,076
1,000,000,000,039 999,999 500,000 266,668 78,498 188,170
Delta 用于突出庞大素数列表带来的节省。
long long Test[] = { 101, 1009, 2003, 3001, 4001, 5003, 6007, 7001, 8009,
9001, 10007, 20011, 30011, 40009, 49999, 1000003, 4000021, 9000011,
16000057, 25000009, 36000007, 49000027, 64000031, 81000001,
100000007, 121000003, 144000001, 169000033, 196000001, 225000011,
256000001, 289000001, 10000000019, 1000000000039, 0 };
cout << "Comparaison Variantes" << endl;
cout << "Number\tTD_SR\tTD_SREF\tTD_SRW\tTD_SRLPW\tDelta" << endl;
// test after first 0
for (Ind = 0; Test[Ind] != 0; Ind++) {
cout << Test[Ind] << "\t";
TD_SR(Test[Ind]);
cout << Count << "\t";
TD_SREF(Test[Ind]);
cout << Count << "\t";
TD_SRW(Test[Ind]);
int C1 = Count;
cout << Count << "\t";
TD_SRLPW(Test[Ind]);
cout << Count << "\t";
cout << C1 - Count << endl;
}
cout << endl;
TrialDivLP.cpp 在 TrialDivLP.zip 中.
128位整数
GCC 通过使用 2 个 64 位寄存器来提供 128 位变量的支持,这对于计算来说没问题,但不支持作为源代码中的常量或用于 I/O 操作。需要一些编程来绕过这些限制。
//#define int64
// GCC have provisions to handle 128 bits integers, but not for constants in source code
// and not IO
#ifdef int64
// 64 bits integer
typedef long long MyBigInt;
void output(MyBigInt Num, int Size) {
cout << setfill(' ') << setw(Size);
}
#else
// 128 bits integer
typedef __int128 MyBigInt;
void output(MyBigInt Num, int Size) {
if (Num <= (long long) 0x7fffffffffffffff) {
if (Size > 0) {
cout << setfill(' ') << setw(Size);
}
cout << (long long) Num;
} else {
output(Num / 1000, Size - 4);
long long Tmp = Num % 1000;
cout << "," << setfill('0') << setw(3) << Tmp;
}
}
#endif
TrialDivLP.cpp 在 TrialDivLP.zip 中.
Number Fartor Timing
10,000,000,000,000,061 1 365 ms
100,000,000,000,000,003 1 1,173 ms
1,000,000,000,000,000,003 1 3,866 ms
10,000,000,000,000,000,051 1 11,988 ms
100,000,000,000,000,000,039 1 82,326 ms
1,000,000,000,000,000,000,117 1 272,376 ms
每次数字乘以 10,运行时间大约乘以 3(sqrt(10))。注意当数字跨越 64 位边界时运行时间的差距。
#ifndef int64
long long Test2[][3] = { { 10, 16, 61 }, { 10, 17, 3 }, { 10, 18, 3 }, { 10,
19, 51 }, { 10, 20, 39 }, { 10, 21, 117 }, { 0, 0, 0 } };
// for each triplet A B C, the number is
// Cand= A ^ B + C
cout.imbue(locale(cout.getloc(), new gr3));
for (Ind = 0; Test2[Ind][0] > 0; Ind++) {
for (int Offset = 0; Offset < 1000; Offset += 2) {
MyBigInt Cand = 1;
for (int Scan = 0; Scan < Test2[Ind][1]; Scan++) {
Cand *= Test2[Ind][0];
}
Cand += Test2[Ind][2] + Offset;
output(Cand, 30);
cout << "\t";
auto start2 = high_resolution_clock::now();
long long Tmp = TD_SRLPW(Cand);
auto stop2 = high_resolution_clock::now();
if (Tmp == 1) {
output(Tmp, 0);
} else {
cout << Tmp;
cout << "\t";
output((Cand / Tmp) * Tmp, 0);
}
cout << "\t";
auto duration = duration_cast<milliseconds>(stop2 - start2);
cout << " " << duration.count() << " ms";
cout << endl;
if (Tmp == 1) {
break;
}
}
}
cout.imbue(locale(cout.getloc(), new gr0));
#endif
TrialDivLP.cpp 在 TrialDivLP.zip 中.
使用压缩的素数列表加速 IsPrime 判断
我们可以利用庞大的素数列表并将其用作查找表。
使用普通列表,我们需要进行二分查找。
使用编码列表,几乎可以进行直接读取。
// IsPrime taking advantage of long list of primes table
// returns 1 if prime, returns 0 otherwise
int IsPrimeLP(MyBigInt Cand) {
const unsigned long long Wheel[] = { 6, 4, 2, 4, 2, 4, 6, 2, 0 };
const unsigned long long WOffset[] = { 1, 7, 11, 13, 17, 19, 23, 29, 0 };
if (Cand <= EPrimesMax) {
Count = 1;
// Cand is within list of primes
long long x = Cand / WSize; // position in wheel list
long long y = Cand % WSize; // offset in wheel
for (int Index = 0; WOffset[Index] != 0; Index++) {
if (WOffset[Index] == y) {
return ((EPrimes[x] >> Index) & 0x01);
}
}
if (Cand == 2)
return 1;
else if (Cand == 3)
return 1;
else if (Cand == 5)
return 1;
else
return 0; // multiple of 2, 3 or 5
}
// Search a Factor
return (TD_SRLPW(Cand) == 1);
}
TrialDivLP.cpp 在 TrialDivLP.zip 中.
关注点
随着素数列表的增大,试除法算法会得到改进,而压缩技术在这方面帮助很大。
这项工作是原创的。如果您知道类似的工作,请在下面的论坛中提供链接。
链接
历史
- 2020年12月18日:初版
- 2020年12月25日:第二版 - 修正和改进
- 2021年2月6日:添加 IsPrime 代码 + 修正
- 2022年8月7日:添加目录
- 2022年8月20日:源代码中的拼写错误