65.9K
CodeProject 正在变化。 阅读更多。
Home

整数分解:试除算法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (10投票s)

2019年5月30日

CPOL

6分钟阅读

viewsIcon

42597

downloadIcon

503

试除算法的小回顾

目录

  1. 背景
  2. 引言
  3. 试除法:经典变体
    1. 暴力破解
    2. 基本版本:平方根
    3. 去除偶数因子
    4. 轮子
    5. 素数列表
  4. 基准测试
    1. 基准测试 1
    2. 基准测试 2
  5. 检查变体正确性
  6. 关注点
  7. 链接
  8. 历史

背景

本文是系列文章的一部分。每篇文章都着重介绍整数分解的某个方面。

xPrompt 下载和 prg 文件

xPrompt.zip 包含 xHarbour Scripting Engine for Windows 的二进制文件。这是运行 prg 代码所需的应用程序。

用法

  • prg 文件复制到 xPrompt 的同一目录下
  • 启动 xPrompt
  • 输入 "do FileName.prg" 来运行代码

xHarbour 是免费软件,可在 xHarbour.org[^] 下载

xHarbour 是 xBase 系列语言的一部分:xBase - Wikipedia[^]

流程图

流程图与本文相关:使用树形图支持的自顶向下分析方法[^]

如果图片太小,请在新标签页中打开并缩放,这是一个 svg 文件。

引言

在开始玩弄整数分解时,尝试所有可能的因子是第一个想法,该算法称为 **试除法**。

该算法有两个目的——找到一个素数因子,或者通过不找到素数因子来判断一个整数是否为素数。

由于该算法是关于查找因子的,所以最坏的情况是待分解的整数是素数。

试除法:经典变体

很明显,最有效的方法是只检查素数,但处理素数列表本身就是一个问题。经典的变体是为了避免这个问题而存在的。随着方法的不断完善,它们会去除更多无用的非素数,从而更有效。

暴力破解

一些新手会测试小于 n 的所有单个数字。这是多余的。只要待分解的整数不是素数,低效性就会隐藏起来。

n 的最大成本是 n-2 次除法。复杂度为 O(n)。

// Trial Division: Brute Force 1
// Check all numbers until Cand - 1
long long TD_BF1(long long Cand) {
    Count = 0;
    long long Top = Cand - 1;
    for (long long Div = 2; Div <= Top; Div++) {
        Count++;
        if (Cand % Div == 0) {
            return Div;
        }
    }
    return Cand;
}
//  Trial Division Brute Force 1
// May 2019
function TD_BF1(Prod)
    Local D, Top
    Top= Prod-1
    for D= 2 to Top
        if Prod % D = 0
            return D
        endif
    next
    return Prod

在 TrialDiv.cpp 和 TrialDiv.prg 中,位于 TrialDiv.zip 内.

TD_BF1.graphml 和 TD_BF1.svg 在 TrialDiv.zip 内.

测试直到 n/2 的所有数字会更好,但同样是多余的。就像以前的方法一样,只要待分解的整数不是素数,低效性就会隐藏起来。

n 的最大成本是 n / 2 次除法。复杂度为 O(n)。

// Trial Division: Brute Force 2
// Check all numbers until Cand / 2
long long TD_BF2(long long Cand) {
    Count = 0;
    long long Top = Cand / 2;
    for (long long Div = 2; Div <= Top; Div++) {
        Count++;
        if (Cand % Div == 0) {
            return Div;
        }
    }
    return Cand;
}
//  Trial Division Brute Force 2
// May 2019
function TD_BF2(Prod)
    Local D, Top
    Top= Prod/2
    for D= 2 to Top
        if Prod % D = 0
            return D
        endif
    next
    return Prod

在 TrialDiv.cpp 和 TrialDiv.prg 中,位于 TrialDiv.zip 内.

TD_BF2.graphml 和 TD_BF2.svg 在 TrialDiv.zip 内.

基本版本:平方根

稍微分析一下就知道,平方根之后就没有需要检查的因子了。

n 的最大成本是 √n 次除法。复杂度为 O(√n)。

// Trial Division: Square Root
// Check all numbers until Square Root
long long TD_SR(long long Cand) {
    Count = 0;
    long long Top = sqrt(Cand);
    for (long long Div = 2; Div <= Top; Div++) {
        Count++;
        if (Cand % Div == 0) {
            return Div;
        }
    }
    return Cand;
}
//  Trial Division Square Root
// May 2019
function TD_SR(Prod)
    Local D, Top
    Top= int(sqrt(Prod))
    for D= 2 to Top
        if Prod % D = 0
            return D
        endif
    next
    return Prod

在 TrialDiv.cpp 和 TrialDiv.prg 中,位于 TrialDiv.zip 内.

TD_SR.graphml 和 TD_SR.svg 在 TrialDiv.zip 内.

去除偶数因子

进一步分析可知,除了 2 之外,不会有偶数因子。

n 的最大成本是 (√n) * 1 / 2 => (√n) * 0.50 次除法。复杂度为 O(√n)。

// Trial Division: Square Root + Even Factors
// Check all numbers until Square Root and Skip Even Factors
long long TD_SREF(long long Cand) {
    Count = 0;
    // check 2 the only Even Prime
    Count++;
    if (Cand % 2 == 0) {
        return 2;
    }
    long long Top = sqrt(Cand);
    for (long long Div = 3; Div <= Top; Div += 2) {
        Count++;
        if (Cand % Div == 0) {
            return Div;
        }
    }
    return Cand;
}
//  Trial Division Square Root + Even Factors
// May 2019
function TD_SREF(Prod)
    Local D, Top
    if Prod % 2 = 0
        return 2
    endif
    Top= int(sqrt(Prod))
    for D= 3 to Top step 2
        if Prod % D = 0
            return D
        endif
    next
    return Prod

在 TrialDiv.cpp 和 TrialDiv.prg 中,位于 TrialDiv.zip 内.

TD_SREF.graphml 和 TD_SREF.svg 在 TrialDiv.zip 内.

轮子

轮子是先前优化的扩展。轮子由小的素数(例如 2 和 3)构建,轮子的大小是 2 * 3 = 6。当在一张 6 列的表格中写下一系列数字并去掉 2 和 3 时,可以看出素数只出现在第一列和第五列。这就是轮子,我们只需要检查第 1 列和第 5 列的数字。

1 2 3 4 5 6
7 8 9 10 11 12
13 14 15 16 17 18
19 20 21 22 23 24
25 26 27 28 29 30
31 32 33 34 35 36
37 38 39 40 41 42
43 44 45 46 47 48
49 50 51 52 53 54
55 56 57 58 59 60

n 的最大成本是 (√n) * (1 / 2) * (2 / 3) => (√n) * (1 / 3) => (√n) * 0.33 次除法。复杂度为 O(√n)。

    //  Trial Division Square Root + Wheel
    // May 2019
<pre lang="c++">
// Trial Division: Square Root + Wheel
// Check all numbers until Square Root and Skip useless factors with a wheel
long long TD_SRW(long long Cand) {
    long long SPrimes[] = { 2, 3, 0 };
    long long Wheel[] = { 4, 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 = 1;
    while (Div <= Top) {
        for (long long Ind = 0; Wheel[Ind] != 0; Ind++) {
            Div += Wheel[Ind];
            if (Div > Top) {
                break;
            }
            Count++;
            if (Cand % Div == 0) {
                return Div;
            }
        }
    }
    return Cand;
}
function TD_SRW(Prod)
	local D, Top, SPrimes, Wheel, W
	// Check small primes
	SPrimes= {2, 3}
	Wheel= {4, 2}
	for each D in SPrimes
		if Prod % D = 0
			return D
		endif
	next
	// Start the wheel
	D= 1
	Top= int(sqrt(Prod))
	while D <= Top
		for each W in wheel
			D += W
			if Prod % D = 0
				return D
			endif
		next
	enddo
	return Prod

在 TrialDiv.cpp 和 TrialDiv.prg 中,位于 TrialDiv.zip 内.

TD_SRW.graphml 和 TD_SRW.svg 在 TrialDiv.zip 内.

轮子可以包含更多素数。对于 2、3 和 5,轮子的大小是 30。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90

n 的最大成本是 (√n) * (1 / 2) * (2 / 3) * (4 / 5) => (√n) * (4 / 15) => (√n) * 0.27 次除法。复杂度为 O(√n)。

    long long SPrimes[] = { 2, 3, 5, 0 };
    long long Wheel[] = { 6, 4, 2, 4, 2, 4, 6, 2, 0 };
    // only those 2 lines need change
    SPrimes= {2, 3, 5}
    Wheel= {6, 4, 2, 4, 2, 4, 6, 2}

或者更大。对于 2、3、5 和 7,轮子的大小是 210。

n 的最大成本是 (√n) * (1 / 2) * (2 / 3) * (4 / 5) * (6 / 7) => (√n) * (24 / 105) => (√n) * 0.23 次除法。复杂度为 O(√n)。

素数列表

基本上,它是一个轮子变体,其中小素数列表超出了轮子所需的数量,当列表结束时,我们会回到轮子。与简单轮子的优点是,它避免了测试轮子未过滤的非素数因子。只要我们在素数列表中,工作量就是最佳的。

只需注意在素数列表末尾正确设置轮子。

n 的最大成本略好于轮子,素数列表只是节省了轮子中的非素数除数。因此,素数列表越长,效果越好。复杂度为 O(√n)。

// 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, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661,
			673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757,
			761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853,
			857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941,
			947, 953, 967, 971, 977, 983, 991, 997, 1009, 1013, 1019, 1021,
			1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, 1093,
			1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, 1171, 1181,
			1187, 1193, 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249, 1259,
			1277, 1279, 1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321,
			1327, 1361, 1367, 1373, 1381, 1399, 1409, 1423, 1427, 1429, 1433,
			1439, 1447, 1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493,
			1499, 1511, 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579,
			1583, 1597, 1601, 1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657,
			1663, 1667, 1669, 1693, 1697, 1699, 1709, 1721, 1723, 1733, 1741,
			1747, 1753, 1759, 1777, 1783, 1787, 1789, 1801, 1811, 1823, 1831,
			1847, 1861, 1867, 1871, 1873, 1877, 1879, 1889, 1901, 1907, 1913,
			1931, 1933, 1949, 1951, 1973, 1979, 1987, 1993, 1997, 1999, 2003,
			2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069, 2081, 2083, 2087,
			2089, 2099, 2111, 2113, 2129, 2131, 2137, 2141, 2143, 2153, 2161,
			2179, 2203, 2207, 2213, 2221, 2237, 2239, 2243, 2251, 2267, 2269,
			2273, 2281, 2287, 2293, 2297, 2309, 2311, 2333, 2339, 2341, 2347,
			2351, 2357, 2371, 2377, 2381, 2383, 2389, 2393, 2399, 2411, 2417,
			2423, 2437, 2441, 2447, 2459, 2467, 2473, 2477, 2503, 2521, 2531,
			2539, 2543, 2549, 2551, 2557, 2579, 2591, 2593, 2609, 2617, 2621,
			2633, 2647, 2657, 2659, 2663, 2671, 2677, 2683, 2687, 2689, 2693,
			2699, 2707, 2711, 2713, 2719, 2729, 2731, 2741, 2749, 2753, 2767,
			2777, 2789, 2791, 2797, 2801, 2803, 2819, 2833, 2837, 2843, 2851,
			2857, 2861, 2879, 2887, 2897, 2903, 2909, 2917, 2927, 2939, 2953,
			2957, 2963, 2969, 2971, 2999, 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 = 3001;
	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;
}
    //  Trial Division Square Root + Wheel + list of primes
    // with a large list of primes
    // May 2019
function TD_SRW2(Prod)
    local D, Top, SPrimes, Wheel, W
    // Check small primes
    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}
    Wheel= {6, 4, 2, 4, 2, 4, 6, 2}
    
    for each D in SPrimes
        if Prod % D = 0
            return D
        endif
    next
    // Start the wheel
    D= 301
    Top= int(sqrt(Prod))
    while D <= Top
        for each W in wheel
            D += W
            if Prod % D = 0
                return D
            endif
        next
    enddo
    return Prod

在 TrialDiv.cpp 和 TrialDiv.prg 中,位于 TrialDiv.zip 内.

基准测试

基准测试是为了突出变体的效率。

由于除法/模运算是耗时的,因此计算它们足以进行基准测试。

基准测试 1

第一个基准测试是为了强调暴力破解变体的低效性。

long long Test[] = { 11, 31, 53, 71, 97, 0, 101, 1009, 2003, 3001, 4001,
		5003, 6007, 7001, 8009, 9001, 10007, 20011, 30011, 40009, 49999,
		1000003, 4000021, 9000011, 0 };
cout << "Comparaison Variantes avec Brute Force" << endl;
cout << "Number\tTD_BF1\tTD_BF2\tTD_SR\tTD_SREF\tTD_SRW\tTD_SRPW" << endl;
for (Ind = 0; Test[Ind] != 0; Ind++) {
	cout << Test[Ind] << "\t";
	TD_BF1(Test[Ind]);
	cout << Count << "\t";
	TD_BF2(Test[Ind]);
	cout << Count << "\t";
	TD_SR(Test[Ind]);
	cout << Count << "\t";
	TD_SREF(Test[Ind]);
	cout << Count << "\t";
	TD_SRW(Test[Ind]);
	cout << Count << "\t";
	TD_SRPW(Test[Ind]);
	cout << Count << endl;
}
cout << endl;
Test = { 11, 31, 53, 71, 97, 101, 1009, 2003, 3001, 4001, 5003, 6007, 7001, 8009, 9001, 10007, 20011, 30011, 40009, 49999, 1000003, 4000021, 9000011 }
? "Comparaison Variantes avec Brute Force"
? "Number","TD_BF1","TD_BF2","TD_SR","TD_SREF","TD_SRW","TD_SRPW"
for Ind = 1 to 5
	? Test[Ind]
	TD_BF1(Test[Ind])
	?? Count
	TD_BF2(Test[Ind])
	?? Count
	TD_SR(Test[Ind])
	?? Count
	TD_SREF(Test[Ind])
	?? Count
	TD_SRW(Test[Ind])
	?? Count
	TD_SRPW(Test[Ind])
	?? Count
next
?
Number  TD_BF1  TD_BF2  TD_SR   TD_SREF TD_SRW  TD_SRPW
11      9       4       2       2       2       2
31      29      14      4       3       3       3
53      51      25      6       4       4       4
71      69      34      7       4       4       4
97      95      47      8       5       4       4

基准测试 2

这个扩展基准测试用于暴力破解之外的变体。

	cout << "Comparaison Variantes sans Brute Force" << endl;
	cout << "Number\tTD_SR\tTD_SREF\tTD_SRW\tTD_SRPW\tDelta" << endl;
// test after first 0
	for (Ind++; 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_SRPW(Test[Ind]);
		cout << Count << "\t";
		cout << C1 - Count << endl;
	}
	cout << endl;
? "Number","TD_SR","TD_SREF","TD_SRW","TD_SRPW"
for Ind = 6 to len(Test)
	? Test[Ind]
	TD_SR(Test[Ind])
	?? Count
	TD_SREF(Test[Ind])
	?? Count
	TD_SRW(Test[Ind])
	?? Count
	TD_SRPW(Test[Ind])
	?? Count
next
?
Comparison Variants sans Brute Force
Number	TD_SR	TD_SREF	TD_SRW	TD_SRPW	Delta
101		9		5		4		4		0
1009	30		16		11		11		0
2003	43		22		14		14		0
3001	53		27		17		16		1
4001	62		32		19		18		1
5003	69		35		20		19		1
6007	76		39		23		21		2
7001	82		42		25		23		2
8009	88		45		26		24		2
9001	93		47		27		24		3
10007	99		50		28		25		3
20011	140		71		40		34		6
30011	172		87		49		40		9
40009	199		100		56		46		10
49999	222		112		62		48		14
1000003	999		500		268		168		100
4000021	1800	901		483		279		204
9000011	2999	1500	802		430		372

Delta 显示了素数列表如何改进性能,并且随着素数列表的增长而变得更好。

检查变体正确性

Rgis 代码通过比较变体结果来检查代码的正确性。

	cout << "Vérification Variantes" << endl;
	for (long long Cand = 3; Cand < 10000000; Cand += 10) {
		long long d1 = TD_SREF(Cand);
		long long d2 = TD_SRPW(Cand);
		if (d1 != d2) {
			cout << Cand << "\t" << d1 << "\t" << d2 << endl;
		}
	}

关注点

试除法是暴力破解,因此可以看出暴力破解也有好坏之分。

历史

  • 2019年4月1日:第一个版本
  • 2020年11月27日:第二个版本
  • 2020年12月20日:第三个版本:一些清理,将下载移至顶部
  • 2020年12月25日:第四个版本:一些清理和更正
  • 2021年1月31日:更正
  • 2022年1月30日:添加目录和少量更新
  • 2023年9月16日:添加流程图和更正。
  • 2020年12月15日:拼写错误
© . All rights reserved.