有效地计算第一个20位子串,在Pi的十进制扩展中重复

时间:2012-04-17 18:55:37

标签: algorithm

问题

Pi = 3.14159 26 5358979323846 26 433 ...所以重复的第一个2位数字符串是26。

找到重复的第一个20位子字符串的有效方法是什么?

约束

  • 我有大约500千兆字节的Pi(每位1个字节),大约500千兆字节的磁盘空间。

  • 我有大约5千兆字节的RAM免费。

  • 我感兴趣的是一种适用于任意序列的高效算法,而不是Pi本身的特定答案。换句话说,即使打印的数字是正确的,我对“print 123 .... 456”形式的解决方案也不感兴趣。

我尝试了什么

我将每个子字符串放入哈希表并报告第一次碰撞。

(哈希表构造为排序链表的数组。数组的索引由字符串的底部数字(转换为整数)给出,存储在每个节点中的值是扩展Pi的子串首次出现。)

这个工作正常,直到我用完RAM。

为了扩展到更长的序列,我考虑过:

  • 为从某个范围开始的所有子字符串生成哈希值,然后继续搜索其余数字。这需要为每个范围重新扫描整个Pi序列,因此变为阶数N ^ 2

  • 将20位数字的子串排序到多个文件,然后使用哈希表分别查找每个文件中的第一个重复。不幸的是,使用这种方法,我的磁盘空间不足,因此需要20次通过数据。 (如果我以1000位开头,那么我最终会得到1000个20位数的子串。)

  • 每个字节存储2位Pi,以释放更多内存。

  • 将基于磁盘的后备存储添加到我的哈希表。我担心这会表现得非常糟糕,因为没有明显的参考地点。

有更好的方法吗?

更新

  1. 我尝试了Adrian McCarthy的qsort方法,但这似乎比找到重复的哈希慢一点

  2. 我查看了btilly的MapReduce并行算法的建议,但它在我的单台计算机上严重违约,因此不适合我(使用我的单磁盘驱动器)

  3. 我实施了supercat的方法,用于昨晚拆分文件并搜索前180亿个数字中的19位数字子串。

  4. 这找到16场比赛,所以我用Jarred的建议重新检查了19位数的比赛,以找到前20位数的比赛

  5. 要搜索180亿个数字 分割文件需要3个小时,然后40分钟重新扫描文件以查找匹配项。

    答案

    在Pi的十进制扩展中,位数为1,549,4062,637和17,601,613,330的20位子串84756845106452435773。

    非常感谢大家!

4 个答案:

答案 0 :(得分:6)

这是一个有趣的问题。

首先让我们回顾一下信封号码。任何特定的20位数序列将在10 20 中匹配一次。如果我们走到第n位,我们有大约n 2 / 2对20位序列。因此,为了找到匹配的好几率,我们可能需要n超过10 10 。假设我们每个记录占用40个字节,我们将需要大约400 GB数据的东西。 (我们实际上需要比这更多的数据,所以我们应该为超过1TB的数据做好准备。)

这让我们了解了所需的数据量。数十亿的数字。数百GB的数据。

现在问题就在于此。如果我们使用任何需要随机访问的数据结构,则随机访问时间由磁盘速度设置。假设您的磁盘速度为6000 rpm。那是每秒100次。平均而言,您想要的数据是磁盘的中间位置。所以你平均每秒可以获得200次随机访问。 (这可能因硬件而异。)访问它100亿次需要5000万秒,这是一年多的时间。如果你阅读,然后写,并最终需要200亿个数据点 - 你超过了硬盘的预计寿命。

另一种方法是以不随机访问的方式处理一批数据。经典是做一个好的外部排序,如合并排序。假设在排序期间我们有1 TB的数据,我们读了30次,写了30次。 (两个估计都高于需要,但我在这里描绘的是最糟糕的情况。)假设我们的硬盘具有100 MB / s的持续吞吐量。然后每次通过需要10,000秒,持续600,000秒,这稍微低于一周。这是非常可行的! (实际上它应该比这更快。)

所以这是算法:

  1. 从一长串数字开始,3141 ......
  2. 将其转换为更大的文件,其中每行为20位,然后是pi中显示的位置。
  3. 对这个更大的文件进行排序。
  4. 在已排序的文件中搜索任何重复项。
    1. 如果找到,请返回第一个。
    2. 如果没有找到,请用另一大块数字重复步骤1-3。
    3. 将其合并到上一个已排序的文件中。
    4. 重复此搜索。
  5. 现在这很好,但是如果我们不想花一个星期呢?如果我们想要多台机器怎么办?事实证明这很容易。有众所周知的分布式排序算法。如果我们将初始文件拆分为块,我们可以将步骤1和4并行化。如果在步骤4之后我们找不到匹配,那么我们可以从一开始就用更大的输入块重复。

    实际上这种模式非常普遍。所有真正不同的是将初始数据转换为要排序的东西,然后查看匹配的组。这是http://en.wikipedia.org/wiki/MapReduce算法。这对于这个问题也会很好。

答案 1 :(得分:4)

<强> Trie树

RBarryYoung指出,这将超出内存限制。

trie数据结构可能是合适的。在一次通过中,您可以使用您看到的长度为n的每个前缀(例如,n = 20)构建一个trie。当您继续处理时,如果您到达已存在的级别n的节点,您刚刚找到了重复的子字符串。

后缀匹配

另一种方法是将扩展视为字符串。这种方法找到了常见的后缀,但是你想要共同的前缀,所以首先要反转字符串。创建一个指针数组,每个指针指向字符串中的下一个数字。然后使用词典排序对指针进行排序。在C中,这将是:

qsort(array, number_of_digits, sizeof(array[0]), strcmp);

当qsort完成时,类似的子串将在指针数组中相邻。因此,对于每个指针,您可以对该字符串进行有限的字符串比较,并使用下一个指针指向的字符串。再次,在C:

for (int i = 1; i < number_of_digits; ++i) {
  if (strncmp(array[i - 1], array[i], 20) == 0) {
    // found two substrings that match for at least 20 digits
    // the pointers point to the last digits in the common substrings
  }
}

排序是(通常)O(n log_2 n),之后的搜索是O(n)。

这种方法的灵感来自this article

答案 2 :(得分:2)

也许这样的事情会起作用:

  1. 搜索长度为2的重复子串(或一些小的基本情况),记录起始标记S = {s_i}

  2. 对于n = 3..N,从S

  3. 中的索引中查找长度为n的子串
  4. 每次迭代,使用长度为n

  5. 的子字符串更新S.
  6. 在n = 20时,前两个指标将是您的答案

  7. 您可能需要调整初始大小和步长(可能没有必要每次逐步调整1次)

答案 3 :(得分:2)

您的数据集非常大,因此需要某种“分而治之”。我建议,作为第一步,您将问题细分为若干件(例如100)。首先查看文件是否有任何重复的20位数序列以00开头,然后查看它是否有任何以01等开头,等等达到99.通过写出所有文件来启动每个“主要通行证”以正确数字开头的20位数序列。如果前两位数是常数,那么你只需要写出最后的18位数;由于一个18位十进制数字将适合8字节“长”,输出文件可能会容纳大约5,000,000,000个数字,占用40GB的磁盘空间。请注意,一次生成多个输出文件可能是值得的,以避免必须读取源文件的每个字节100次,但如果您只是读取一个文件并写入一个文件,则磁盘性能可能会更好

一旦为特定的“主要通行证”生成了数据文件,就必须确定其中是否有任何重复。基于存储在其中的数字中的比特将其细分为若干个较小的部分可能是一个很好的下一步骤。如果将它细分为256个较小的部分,每个部分将有大约16-32百万个数字;一个拥有的5千兆字节的RAM可用于为256个桶中的每一个缓冲一百万个数字。写出一百万个数字的每个块需要一个随机的磁盘搜索,但这样的写入次数将是非常合理的(可能大约10,000次磁盘搜索)。

一旦数据被细分为每个16-32百万个数字的文件,只需将每个这样的文件读入内存并查找重复数据。

所描述的算法可能不是最优的,但它应该相当接近。最令人感兴趣的是,将主要通道的数量减少一半将减少必须通过源数据文件读取的次数的一半,但是一旦其数据被处理,将使处理每次通过所需的时间增加一倍以上。复制。我猜想使用100次遍历源文件可能不是最佳的,但使用该分割因子的整个过程所需的时间将非常接近使用最佳分割因子的时间。