如何切片数组,同时避免每个切片中的重复值?

时间:2014-10-05 10:40:52

标签: ruby arrays random-sample

假设我有这个数组:

a = [1,2,3,3,3,3,3,3,3,3,3,4,5,6,6,7,8,9,10,11]

a.each_slice(2).to_a将生成对,但这些对将包含非唯一值,如[3,3]。所以我想我正在寻找某种unique_each_slice方法。

我想要的是能够继续洗牌这个阵列,直到我得到一个独特的2对(不是必须是2,可以是任何东西)的点,就像这样(使用2和示例):

[3, 1, 3, 7, 6, 3, 4, 5, 8, 3, 9, 3, 2, 3, 6, 3, 3, 11, 10, 3]

如果你在这个数组上执行each_slice(2),你将获得唯一的对:

[[3, 1], [3, 7], [6, 3], [4, 5], [8, 3], [9, 3], [2, 3], [6, 3], [3, 11], [10, 3]]

与你原来的相比:

[[1, 2], [3, 3], [3, 3], [3, 3], [3, 3], [3, 4], [5, 6], [6, 7], [8, 9], [10, 11]]

每个都有非唯一对,如[3,3]

另一个例子,假设我有:

a = [1,2,3,3,3,3,3,3,3,3,3,4,5,6,6,7,8,9,10,11,12,13,14,15,16,17]

现在,假设有一些功能a.unique_slices_of(3),我得到:

[[4, 16, 3], [1, 9, 3], [3, 6, 17], [3, 6, 10], [15, 3, 2], [3, 8, 12], [11, 3, 14], [7, 13, 3], [3, 5]]

通过"独特切片"我的意思是一个切片,其中相同的数字不会重复两次:[1,2,3]是一个独特的切片,[3,1,3]不是。

到目前为止,我已经提供了以下方法,它似乎需要多次迭代才能解决问题:

class Array
  def unique_slices_of!(slices)
    loop do
      unique = true
      self.each_slice(slices) do |slice|
        if slice != slice.uniq
          self.shuffle!
          unique = false # so we know whether to loop again
          break
        end
      end
      break if unique # if unique didn't change, that means all slices were equal
      if unique == false then unique == true end # reset and start again
    end
    self 
  end
end

我的代码的主要问题是:a)我不认为我使用了一些惯用的Ruby方法,可以将这个过程缩短一半或更多。 b)如果数组不能包含唯一切片,则存在无限循环的可能性。我可能需要在这里使用一些组合理论,但我不确定如何。

4 个答案:

答案 0 :(得分:2)

采样唯一组合

如果您正在寻找更具惯用性的内容,并且如果算法的效率不是您的主要关注点,您可以尝试以下方法:

a = [1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 5, 6, 6, 7, 8, 9, 10, 11]
a.combination(2).reject { |pair| pair[0] == pair[1] }.sample(a.size / 2)

这种方法的主要缺点是 a 较大时的速度,因为Array#combination会在您使用Array#reject和{{3]结果取消结果之前生成所有可能的组合}}。但是,对于适度大小的阵列来说,它看起来确实足够快。

评估解决方案的性能

休闲测试表明,这对于适度规模的阵列而言足够快。考虑:

require 'benchmark'

a = [1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 5, 6, 6, 7, 8, 9, 10, 11]

Benchmark.measure do
  a.combination(2).reject { |pair| pair[0] == pair[1] }.sample(a.size / 2)
end.to_s
#=> "  0.000000   0.000000   0.000000 (  0.000052)\n"

即使在100,000次迭代中,我的系统仍然只需要3.650299秒。考虑到你发布的语料库,这似乎足够实用,但你的里程可能会有所不同。

允许比较任意子阵列大小

比较具有计数

的成员

在评论中,OP询问是否可以将其推广到winnow子阵列,每个子阵列有2,3或4个元素。是的,稍微重构一下,尽管随着组合中元素数量的增加,性能会下降。考虑:

array = [1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 5, 6, 6, 7, 8, 9, 10, 11]
element_size = 4 

array.combination(element_size).
  reject { |element| element.map { |member| element.count(member) > 1 }.any? }.
      sample(array.size / element_size)

这使用所需的 element_size 来确定动态采样的数量。这有利于放弃任何部分填充的阵列,消除掉"悬空"你Array#sample获得的元素。

这里的主力仍然是拒绝方法,它现在使用#each_slice迭代每个子数组的每个成员,并拒绝具有#count个成员的元素,这些成员在该子元素中出现多次阵列。即使使用更好的变量名称,与我们拥有固定元素大小相比,它更难以遵循,但它肯定更灵活。

更易读(且速度更快)的比较

通过@pguardiario的帽子提示(参见#any?),你甚至可以通过选择只有所有数组的子数组来将其缩短使其更具可读性成员是this related answer。例如:

array.combination(element_size).
  select { |subarray| subarray == subarray.uniq }.
    sample(array.size / element_size)

答案 1 :(得分:2)

a = [1,2,3,3,3,3,3,3,3,3,3,4,5,6,6,7,8,9,10,11]

您可以测试切片是否是"唯一的"由:

a.each_slice(2).all?{|x| x == x.uniq}

所以现在你只需要洗牌,直到得到你想要的东西:

a.shuffle! until a.each_slice(2).all?{|x| x == x.uniq}

避免无限循环的最简单方法是使用timeout

require 'timeout'
# raise an error if it takes more than 1 second
timeout(1){ a.shuffle! until a.each_slice(3).all?{|x| x == x.uniq} }

答案 2 :(得分:0)

我有一个似乎有效的解决方案。基本思想是将具有最大计数的元素分布到尽可能多的切片中。添加一些shuffle以使其看起来是随机的。

class Array
  def unique_slices_of(slice_length)
    buf = []
    arr = []
    hash = Hash.new 0
    self.each {|i| hash[i] += 1}
    sorted = hash.sort_by {|k, v| v}.reverse
    # sorted[][0] holds the element and sorted[][1] holds the count
    return nil if sorted[0][1] > ((self.length * 1.0) / slice_length).ceil
    index = 0
    until sorted.length.zero?
      # Add element to buf and decrement count
      # if count == 0, remove the entry from sorted
      buf << sorted[index][0]
      sorted[index][1] -= 1
      if sorted[index][1] == 0
        sorted.delete_at index
        break if sorted.length == 0
        index -= 1
      end
      index = (index + 1) % sorted.length
      if buf.length == slice_length
        arr << buf.shuffle
        buf.clear
        index = 0
      end
    end
    arr << buf.shuffle if buf.length > 0
    arr.shuffle
  end
end

输出:

[3, 1, 3, 7, 6, 3, 4, 5, 8, 3, 9, 3, 2, 3, 6, 3, 3, 11, 10, 3].unique_slices_of(2)
#=> [[8, 3], [3, 6], [3, 5], [1, 10], [3, 4], [3, 6], [7, 3], [9, 3], [3, 11], [3, 2]]

[1,2,3,3,3,3,3,3,3,3,3,4,5,6,6,7,8,9,10,11,12,13,14,15,16,17].unique_slices_of(3)
#=> [[3, 9, 6], [6, 14, 3], [3, 2, 17], [7, 3, 16], [3, 11, 10], [15, 3, 8], [4, 3, 5], [3, 13, 12], [1, 3]]

答案 3 :(得分:0)

以非随机方式执行此操作

这里的想法是将不同的值放入箱中。然后,虽然剩下任何垃圾箱:

  • 先按照尺寸,最大的垃圾箱订购垃圾箱
  • 通过为每个第一个max_slice_size箱子取一个数字来制作切片
  • 删除空箱

因为切片中的每个值都来自不同的bin,所以保证切片将包含不同的值。

代码:

def slices_without_repeats(a, max_slice_size)
  slices = []
  bins = a.group_by { |e| e }.values
  until bins.empty?
    bins = bins.sort_by(&:size).reverse
    slice_size = [max_slice_size, bins.size].min
    slice = slice_size.times.map do |i|
      bins[i].pop
    end
    slices << slice
    bins.reject!(&:empty?)
    if slice.size < max_slice_size && !bins.empty?
      raise ArgumentError, "An element repeats too much"
    end
  end
  slices
end

此算法不使用显式随机性。它确实使用了Ruby的快速排序,这是不稳定的,并且可能潜在地利用随机性(如选择枢轴点时)。

使用中:

a = [1,2,3,3,3,3,3,3,3,3,3,4,5,6,6,7,8,9,10,11]
p slices_without_repeats(a, 2)
# [[3, 6], [3, 9], [3, 7], [3, 2], [3, 6],
#  [3, 10], [3, 11], [3, 4], [1, 8], [3, 5]]

它检测何时无法完成:

p slices_without_repeats(a, 3)
# An element repeats too much (ArgumentError)

它处理最后一个切片未满的情况:

p slices_without_repeats([1, 2, 3, 3, 4, 4, 4], 3)
# [[4, 3, 2], [4, 3, 1], [4]]