棒球如何激发对软件编码优化的追求






4.93/5 (9投票s)
寻找高效算法以找到 Ruth-Aaron 对
背景
我的故事始于1895年2月6日,在马里兰州巴尔的摩。那天,一个名叫小乔治·赫尔曼·鲁斯的男婴出生了。当时,没有人能预料到他将获得的声誉。后来他被昵称为“小可爱”、“全垒打之王”和“贝比”,他开始在棒球比赛中展现出精湛的技艺。1914年7月11日,19岁的他以波士顿红袜队投手身份首次亮相大联盟。但由于渴望更多的上场时间,他被允许转为外野手。1919年,他打破了美国职业棒球大联盟(MLB)单赛季本垒打纪录。1920年,鲁斯被卖到纽约洋基队,在那里他继续打出本垒打。到1935年他退役时,他生涯累计打出714支本垒打,这一纪录似乎不可能被打破。
我故事的下一部分始于1934年2月5日,在阿拉巴马州莫比尔,距离贝比·鲁斯出生差一天39年,也是他为纽约洋基队效力的最后一年。(他还在1935年为波士顿勇士队效力了部分时间。)那天,一个名叫亨利·路易斯·“汉克”·阿伦的男婴出生了。他也被称为“铁锤”或“重锤汉克”,于1954年4月13日,20岁时首次亮相大联盟。在接下来的22年里,他将效力于密尔沃基勇士队(接替鲁斯离开勇士队的位置)、亚特兰大勇士队和密尔沃基酿酒人队。阿伦和鲁斯一样是本垒打好手。到1973赛季结束时,他已经打出了713支生涯本垒打,距离鲁斯38年前创下的纪录只差一支。
你可以想象1973年和1974赛季之间亚特兰大的狂热,许多球迷都在期待汉克打破贝比的生涯本垒打纪录。你可以问,“715什么时候发生?”所有人都知道你在说什么。事实证明,1974年4月8日就是这个问题的答案,阿伦打出他职业生涯的第715支本垒打,超越了鲁斯的纪录。
注意到这种兴奋,尤其是这些数字的人,是佐治亚大学一位名叫卡尔·波默兰斯的年轻数学助理教授。他注意到714和715的乘积也是前七个素数的乘积。然后,一位同事的学生发现714的素因子之和等于715的素因子之和。素数只能被1和它本身整除。以下是这些信息的概要。
2 x 3 x 5 x 7 x 11 x 13 x 17 = 714 x 715 = 510,510
714 = 2 x 3 x 7 x 17
715 = 5 x 11 x 13
Sum of 714’s prime factors = 2 + 3 + 7 + 17 = 29
Sum of 715’s prime factors = 5 + 11 + 13 = 29
有趣的是,素因子之和29也是一个素数,而714和715的乘积有趣在于它是一个重复的510。或许一个代表鲁斯,一个代表阿伦?510的素因子是2、3、5和17,它们加起来是27。棒球迷可能知道,4月27日被称为国家贝比·鲁斯日。正是在1947年的那天,鲁斯在洋基体育场对近60,000名观众发表演讲,当时他正忍受着癌症的痛苦。关于汉克·阿伦,维基百科说1955年标志着他职业生涯的“巅峰”开始。那一年,他打出27支本垒打,并获得了各种荣誉。巧合吗?
事实证明,两个连续整数具有相同素因子之和的情况非常罕见。它们被称为鲁斯-阿伦对。更罕见的是,一个鲁斯-阿伦对的组合素因子是连续的。事实上,除了平凡的5-6对(其素因子为2、3和5)之外,714-715组合似乎在这方面是独一无二的。我称其为平凡,因为5只有一个素因子。顺便说一句,对于真正的极客们,请注意汉克·阿伦出生在当月的5日,而贝比·鲁斯出生在当月的6日。所以,也许独特的5-6对应该被称为阿伦-鲁斯对。对于极端极客来说,请注意鲁斯和阿伦都出生在一年中的第二个月,而2是第一个素数。
我与鲁斯-阿伦对的初次邂逅
我第一次读到鲁斯-阿伦对是在一位朋友借给我的一本书中,那本书是他花75美分在一家旧书店买的。那本书是保罗·霍夫曼的《只爱数字的人:保罗·埃尔德什与数学真理的探索故事》。我被鲁斯-阿伦对的想法深深吸引,决定用我的电脑亲自寻找这些对。这是我长达两个月的编程优化探索的开始。尽管寻找鲁斯-阿伦对的过程很简单,但以最有效的方式进行却并非易事。
首先,让我为您概述一下我用来寻找鲁斯-阿伦对的算法。
- 从2开始遍历整数,将此值设置为鲁斯数候选。
- 将鲁斯数候选值加1以获得阿伦数候选值。
- 找出鲁斯数候选值的素因子并求和。
- 找出阿伦数候选值的素因子并求和。
- 如果两个和相等,则这些数字构成一个鲁斯-阿伦对。
- 此外,可以检查素因子是否唯一,即在鲁斯-阿伦对内部和之间没有重复的因子。
- 进一步地,如果素因子是唯一的,还可以检查它们是否连续。
使用Excel
我的搜索始于使用Microsoft Excel 365及其内置编程语言——Visual Basic for Applications (VBA)。我暂时不会深入探讨编程细节,因为整个过程充满了曲折。现在,我只想让您了解我尝试过的一些方法以及它们的表现。
首先声明,我的台式电脑虽然已经六年了,但性能依然相当强大。它配备了英特尔四核i7-3770K 3.50 GHz CPU,超频至4.20 GHz,并配有液体冷却,以及32 GB DDR3/1600MHz双通道内存。它运行的是最新版本的Windows 10。
鉴于鲁斯-阿伦对的搜索涉及素数,因此我需要一个素数列表作为起点是合乎逻辑的。我最初自己生成素数并将它们放在Excel工作簿的某个工作表上。
我设置了另一个工作表,可以在其中选择起始和结束的鲁斯数。当程序找到鲁斯-阿伦对时,有关这些对的数据会添加到该工作表中,如下所示。`Time`列让我能够确定程序运行的速度。
寻找每个候选鲁斯数和阿伦数的素因子的算法如下:
- 检查鲁斯或阿伦候选数是否为素数。如果是,则唯一的素因子就是它本身。完成。
- 如果候选数不是素数,则从第一个素数2开始。
- 如果候选数能被该素数整除,则将其添加到因子数组中。将除法的商作为新的待测试数。
- 继续测试相同的素数,每次能被整除时都将其再次添加到因子数组中。每次都将商作为新的待测试数。
- 当该素数不能再整除该数时,移至下一个素数。
- 重复步骤3-5,直到商本身是素数,并且除了它本身之外不能被任何数整除。将这个最终的商添加到因子数组中。
- 因子数组现在将包含原始数字的所有素因子。
您可能认为在步骤1中确定一个数字是否为素数只需搜索素数列表,如果找到,则将候选数字标记为素数。但事实证明这非常慢,特别是对于大数字,即使素数首先被读入数组。更好的方法是开始用素数(从2开始)除候选数字。如果只有一个素数能整除候选数字,则它不是素数。然而,如果达到一个值大于候选数字一半的素数,并且没有一个能整除候选数字,那么候选数字本身就是素数。
使用这些寻找素数和鲁斯-阿伦对的方法,我生成了前一百万个整数的鲁斯-阿伦对,并为了比较速度,还生成了900万到1000万之间一百万个整数的鲁斯-阿伦对。以下是结果:
搜索鲁斯-阿伦对的数值范围 | 完成时间 |
2 – 1,000,000 | 0 小时 31 分 25 秒 |
9,000,001 – 10,000,000 | 4 小时 44 分 33 秒 |
正如预期的那样,对于较大的数字,寻找鲁斯-阿伦对的时间显著增加。这是由于测试素数和寻找素因子所需的计算量大大增加。
我使用Windows任务管理器查看Excel程序运行时我的CPU处理能力使用了多少。它只有大约18%。我想知道是否可以通过同时运行多个Excel程序实例来加快速度。由于我有一个四核CPU,每个核心能够处理两个超线程,我决定八个实例是我可以运行的最大数量,并且仍然能够获得速度提升。当我测试两个实例时,CPU使用率上升到36%,这令人鼓舞。但对于三个或更多实例,它稳定在约45-50%,速度提升不大。而且设置所有实例并分配它们之间的任务非常麻烦。
使用Visual Basic .NET
此时,我决定为了获得更高的速度,我必须将程序从VBA移植到Visual Basic .NET,因为VBA不像VB.NET那样完全编译。我在Visual Basic 6及更早版本时代编写过很多程序,但在VB.NET方面很少。管他呢,这是一个学习的机会。我的电脑上安装了VB.NET 2017社区版,所以我将现有的VBA程序复制到VB.NET程序中。我必须构建自己的界面并修改代码才能让一切正常工作。我最终决定素数需要从文本文件而不是Excel工作表加载到数组中,但我仍然认为使用Excel导出鲁斯-阿伦对搜索的最终结果是一个不错的选择。一切运行就绪后,我运行了一个测试,与之前VBA程序的结果进行比较。
搜索鲁斯-阿伦对的数值范围 | 完成时间 |
2 – 1,000,000 | 0 小时 1 分 31 秒 |
这意味着VB.NET程序比VBA程序快了20多倍,而且没有对代码进行任何过程性修改。太棒了!但我的速度需求尚未满足。
多线程
进一步提高搜索速度,我首先想到的是将搜索分解成多个任务并使用多线程。这与使用多个Excel实例类似,但实现起来要容易得多。多线程是我一直很着迷但从未在任何程序中实际实现过的功能。所以,我做了一些研究。然而,一旦理解,它真的非常简单。最困难的部分是如何在线程之间分配搜索任务。再一次,由于我的CPU可以处理八个超线程,我猜测八个线程将是一个很好的最大值。然而,为了安全起见,我允许程序运行多达16个。在进行代码更改以处理多线程后,我获得了以下结果。
线程数 | 从2到1,000,000寻找鲁斯-阿伦对的时间 |
1 | 1 分 36 秒 |
2 | 0 分 43 秒 |
3 | 0 分 38 秒 |
4 | 0 分 28 秒 |
5 | 0 分 25 秒 |
6 | 0 分 24 秒 |
7 | 0 分 21 秒 |
8 | 0 分 19 秒 |
9 – 16 | 0 分 19 秒 |
正如您所见,我的直觉是正确的。八个线程使速度最大化。更多的线程没有增加额外的速度。我还发现,在八个或更多线程时,CPU利用率达到了100%。我注意到,当运行一个线程时,速度比不使用线程时慢了一点。我猜这是由于线程相关的开销造成的。但是,使用多个线程带来的速度提升完全弥补了开销造成的减慢。
由于寻找鲁斯-阿伦对的速度随着数字的增大而降低,我编写程序使其自动分割搜索,以便每个线程都同时处理小数字和大数字。例如,如果我选择运行四个线程,工作将按以下方式划分:
线程1对搜索 | 线程2对搜索 | 线程3对搜索 | 线程4对搜索 |
(2,3), (6,7), (10,11), … | (3,4), (7,8), (11,12), … | (4,5), (8,9), (12,13), … | (5,6), (9,10), (13,14), … |
通过这种方式划分工作,每个线程需要执行的任务量几乎相同。但事实证明,有些线程运行得更快,因为有些对需要较少的测试工作。我可以通过程序界面上的16个进度条来跟踪每个线程的进度。由于有些线程比其他线程运行得快,因此会提前完成,所以拥有16个线程是有用的。通过这么多的初始线程,超过一半的工作可以在速度下降之前完成(八个线程完成)。如果最初只运行八个线程,效率会从第一个完成的线程开始下降。因此,拥有16个可用线程被证明是有价值的。
素数测试
回顾一下,我最初在Excel中使用VBA构建了我的鲁斯-阿伦对软件,搜索前一百万个潜在对大约需要31.5分钟。然后我将程序转换为VB.NET并添加了多线程。使用八个或更多线程将前一百万个潜在对的搜索时间缩短到19秒,速度提高了近100倍。还不错。但再次地,我并不满意。我相信一定有办法让搜索更快。由于鲁斯-阿伦对程序大部分时间都花在确定鲁斯和阿伦候选数是否为素数或寻找这些候选数的素因子上,我非常确定这就是我需要开始寻找更优化算法的地方。
我注意到VB.NET中的`Array`类有一个名为`IndexOf`的方法,可以搜索数组中的一个值。我以为如果这个方法以某种方式进行了优化,或许可以用它来通过在素数数组中搜索被测试的数字来确定一个数字是否为素数。实际上,`IndexOf`方法非常慢。
确定数字是否为素数的方法
我开始思考我目前使用的算法,即从最小的素数2开始,然后不断遍历素数,看是否有任何素数能整除被测试的数字。搜索将在数字的一半处停止,因为没有任何数字能被大于其自身一半的数字整除。以下是用于此测试的代码。
Private Function IsPrime(n As Integer) As Boolean
'n/2 Method
Dim i As Integer
Dim endVal As Integer = n / 2
'loop through prime array, if n is evenly divisible
'by one of these primes, then n is not prime
i = 0
Do While PrimeArray(i) <= endVal
If n Mod PrimeArray(i) = 0 Then
Return False
Else
i += 1
End If
Loop
Return True
End Function
当我思考如何将可整除素数的搜索限制在n/2时,我突然意识到,一旦检查过3的可除性,就不需要检查任何大于n/3的素数。一旦检查过5,就不需要检查任何大于n/5的素数。那么,不需要检查的最小临界值是多少呢?它将是正在检查的素数p等于n/p的情况。要找到那个点,只需将p设为n/p并求解p。
p = n / p
p^2 = n
p = sqrt(n)
啊哈!一切都说得通了。没有必要检查任何超过被测试数字平方根的素数是否能被整除。因为如果在此点之后有任何素数能整除被测试数字,那么必然会有一个或多个小于平方根的素数与其相乘才能得到该数字。而那些素数在此之前就已经被测试过了。
为了实现这个算法,我只需将上面的代码修改为将`endVal`设为`Sqrt(n)`而不是`n/2`。另一种获取一个数字平方根的方法是取该数字的10为底的对数,将其除以2,然后将10提升到结果,即10^(Log10(n)/2)。我想也许这种计算可能比使用平方根函数更快。
我需要测试所有这些不同的寻找素数的方法,看看每种方法的运行速度。我为此编写了一个单独的程序。我创建了一个文本文件,其中包含2到100,000,000之间的所有素数,可以加载到数组中用于测试不同的算法。(**注意**:创建素数生成器很容易,但它们也可以在线获得。)为了与`IndexOf`方法进行比较,我编写了自己的例程来直接搜索数组。在这个例程中,我首先通过检查n模2来排除偶数,然后搜索素数数组中被测试的数字,当找到数字或数组值大于该数字时停止搜索。以下是结果:
用于确定数字是否为素数的方法 1 | 测试从2到10,000的数字所需时间 | 测试从2到1,000,000的数字所需时间 | 测试从2到100,000,000的数字所需时间 |
---|---|---|---|
IndexOf | 15.839 秒 | 太长 | 太久了 |
直接数组搜索 | 0.007 秒 | 39.287 秒 | 太长 |
搜索到n/2 | 0.002 秒 | 4.576 秒 | 太长 |
搜索到10^(Log10(n)/2) | 0.001 秒 | 0.104 秒 | 24.193 秒 |
搜索到Sqrt(n) | 0.001 秒 | 0.053 秒 | 19.191 秒 |
1 加载包含前1亿个整数中素数的文件耗时1.508秒。
正如您所见,搜索到被测试数字的平方根以内的可整除素数的方法确实是最快的。然而,我仍然觉得一定还有另一种更快的方法。我又想出了两个额外的想法来尝试。
更多判断数字是否为素数的方法
一个想法是预先确定所有高达1亿的整数的素性,并将这些信息存储在一个文件中。该文件可以加载到数组中,这样要判断一个数字是否为素数,只需检查由被测试数字表示的索引处的数组值即可。例如,假设数组由布尔值组成。它将看起来像这样:
a(0) = False
a(1) = False
a(2) = True
a(3) = True
a(4) = False
a(5) = True
a(6) = False
a(7) = True
…
使用这个数组,测试一个数字的素性非常简单。
Private Function IsPrime(n as Integer) as Boolean
Return a(n)
End Function
事实上,它非常简单,甚至不需要函数;只需在需要测试的地方放置a(n)即可。我最初在文件中将“True”和“False”单词分别存储在不同的行上,但很快发现读取这些文本并将其转换为布尔数组速度很慢。最终,我发现存储零和一并将其读入字符数组(我称之为`IsPrime`数组)更有效。然而,为每个值单独一行会使文件比必要的大三倍,因为每行末尾都有一个回车符和换行符。我通过将零和一全部存储在文件的一行中,然后一次一个地将每个值读入字符数组来减小文件大小。
第二个想法利用了在鲁斯-阿伦对搜索中测试素性时,被测试的数字总是越来越大的事实。通过设置一个静态索引值,可以搜索素数数组,直到找到或超过被测试的数字。数组中的当前索引将保持不变,直到下一个更高数字的测试。然后搜索将从该索引继续。这消除了为每个被测试数字搜索数组中较低值的需要。
以下是这两个测试的结果:
用于确定数字是否为素数的方法 | 测试从2到10,000的数字所需时间 | 测试从2到1,000,000的数字所需时间 | 测试从2到100,000,000的数字所需时间 |
IsPrimeArray 索引 2 | 0.001 秒 | 0.036 秒 | 2.883 秒 |
静态索引搜索 | 0.000 秒 | 0.003 秒 | 0.306 秒 |
2 加载包含1亿以内整数素数信息的文件耗时1.312秒。
显然,静态索引搜索是最快的方法,也是应该在鲁斯-阿伦对程序中实现的方法。然而,在多线程环境下这样做会带来一个问题。我不能为所有线程只设置一个静态索引,因为有些线程可能会超前其他线程并弄乱索引。因此,我不得不创建一个索引数组,每个线程一个。但由于某种原因,这运行得非常慢。我相信这与每个线程几乎同时尝试修改同一个数组有关。所以,我选择了`IsPrimeArray Index`方法,该方法在测试高达1亿的对时仍然比平方根方法快近七倍。但这确实意味着必须将另一个文件读入数组。
您可能会问自己,为什么我需要同时拥有一个只包含素数的`Prime`数组和一个包含每个整数的“`0`”或“`1`”以指示其素性的`IsPrime`数组。嗯,第一个数组可以非常快速轻松地找到素因子并确定它们是否按顺序排列。第二个数组可以快速轻松地确定一个数字是否为素数。但即使如此,我仍然不喜欢我有两个包含冗余信息的文件这个想法。因此,我决定不将一个单独的文件读入`Prime`数组,而是决定在将另一个文件读入`IsPrime`数组时生成该数组,方法是当该索引处的值为“`1`”时,将索引值添加到`Prime`数组。
读取包含前1亿个整数的0和1的文件,例程耗时不到4秒。读取包含前10亿个整数素数信息的文件,耗时约28秒。
我尝试了另一种方法将*IsPrime*文件读入程序。我没有将零和一读入`IsPrime`数组,而是尝试将整个文件读入一个`IsPrime`字符串。然而,当尝试读取包含前10亿个整数信息的那个文件时,VB.NET抛出了一个错误,通知我内存溢出。我一直为32位计算机编译程序,所以切换到为64位编译,看看`string`s是否可以更长。它成功了,并且只花了大约18秒将文件加载到`string`(3秒)并创建`Prime`数组(15秒),比以前的方法提速了35%。检查数字是否为素数的方法必须从数组索引更改为`string`索引。但这也显示出速度的提升。
进一步思考后,我意识到`Prime`数组不需要包含`IsPrime`字符串中表示的所有素数。虽然我需要测试大数字的素性,但我只需要`Prime`数组中直到`IsPrime`数组中最大数字的平方根的数字。原因与之前描述的相同。通过限制`Prime`数组的大小,加载`IsPrime`字符串和创建`Prime`数组的时间下降到大约3.5秒(加载`IsPrime`字符串3秒,创建`Prime`数组0.5秒)。这是最终例程的代码。
Private Sub LoadIsPrimeStr()
Dim fn As String = "\IsPrimes-1,000,000,000.txt"
Dim i As Integer = 0
Dim j As Integer = 0
Dim length As Integer
Dim maxPrimeToLoad As Integer
'read in IsPrime string
IsPrimeStr = My.Computer.FileSystem.ReadAllText(txtPath.Text & fn)
'get length of the string
length = IsPrimeStr.Length
'make Prime array bigger than needed, less than 6% of numbers are prime
ReDim PrimeArray(0.06 * length)
'populate PrimeArray up to sqrt of quantity in IsPrimeStr
maxPrimeToLoad = Math.Round(Math.Sqrt(length) + 1)
For i = 0 To maxPrimeToLoad
If IsPrimeStr(i) = "1" Then
PrimeArray(j) = i
j += 1
End If
Next
'redim to just what was loaded and free up memory
ReDim Preserve PrimeArray(j - 1)
GC.Collect()
End Sub
寻找素因子
我在测试确定数字素性的方法时学到的一些经验,我将其运用到寻找数字素因子的例程中。以下是该例程最终迭代的样子:
Private Function GetPrimeFactors(n As Integer, ByRef a() As Integer) As Integer
Dim i As Integer = 0
Dim k As Integer = 0
Dim q As Integer = n
Dim sum As Integer = 0
ReDim a(0)
If IsPrimeStr(n) = "1" Then 'number is prime
a(k) = n
Return n
Else 'loop through primes to determine factors for this number
Do
If q Mod PrimeArray(i) = 0 Then
ReDim Preserve a(k)
a(k) = PrimeArray(i)
sum = sum + a(k) 'sum up factors
q = q / PrimeArray(i)
If q = 1 Then Exit Do
k += 1
If IsPrimeStr(q) = "1" Then 'quotient is prime so finished
ReDim Preserve a(k)
a(k) = q
sum += q
Exit Do
End If
Else
i += 1
End If
Loop
Return sum
End If
End Function
最终结果
那么,在对鲁斯-阿伦对程序实施了所有这些优化更改后,我获得了什么样的速度呢?下面是一个表格,显示了我开始和结束时的状况。
鲁斯-阿伦对搜索方法 | 完成2到1,000,000的耗时 |
Excel 和 VBA,无优化 | 0 小时 31 分 25 秒 |
VB.NET,与上文无程序性改变 | 0 小时 1 分 31 秒 |
VB.NET,8个线程 | 0 小时 0 分 19 秒 |
VB.NET,16个线程,使用优化 | 0 小时 0 分 0.271 秒 |
VB.NET编译为64位,16个线程,使用所有优化,包括InPrimeStr | 0 小时 0 分 0.224 秒 |
哇!从31.5分钟到224毫秒。这速度提升相当大。几乎快了8500倍。达到这个速度水平,我甚至可以测试更大的鲁斯-阿伦对。使用包含高达10亿的数字素数信息的*IsPrime*文件,我找到了那个值以下的鲁斯-阿伦对。找到这些对花了6分钟多一点。如果您感兴趣,共有6810对。
一种未提及的方法
在我的优化搜索过程中,我曾一度意识到关于整数的某些事实不会改变,例如它们是否为素数、它们的素因子是什么、这些因子之和、它们的素因子是否唯一以及它们是否连续。那么,为什么不预先计算这些信息,将其存储在文件中,然后将该文件读入鲁斯-阿伦对程序并遍历它来确定鲁斯-阿伦对,而不是每次都计算所有内容呢。
这个想法奏效了,但有两个注意事项:将文件加载到数组所需的时间以及该数组消耗的内存量。我无法将整个文件加载到数组中,因为VB.NET会拒绝,告诉我内存溢出。但即便如此,考虑到预计算值文件的加载时间,我最终添加到程序中的所有优化实际上使得在运行时进行计算更快。
小题大做?
此时,您可能会问:“为什么要费尽心思进行这些优化?鲁斯-阿伦对是不会改变的。为什么不让程序运行一周来生成这些对,然后就完事呢?”对此,我会说:“好问题!”
对我来说,整个旅程都是关于学习。发现那一点点能大大加快程序速度的改变,让我感到非常兴奋。而且别忘了,有了这提高的速度,我一周内现在可以找到比修改前多得多的鲁斯-阿伦对。
但这故事还有更多。官方地,一个鲁斯-阿伦对必须是两个连续的数字,其素因子之和相等。然而,这可以扩展到我所称的伪鲁斯-阿伦对。那么,寻找两个相隔两个数字的对,它们的素因子之和相等呢?或者相隔三个、四个,甚至一百万个呢?那难道不也很有趣吗?随着速度的极大提升,我们现在能够快速搜索所有这些伪鲁斯-阿伦对。
这是我的程序界面
“对之间的间隔”值是伪鲁斯值和阿伦值之间的分离量。请注意,还有一个地方可以输入批处理文件的名称。这是我添加的另一个不错的功能,您可以在Excel模板文件中填写运行参数信息。程序可以读取该文件并自动执行多次运行。因子文件是包含上一节中提到的预计算素数和素因子信息的文件。要使用它,运行类型必须设置为`RAPC`。
前面,我展示了鲁斯-阿伦对程序在VBA中运行时Excel输出表格的样子。那个表格随着时间而改变。现在它看起来像这样:
此外,在同一文件中还有一个用于记录统计信息的工作表。
最新增补
我最新添加到鲁斯-阿伦对程序的代码使其能够找到鲁斯-阿伦对的**所有因子**,而不仅仅是素因子,并确定它们的和是否相等。我想知道是否存在既是鲁斯-阿伦素因子对又是鲁斯-阿伦所有因子对的情况。我目前还没有找到。以下表格显示了前十亿个整数中最后几对鲁斯-阿伦所有因子对。
寻找所有因子的算法与寻找素因子的算法类似。然而,由于所有因子包括任何整数,无论是素数还是非素数,因此不能使用`IsPrimeArray`或`IsPrimeStr`测试。不过,平方根测试非常有用。以下是我使用的算法,其中`n`是被分解的数字。
- 如果`n = 1`,则唯一的因子是`1`。
- 如果`n`是素数,则唯一的因子是`1`和`n`。
- 否则,遍历整数`i`,从`1`到`n`的平方根。对于偶数`n`,步长为`1`;对于奇数`n`,步长为`2`,因为奇数只能有奇数因子。
- 如果`i`能整除`n`,那么`i`和`n/i`都是因子。
- 如果`n`是一个整数的平方,那么最后一个因子将是`Sqrt(n)`。
在前十亿个整数中,有533个鲁斯-阿伦所有因子对。有趣的是,这些对中的鲁斯值或阿伦值都不是整数的平方,这在上述最后一步中有所提及。我不知道这对所有对是否都成立。也许某位数学家已经证明了这一点,但我对此证明一无所知。
结论
这个项目对我来说非常有趣。自从我在Visual Basic 6时代创建共享软件以来,我从未对编程如此兴奋过。在那之前,我用C和C++为Amiga电脑创建共享软件,并为Amiga电脑杂志撰写关于编码的文章,这让我玩得很开心。
我希望我的程序优化之旅对您来说很有趣,并能为您自己的编程提供一些想法。