Ruby:你如何设置枚举器的状态?

Posted

技术标签:

【中文标题】Ruby:你如何设置枚举器的状态?【英文标题】:Ruby: How do you set an Enumerator's state? 【发布时间】:2014-06-28 14:18:40 【问题描述】:

我正在做一个以 64 为基数的置换增量器。我已经编写了所有工作代码。但是看到 Ruby 已经作为 Array::permutation 产生了一个枚举器;我想使用它并更进一步。

不必使用“下一个”来遍历每个排列,我可以设置起点吗?

x = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a + ['+','/']
y = x.permutation(12)
y.peek.join
=> "ABCDEFGHIJKL"
y.next
y.peek.join
=> "ABCDEFGHIJKM"

。 # 做这样的事情

y.set_current_enumerator_state_to("ABCDEFGHIJK/".split(""))

。 # 并按以下方式领取

y.peek.join
=> "ABCDEFGHIJK/"
y.next
y.peek.join
=> "ABCDEFGHIJLK"

能够设置枚举器将继续运行的状态将简单地解决所有问题。我为此创建了一个高尔夫挑战。写出所有代码我无法将其写成少于 600 多个字符的代码。但是,如果我可以设置 Enumerator 将从哪个状态开始,我可以轻松地在接近 100 个或更少的代码字符中完成它。

我真的只是在尽我最大的努力去了解 Ruby 的一些深奥的秘密以及它的核心。

如果您对代码感兴趣,这是我所做的高尔夫挑战:http://6ftdan.com/2014/05/03/golf-challenge-unique-base-64-incrementor/

【问题讨论】:

Enumerator 混合在 Enumerable 中。这有帮助吗? 我已经浏览了这两个核心对象的 RubyDoc。 ruby-doc.org/core-1.9.3/Enumerable.html 和 ruby-doc.org/core-1.9.3/Enumerator.html 我遇到的最接近的是 slice_before(state) ,它返回一个枚举器。但这并不能解决任何问题,因为它只是从开头开始的另一个小节,然后会降低值,这正是我所需要的。 简短的回答是“你不知道”。枚举器不保证一个固定的顺序,只是你最终会得到所有的元素。我认为你想要的是将整个枚举转换为一个数组,然后对数组进行排序可能会让你想要你想要的。 任何计算机在尝试生成以 64 为基数的完整排列列表时都会挂起。如果数组大小有最大值,则可能超出此范围。 【参考方案1】:

结果

以你的为例,

x = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a + ['+','/']

start = "ABCDEFGHIJK/".split("")

下面是用我在下面构造的枚举器head_start_permutation得到的:

y = x.head_start_permutation(start)
  #=> #<Enumerator: #<Enumerator::Generator:0x000001011e62f0>:each>
y.peek.join(' ') #=>  "A B C D E F G H I J K /"
y.next.join(' ') #=>  "A B C D E F G H I J K /"
y.next.join(' ') #=>  "A B C D E F G H I J L K"
y.next.join(' ') #=>  "A B C D E F G H I J L M"
y.take(3).map  |a| a.join(' ') 
                 #=> ["A B C D E F G H I J L M",
                 #    "A B C D E F G H I J L N",
                 #    "A B C D E F G H I J L O"]

第二个next 是最有趣的。由于'A''/'x 的第一个和最后一个元素,'K/' 之后的下一个元素将是'LA',但由于'A' 已经出现在排列中,'LB' 被尝试并以同样的原因被拒绝,依此类推,直到'LK' 被接受。

另一个例子:

start = x.sample(12)
  # => ["o", "U", "x", "C", "D", "7", "3", "m", "N", "0", "p", "t"]
y = x.head_start_permutation(start)

y.take(10).map  |a| a.join(' ') 
  #=> ["o U x C D 7 3 m N 0 p t",
  #    "o U x C D 7 3 m N 0 p u",
  #    "o U x C D 7 3 m N 0 p v",
  #    "o U x C D 7 3 m N 0 p w",
  #    "o U x C D 7 3 m N 0 p y",
  #    "o U x C D 7 3 m N 0 p z",
  #    "o U x C D 7 3 m N 0 p 1",
  #    "o U x C D 7 3 m N 0 p 2",
  #    "o U x C D 7 3 m N 0 p 4",
  #    "o U x C D 7 3 m N 0 p 5"]

请注意,'x''3' 作为每个数组中的最后一个元素被跳过,因为排列的其余部分包含这些元素。

排列顺序

在考虑如何有效地处理你的问题之前,我们必须考虑排列顺序的问题。由于您希望从特定排列开始枚举,因此有必要确定哪些排列在前面,哪些在后面。

我假设您想通过数组元素的偏移量来使用数组的lexicographical ordering(如下所述),这就是Ruby 用于Array#permuation、Array#combinaton 等等。这是对单词“字典”排序的概括。

举例来说,假设我们想要以下元素的所有排列:

arr = [:a,:b,:c,:d]

一次拍三张。这是:

arr_permutations = arr.permutation(3).to_a
  #=> [[:a,:b,:c], [:a,:b,:d], [:a,:c,:b], [:a,:c,:d], [:a,:d,:b], [:a,:d,:c],
  #=>  [:b,:a,:c], [:b,:a,:d], [:b,:c,:a], [:b,:c,:d], [:b,:d,:a], [:b,:d,:c],
  #=>  [:c,:a,:b], [:c,:a,:d], [:c,:b,:a], [:c,:b,:d], [:c,:d,:a], [:c,:d,:b],
  #=>  [:d,:a,:b], [:d,:a,:c], [:d,:b,:a], [:d,:b,:c], [:d,:c,:a], [:d,:c,:b]]

如果我们将arr的元素替换为它们的位置:

pos = [0,1,2,3]

我们看到了:

pos_permutations = pos.permutation(3).to_a
  #=> [[0, 1, 2], [0, 1, 3], [0, 2, 1], [0, 2, 3], [0, 3, 1], [0, 3, 2],  
  #    [1, 0, 2], [1, 0, 3], [1, 2, 0], [1, 2, 3], [1, 3, 0], [1, 3, 2],
  #    [2, 0, 1], [2, 0, 3], [2, 1, 0], [2, 1, 3], [2, 3, 0], [2, 3, 1],
  #    [3, 0, 1], [3, 0, 2], [3, 1, 0], [3, 1, 2], [3, 2, 0], [3, 2, 1]]

如果您将这些数组中的每一个都视为以 4 为底的三位数字 (arr.size),您可以看到我们在这里只是从零到最大的 333 来计算它们,而跳过了具有常见数字的那些。这是 Ruby 使用的顺序,也是我将使用的顺序。

请注意:

pos_permutations.map  |p| arr.values_at(*p)  == arr_permutations #=> true

这表明一旦我们有了pos_permutations,我们就可以将它应用于任何需要排列的数组。

简单的开头枚举器

假设对于上面的数组arr,我们需要一个枚举器,它一次置换三个所有元素,第一个是[:c,:a,:d]。我们可以通过如下方式获得该枚举器:

temp = arr.permutation(3).to_a
ndx = temp.index([:c,:a,:d]) #=> 13
temp = temp[13..-1]
  #=>[              [:c,:a,:d], [:c,:b,:a], [:c,:b,:d], [:c,:d,:a], [:c,:d,:b],
  #   [:d, :a, :b], [:d,:a,:c], [:d,:b,:a], [:d,:b,:c], [:d,:c,:a], [:d,:c,:b]]
enum = temp.to_enum
  #=> #<Enumerator: [[:c, :a, :d], [:c, :b, :a],...[:d, :c, :b]]:each>
enum.map  |a| a.map(&:to_s).join 
  #=> [       "cad", "cba", "cbd", "cda", "cdb",
  #    "dab", "dac", "dba", "dbc", "dca", "dcb"]

但是等一下!如果我们只希望使用这个枚举器一次,这很难节省时间。如果我们打算多次使用enum(即,总是与相同的枚举起点),这当然是可能的。

滚动你自己的枚举器

上面第一节的讨论表明构造一个枚举器

head_start_permutation(start)

可能并不那么困难。第一步是为偏移数组创建一个next 方法。以下是一种可行的方法:

class NextUniq
  def initialize(offsets, base)
    @curr = offsets
    @base = base
    @max_val = [base-1] * offsets.size
  end

  def next
    loop do
      return nil if @curr == @max_val 
      rruc = @curr.reverse
      ndx = rruc.index  |e| e < @base - 1 
      if ndx
        ndx = @curr.size-1-ndx
        @curr = @curr.map.with_index do |e,i|
          case i <=> ndx
          when -1 then e
          when  0 then e+1
          when  1 then 0
          end
        end
      else
        @curr = [1] + ([0] * @curr.size)
      end
      (return @curr) if (@curr == @curr.uniq) 
    end  
  end  
end

我选择的特定实现并不是特别有效,但确实达到了目的:

nxt = NextUniq.new([0,1,2], 4)
nxt.next #=> [0, 1, 3]
nxt.next #=> [0, 2, 1]
nxt.next #=> [0, 2, 3]
nxt.next #=> [0, 3, 1]
nxt.next #=> [0, 3, 2]
nxt.next #=> [1, 0, 2]

注意这是如何跳过包含重复的数组的。

接下来,我们构造枚举器方法。我选择通过猴子修补类Array 来做到这一点,但可以采取其他方法:

class Array
  def head_start_permutation(start)
    # convert the array start to an array of offsets
    offsets = start.map  |e| index(e)  
    # create the instance of NextUtil
    nxt = NextUniq.new(offsets, size)
    # build the enumerator  
    Enumerator.new do |e|
      loop do
        e << values_at(*offsets)
        offsets = nxt.next
        (raise StopIteration) unless offsets
      end
    end
  end  
end

让我们试试吧:

arr   = [:a,:b,:c,:d]
start = [:c,:a,:d]

arr.head_start_permutation(start).map  |a| a.map(&:to_s).join 
  #=> [       "cad", "cba", "cbd", "cda", "cdb",
  #    "dab", "dac", "dba", "dbc", "dca", "dcb"]

请注意,构造一个枚举器会更容易

head_start_repeated_permutation(start)

唯一的区别是在NextUniq#next 中我们不会跳过有重复的候选人。

【讨论】:

这是一本书... :-) 您提供了一个非常漂亮的答案。我肯定会尝试这个。 谢谢,6ft Dan(不要与 5ft Dan 或 7ft Dan 混淆)。【参考方案2】:

您可以尝试使用drop_while(使用lazy 以避免排列枚举器遍历其所有元素):

z = y.lazy.drop_while  |p| p.join != 'ABCDEFGHIJK/' 
z.peek.join
# => "ABCDEFGHIJK/"
z.next
z.peek.join
# => "ABCDEFGHIJLK"

【讨论】:

我相信 Ruby 2.0 出现了。这看起来是一个很好的解决方案!我想这是唯一的方法。 是的,我相信“懒惰”是 2.0 的功能 Uri,您能否详细说明“使用lazy 避免排列枚举器遍历其所有元素”?你是说它不会重复使用next 来到达所需的起点? ruby-doc.org/core-2.1.1/Array.html#method-i-drop_while : "...并返回一个包含剩余元素的 array" drop_while 实际上迭代了 所有 元素来构建结果数组。 Enumerator::Lazy 有自己的实现:ruby-doc.org/core-2.0/Enumerator/Lazy.html#method-i-drop_while 乌里。这行不通。即使使用lazy,它也需要反复调用next 才能获得起始值。假设起始排列的第一个元素是'o'。与x.size =&gt; 64x.index('o') =&gt; 40 一样,next 必须按 64**40 次的顺序调用才能将'o' 作为排列的第一个元素。如果你用x.sample(12) 试试你的代码,你就会明白我的意思了。

以上是关于Ruby:你如何设置枚举器的状态?的主要内容,如果未能解决你的问题,请参考以下文章

ruby 创建一个接收N个枚举器的枚举器然后排序

labview条件结构的分支选择器的问题

没有循环迭代器的枚举

设置触发选择器的选中状态的属性是?

不允许SAM账户和共享的匿名枚举?

问题记录