Ruby: How do you set an Enumerator's state?

别说谁变了你拦得住时间么 提交于 2019-12-22 12:37:21

问题


I'm doing a base 64 permutation incrementor. I've already written all the working code. But seeing as how Ruby already as Array::permutation which produces an Enumerator; I'd like to use that and take it a step further.

Without having to go through every permutation by using 'next', can I set the start point?

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"

. # DO SOMETHING LIKE THIS

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

. # AND PICK UP AS FOLLOWS

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

Being able to set the state at which the Enumerator will continue would solve everything in simplicity. I created a Golf challenge for this. Writing all the code out I couldn't get it to less than 600+ characters of code. But if I can set the state from which Enumerator will proceed from I could easily do it in closer to around 100, or less, characters of code.

I'm really just doing my best to learn some of the deep dark secret's of Ruby and it's core.

Here's the golf challenge I did if you're interested in the code: http://6ftdan.com/2014/05/03/golf-challenge-unique-base-64-incrementor/


回答1:


Results

For your example,

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

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

the following is obtained with the enumerator head_start_permutation I've constructed below:

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"]

The second next is the most interesting. As 'A' and '/' are the first and last elements of x, the next element in sequence after 'K/' would be 'LA' but since 'A' already appears in the permutations, 'LB' is tried and rejected for the same reason, and so on, until 'LK' is accepted.

Another example:

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"]

Notice that 'x' and '3'were skipped over as the last element in each of the arrays, because the remainder of the permutation contains those elements.

Permutation ordering

Before considering how to effectively deal with your problem, we must consider with the issue of the order of the permutations. As you wish to begin the enumeration at a particular permutation, it is necessary to determine which permutations come before and which come after.

I will assume that you want to use a lexicographical ordering of arrays by the offsets of array elements (as elaborated below), which is what Ruby uses for Array#permuation, Array#combinaton and so forth. This is a generalization of "dictionary" ordering of words.

By way of example, suppose we want all permutations of the elements of:

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

taken three at a time. This is:

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]]

If we replace the elements of arr with their positions:

pos = [0,1,2,3]

we see that:

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]]

If you think of each of these arrays as a three-digit number in base 4 (arr.size), you can see we are here merely counting them from zero to the largest, 333, skipping over those with common digits. This is the ordering that Ruby uses and the one I will use as well.

Note that:

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

which shows that once we have pos_permutations, we can apply it to any array for which permutations are needed.

Easy head-start enumerator

Suppose for the array arr above we want an enumerator that permutes all elements three at a time, with the first being [:c,:a,:d]. We can obtain that enumerator as follows:

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"]

But wait a minute! This is hardly a time-saver if we wish to use this enumerator only once. The investment in converting the full enumerator to an array, chopping off the beginning and converting what's left to the enumerator enum might make sense (but not for your example) if we intended to use enum multiple times (i.e., always with the same enumeration starting point), which of course is a possibility.

Roll your own enumerator

The discussion in the first section above suggests that constructing an enumerator

head_start_permutation(start)

may not be all that difficult. The first step is to create a next method for the array of offsets. Here's one way that could be done:

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

The particular implementation I have chosen is not particularly efficient, but it does achieve its purpose:

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]

Notice how this has skipped over arrays containing duplicates.

Next, we construct the enumerator method. I've chosen to do this by monkey-patching the class Array, but other approaches could be taken:

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

Let's try it:

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"]

Note that it would be even easier to construct an enumerator

head_start_repeated_permutation(start)

The only difference is that in NextUniq#next we would not skip over candidates having duplicates.




回答2:


You can try using drop_while (using lazy to avoid having the permutation enumerator from going over all its elements):

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


来源:https://stackoverflow.com/questions/23588375/ruby-how-do-you-set-an-enumerators-state

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!