【问题标题】:How can I randomly iterate through a large Range?如何随机迭代一个大范围?
【发布时间】:2011-01-28 10:19:24
【问题描述】:

我想随机遍历一个范围。每个值只会被访问一次,所有值最终都会被访问。例如:

class Array
    def shuffle
        ret = dup
        j = length
        i = 0
        while j > 1
            r = i + rand(j)
            ret[i], ret[r] = ret[r], ret[i]
            i += 1
            j -= 1
        end
        ret
    end
end

(0..9).to_a.shuffle.each{|x| f(x)}

其中f(x) 是对每个值进行操作的某个函数。 Fisher-Yates shuffle 用于有效地提供随机排序。

我的问题是 shuffle 需要对数组进行操作,这并不酷,因为我正在处理 天文数 大数。 Ruby 会很快消耗大量 RAM 来尝试创建一个巨大的数组。想象一下用(0..99**99) 替换(0..9)。这也是以下代码不起作用的原因:

tried = {} # store previous attempts
bigint = 99**99
bigint.times {
    x = rand(bigint)
    redo if tried[x]
    tried[x] = true
    f(x) # some function
}

这段代码非常幼稚,随着tried 获得更多条目,很快就会耗尽内存。

什么样的算法可以完成我想做的事情?

[Edit1]:我为什么要这样做?我试图用尽哈希算法的搜索空间来寻找 N 长度的输入字符串以寻找部分冲突。我生成的每个数字都相当于一个唯一的输入字符串、熵等等。基本上,我正在使用custom alphabet“计数”。

[Edit2]:这意味着上述示例中的f(x) 是一种生成哈希并将其与常量目标哈希进行比较以进行部分冲突的方法。在调用f(x) 之后,我不需要存储x 的值,因此内存应该随着时间的推移保持不变。

[Edit3/4/5/6]:进一步澄清/修复。

[解决方案]:以下代码基于@bta 的解决方案。为简洁起见,next_prime 未显示。它产生可接受的随机性,并且每个数字只访问一次。详情请查看实际帖子。

N = size_of_range
Q = ( 2 * N / (1 + Math.sqrt(5)) ).to_i.next_prime
START = rand(N)

x = START
nil until f( x = (x + Q) % N ) == START # assuming f(x) returns x

【问题讨论】:

  • 您显然没有存储函数调用的结果,因为这也会占用大量内存。那么你到底在做什么呢?为什么需要以随机顺序执行此操作?如果您只是累积值,则顺序可能无关紧要。如果您需要解决方案,我想了解更多信息。
  • 如果您不需要将结果返回到数组中,请将示例代码 (0..9).sort_by{rand}.map{|x| f(x)} 更改为使用 each 而不是 map。这将使问题更清楚。
  • sort_by rand 也不正确;它会给出有偏见的结果。请参阅robweir.com/blog/2010/02/microsoft-random-browser-ballot.html(JavaScript,但概念相同)。
  • 正如@Matthew Flaschen 所写,您尝试随机化列表顺序的尝试被严重破坏,并且会返回可能看起来随机但并非随机的结果。他的链接很好地描述了这个问题。
  • 无效,你没有抓住重点。该链接是要做的。您不能按任何随机函数排序(移位随机函数也好不到哪里去)。

标签: ruby random range loops brute-force


【解决方案1】:

我只记得几年前上过的一门课有一个类似的问题;也就是说,在给定非常严格的内存限制的情况下,随机地(相对地)迭代一组(完全耗尽它)。如果我没记错的话,我们的解决方案算法是这样的:

  1. 将范围定义为从 0 到 一些数字N
  2. N内生成一个随机起点x[0]
  3. 生成小于N的迭代器Q
  4. 通过添加Q 来生成连续点x[n] 前一点并在需要时环绕。那 是,x[n+1] = (x[n] + Q) % N
  5. 重复直到生成一个与起点相等的新点。

诀窍是找到一个迭代器,它可以让您遍历整个范围而不会生成两次相同的值。如果我没记错的话,任何相对素数的NQ 都可以工作(数字越接近范围的边界,输入的“随机”就越少)。在这种情况下,不是N 因子的素数应该可以工作。您还可以交换结果数字中的字节/半字节,以更改生成点在N 中“跳跃”的模式。

该算法只需要存储起始点 (x[0])、当前点 (x[n])、迭代器值 (Q) 和范围限制 (N)。

也许其他人记得这个算法并且可以验证我是否记得正确?

【讨论】:

  • 我认为,如果您不存储尝试过的输入并且不能有重复项,那么您可以获得的最佳效果。如果您要测试所有输入并且它们不会干扰,那么真的不需要真正的随机洗牌。要尽可能分散选择,请使用接近黄金分割 (2N/(1+sqrt(5))) 的 Q。
  • 这听起来和我想做的几乎一模一样。我并不过分关注随机性,但它非常重要。如果有人知道这个算法的名称,那就太好了。
  • 我不确定该算法是否有名称。它所基于的特定原理(素数相对于模算术的数学属性)可能有一个名称。
【解决方案2】:

正如@Turtle 回答的那样,您的问题没有解决方案。 @KandadaBoggu 和@bta 解决方案为您提供随机数是一些随机或非随机的范围。你会得到一串数字。

但我不知道你为什么关心同一个数字的重复出现。如果(0..99**99) 是您的范围,那么您是否可以每秒生成 10^10 个随机数(如果您有一个 3 GHz 处理器和大约 4 个内核,您在每个 CPU 周期生成一个随机数 - 这是不可能的,而 ruby​​ 将甚至减慢很多),那么大约需要 10^180 年 才能耗尽所有数字。您也有大约 10^-180 的概率会在一整年中生成两个相同的数字。我们的宇宙大概有 10^9 年,所以如果你的计算机可以在时间开始时开始计算,那么你有大约 10^-170 的概率生成两个相同的数字。换句话说 - 实际上这是不可能的,您不必关心它。

即使您只使用 Jaguar(来自 www.top500.org 超级计算机的前 1 名)完成这项任务,您仍然需要 10^174 年才能获得所有数字。

如果你不相信我,试试看

tried = {} # store previous attempts
bigint = 99**99
bigint.times {
  x = rand(bigint)
  puts "Oh, no!" if tried[x]
  tried[x] = true
}

如果你能看到“哦,不!”,我就给你买杯啤酒。在你的一生中在你的屏幕上:)

【讨论】:

  • 感谢您提供有用的信息。范围 (0..99**99) 只是一个示例。我正在测试的散列算法具有一个搜索空间,该空间在实际长度输入的实际时间内是可以用尽的。我只是希望我的算法能够有效地扩展,同时为每个数字提供相同的被选中概率。至于啤酒,我认为太阳自发传送到银河系另一边的概率更高:)
  • 我正在测试的搜索空间是 (0..(80**N-1)),输入长度为 N。
  • 对于 N = 11,以与上面示例相同的速度耗尽所有数字需要 34 年。因此,当您使用 ruby​​ 时,您不仅要生成数字,还要对它们进行一些计算,那么您不应该关心重复的数字,因为需要很长时间才能耗尽所有可能性。另一方面,对于 N = 6,您可以将所有尝试过的数字存储在数组中的单个位上 - 大约需要 409 MB。 N = 7 你应该有大约 32 GB 的内存 - 所以你可能应该将它存储在硬盘上。但同样需要很多时间。
  • 在我的电脑上这样的简单循环:a = 80**4; b = 0; a.times {b = b+1} 花了大约 16 秒。就是说N加一,这个时间会增加80倍,所以N=6需要24分钟,N=7需要28小时,N=8需要9天以上。通过这个计算,它给出了 N=11 的 13300 年(这是一个具有 2.13 GHz 的核心的真实示例)。
  • 我看起来像你搞砸了你的数学。从N=7N=8 你乘以8 而不是80。N=8 的实际时间略多于3 个月。在选择测试密钥时有足够的随机性,平均案例时间减少了一半。利用多核 CPU 将平均案例时间除以您拥有的核心数量。如果需要更高的效率,我可以切换到不同的语言。将其提升到一个新的水平,我可以使用我的 GPU 进行流处理。
【解决方案3】:

我可能是错的,但我认为如果不存储一些状态这是可行的。至少,你需要一些状态。

即使每个值只使用一位(是否尝试过此值),您也需要 X/8 字节的内存来存储结果(其中 X 是最大的数字)。假设您有 2GB 的可用内存,这将留下超过 1600 万个数字。

【讨论】:

    【解决方案4】:

    将范围划分为可管理的批次,如下所示:

    def range_walker range, batch_size = 100
      size = (range.end - range.begin) + 1
      n = size/batch_size 
      n.times  do |i|
        x = i * batch_size + range.begin
        y = x + batch_size
        (x...y).sort_by{rand}.each{|z| p z}
      end
      d = (range.end - size%batch_size + 1)
      (d..range.end).sort_by{rand}.each{|z| p z }
    end
    

    您可以通过随机选择要处理的批次来进一步随机化解决方案。

    PS:这对 map-reduce 来说是个好问题。每个批次都可以由独立的节点工作。

    参考:

    Map-reduce in Ruby

    【讨论】:

    • 即使 "n" 和 "batch_size" 是相同的数字 (sqrt(n)),生成的数组也会太大而无法存储在内存中。不错的方法。我认为最终的算法必须做类似的事情,除了数组的大小可以管理。
    • 在您的问题中,不清楚您是否希望将结果作为数组。我以为您只是想随机处理一个范围内的数字,以确保处理每个数字。无论范围大小如何,此解决方案都可以做到这一点。如果您想将这些数字作为数组返回,那么您将遇到不同的问题。
    • 很抱歉没有澄清。我不希望将结果作为数组。在该循环内的某个地方,我想调用一个将生成的随机数作为输入的方法。内存使用量应长期保持不变。
    • 尝试调用 range_walker(0..99**99) 你就会明白我的意思了。
    • 我已经解决了这个问题。再试一次。内存消耗将保持不变。由于连续处理,CPU 接近 60%。
    【解决方案5】:

    你可以用 shuffle 方法随机迭代一个数组

    a = [1,2,3,4,5,6,7,8,9]
    a.shuffle!
    => [5, 2, 8, 7, 3, 1, 6, 4, 9]
    

    【讨论】:

      【解决方案6】:

      您想要所谓的“全循环迭代器”...

      这是最简单版本的伪代码,非常适合大多数用途...

      function fullCycleStep(sample_size, last_value, random_seed = 31337, prime_number = 32452843) {
      if last_value = null then last_value = random_seed % sample_size
          return (last_value + prime_number) % sample_size
      }
      

      如果你这样称呼:

      sample = 10
      For i = 1 to sample
          last_value = fullCycleStep(sample, last_value)
          print last_value
      next
      

      它将生成随机数,循环遍历所有 10 个,永不重复,但您仍然永远不会得到重复。

      【讨论】:

        【解决方案7】:

        数据库系统和其他大型系统通过将递归排序的中间结果写入临时数据库文件来做到这一点。这样,他们可以对大量记录进行排序,同时在任何时候只在内存中保留有限数量的记录。这在实践中往往很复杂。

        【讨论】:

          【解决方案8】:

          您的订单必须有多“随机”?如果您不需要特定的输入分布,您可以尝试这样的递归方案来最小化内存使用:

          def gen_random_indices
            # Assume your input range is (0..(10**3))
            (0..3).sort_by{rand}.each do |a|
              (0..3).sort_by{rand}.each do |b|
                (0..3).sort_by{rand}.each do |c|
                  yield "#{a}#{b}#{c}".to_i
                end
              end
            end
          end
          
          gen_random_indices do |idx|
            run_test_with_index(idx)
          end
          

          本质上,您是通过一次随机生成一个数字来构建索引的。在最坏的情况下,这将需要足够的内存来存储 10 *(位数)。您将遇到(0..(10**3)) 范围内的每个数字恰好一次,但顺序只是伪随机的。也就是说,如果第一个循环设置a=1,那么在看到百位数变化之前,您将遇到1xx 形式的所有三位数字。

          另一个缺点是需要手动将函数构造到指定的深度。在您的(0..(99**99)) 案例中,这可能是一个问题(尽管我想您可以编写一个脚本来为您生成代码)。我确信可能有一种方法可以以有状态的递归方式重新编写它,但我无法想到它(想法,任何人?)。

          【讨论】:

          • 尽可能随机。这样它就可以有效地耗尽搜索空间。这也是生日攻击成为可能的原因,大大缩短了搜索时间。把它想象成暴力破解密码锁。
          【解决方案9】:

          [编辑]:考虑到@klew 和@Turtle 的答案,我希望最好的结果是成批随机(或接近随机)数字。


          这是类似于 KandadaBoggu 解决方案的递归实现。基本上,搜索空间(作为一个范围)被划分为一个包含 N 个大小相等的范围的数组。每个范围都以随机顺序作为新的搜索空间反馈。这种情况一直持续到范围的大小达到下限。此时范围已经足够小,可以转换为数组,进行混洗和检查。

          尽管它是递归的,但我还没有炸毁堆栈。相反,当尝试对大于大约 10^19 键的搜索空间进行分区时,它会出错。我必须处理数字太大而无法转换为long。应该可以修复了:

          # partition a range into an array of N equal-sized ranges
          def partition(range, n)
              ranges = []
              first = range.first
              last = range.last
              length = last - first + 1
              step = length / n # integer division
              ((first + step - 1)..last).step(step) { |i|
                  ranges << (first..i)
                  first = i + 1
              }
              # append any extra onto the last element
              ranges[-1] = (ranges[-1].first)..last if last > step * ranges.length
              ranges
          end
          

          我希望代码 cmets 有助于阐明我最初的问题。

          pastebin: full source

          注意:# options 下的PW_LEN 可以更改为较小的数字以获得更快的结果。

          【讨论】:

          • 这很好,但你知道这不是真正的洗牌,对吧?第一个数字将随机分布,但接下来的 BLOCK_SIZE 个数字将全部来自同一范围。
          • 除非我误解了您的评论,否则 Fisher-Yates 是一个真正的洗牌,并且以正确的方式使用。每个块都按随机顺序进行分区和访问。然而,它所能做的最好的就是成批的随机数......
          【解决方案10】:

          对于一个令人望而却步的大空间,比如

          space = -10..1000000000000000000000
          

          您可以将此方法添加到Range

          class Range
          
            M127 = 170_141_183_460_469_231_731_687_303_715_884_105_727
          
            def each_random(seed = 0)
              return to_enum(__method__) { size } unless block_given?
              unless first.kind_of? Integer
                raise TypeError, "can't randomly iterate from #{first.class}"
              end
          
              sample_size = self.end - first + 1
              sample_size -= 1 if exclude_end?
              j = coprime sample_size
              v = seed % sample_size
              each do
                v = (v + j) % sample_size
                yield first + v
              end
            end
          
          protected
          
            def gcd(a,b)
              b == 0 ? a : gcd(b, a % b)
            end
          
            def coprime(a, z = M127)
              gcd(a, z) == 1 ? z : coprime(a, z + 1)
            end
          
          end
          

          你可以

          space.each_random { |i| puts i }
          
          729815750697818944176
          459631501395637888351
          189447252093456832526
          919263002791275776712
          649078753489094720887
          378894504186913665062
          108710254884732609237
          838526005582551553423
          568341756280370497598
          298157506978189441773
          27973257676008385948
          757789008373827330134
          487604759071646274309
          217420509769465218484
          947236260467284162670
          677052011165103106845
          406867761862922051020
          136683512560740995195
          866499263258559939381
          596315013956378883556
          326130764654197827731
          55946515352016771906
          785762266049835716092
          515578016747654660267
          ...
          

          只要您的空间比 M127 小几个数量级,就具有很大的随机性。

          感谢@nick-steele@bta 的方法。

          【讨论】:

            【解决方案11】:

            这并不是一个真正的 Ruby 特定的答案,但我希望它是允许的。 Andrew Kensler 在他的"Correlated Multi-Jittered Sampling" 报告中给出了一个 C++“permute()”函数。

            据我了解,他提供的确切功能仅在您的“数组”大小达到 2^27 时才有效,但总体思路可用于任何大小的数组。

            我会尽力解释一下。第一部分是您需要一个“对于任何二次方大小的域”可逆的哈希。考虑x = i + 1。无论 x 是什么,即使您的整数溢出,您也可以确定 i 是什么。更具体地说,您总是可以从 x 的底部 n 位确定 i 的底部 n 位。加法是一种可逆的散列操作,就像乘以奇数一样,就像按位异或乘以常数一样。如果您知道特定的二次幂域,则可以对该域中的位进行加扰。例如。 x ^= (x &amp; 0xFF) &gt;&gt; 5) 对 16 位域有效。您可以使用掩码指定该域,例如mask = 0xFF,你的哈希函数变成x = hash(i, mask)。当然,您可以将“种子”值添加到该哈希函数中以获得不同的随机化。 Kensler 在论文中列出了更多有效的操作。

            所以你有一个可逆函数x = hash(i, mask, seed)。问题是,如果你散列你的索引,你最终可能会得到一个大于你的数组大小的值,即你的“域”。不能只取模,否则会发生冲突。

            可逆哈希是使用称为“循环行走”技术的关键,该技术在“Ciphers with Arbitrary Finite Domains"”中介绍。因为哈希是可逆的(即 1 对 1),您可以重复应用相同的哈希,直到您的散列值小于您的数组!因为您应用的是相同的散列,并且映射是一对一的,所以您最终得到的任何值都将映射回恰好一个索引,因此您不会发生冲突。所以对于 32 位整数(伪代码),您的函数可能看起来像这样:

            fun permute(i, length, seed) {
              i = hash(i, 0xFFFF, seed)
              while(i >= length): i = hash(i, 0xFFFF, seed)
              return i
            }
            

            可能需要大量哈希才能到达您的域,因此 Kensler 做了一个简单的技巧:他将哈希保持在 2 的下一次幂的域内,这使得它需要很少的迭代(平均约 2 次) ,通过屏蔽不必要的位。最终的算法如下所示:

            fun next_pow_2(length) {
              # This implementation is for clarity.
              # See Kensler's paper for one way to do it fast.
              p = 1
              while (p < length): p *= 2
              return p
            }
            
            permute(i, length, seed) {
              mask = next_pow_2(length)-1
              i = hash(i, mask, seed) & mask
              while(i >= length): i = hash(i, mask, seed) & mask
              return i
            }
            

            就是这样!显然,这里重要的是选择一个好的散列函数,肯斯勒在论文中提供了它,但我想分解解释。如果您希望每次都有不同的随机排列,您可以向 permute 函数添加一个“种子”值,然后将其传递给哈希函数。

            【讨论】:

              猜你喜欢
              • 2016-10-16
              • 1970-01-01
              • 1970-01-01
              • 2021-12-02
              • 2014-09-08
              • 2019-10-30
              • 2018-04-08
              • 1970-01-01
              • 2021-07-24
              相关资源
              最近更新 更多