【问题标题】:Ruby - Pick one element from array by possibilityRuby - 从数组中选择一个元素的可能性
【发布时间】:2021-11-19 04:44:06
【问题描述】:

我有一个包含 3 个元素的数组,我想根据可能性选择一个并将其添加到另一个数组中。

例如,第 1 号有 5% 的机会被选中,第 2 号有 60% 的机会被选中,第 3 号有 35% 的机会被选中。

arr = [{:num=>1, :diff=>-29}, {:num=>2, :diff=>5}, {:num=>3, :diff=>25}]

我从 stackoverflow 中找到了以下方法,只是想知道这是否可行?还是有别的办法?

def get_num(arr)
    case rand(100) + 1
    when 1..5
        p arr[0]
    when 6..65
        p arr[1]
    when 66..100
        p arr[2]
    end
end

get_num(arr)


谢谢!

【问题讨论】:

  • @steenslag,应该是66,我已经编辑了代码。谢谢!

标签: arrays ruby


【解决方案1】:

您的代码很好,但这里有另外两种方法。

使用累积分布函数(“CDF”)

CDF = [[0.05,0], [0.05+0.60,1], [0.5+0.60+0.35,2]]
  #=> [[0.05,0], [0.65,1], [1.0,2]]
def get_num(arr)
  n = rand
  arr[CDF.find { |mx,_idx| n <= mx }.last]
end
arr = [{:num=>1, :diff=>-29}, {:num=>2, :diff=>5}, {:num=>3, :diff=>25}]
get_num(arr)
  #=> {:num=>2, :diff=>5}
get_num(arr)
  #=> {:num=>2, :diff=>5}
get_num(arr)
  #=> {:num=>3, :diff=>25}
get_num(arr)
  #=> {:num=>1, :diff=>-29}
get_num(arr)
  #=> {:num=>2, :diff=>5}

假设:

n = rand
  #=> 0.5385005480168696

然后

a = CDF.find { |mx,_idx| n <= mx }
  #=> [0.65,1]
i = a.last
  #=> 1
arr[i]
  #=> {:num=>2, :diff=>5}

请注意,我遵循了以下约定:find 的第二个块变量 (_idx) 的名称使用下划线开头,以向读者表明该块变量未用于块计算。通常只使用underscore (_)。

现在考虑arr 的每个元素在进行n 抽奖时被随机抽出的次数:

def outcome_fractions(arr, n)
  n.times
   .with_object(Hash.new(0)) { |_,h| h[get_num(arr)] += 1 }
   .transform_values { |v| v.fdiv(n) }
end

从索引数组中随机选择

outcome_fractions(arr, 1_000)
  #=> {{:num=>2, :diff=>5}  =>0.612,
  #    {:num=>3, :diff=>25} =>0.328,
  #    {:num=>1, :diff=>-29}=>0.06}
outcome_fractions(arr, 100_000)
  #=> {{:num=>3, :diff=>25} =>0.34818,
  #    {:num=>1, :diff=>-29}=>0.04958,
  #    {:num=>2, :diff=>5}  =>0.60224}

请注意,随着样本量的增加,随机抽取的每个散列的比例接近其指定的总体概率(尽管“伪随机”抽取并不是真正随机的)。

不要关心outcome_fractions 的工作原理。


这是另一种更有效的方式(因为它不使用find,它执行线性搜索)但使用更多内存。

CHOICE = [*[0]*5, *[1]*60, *[2]*35]
  #=> [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  #    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  #    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  #    1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
  #    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
  #    2, 2, 2, 2, 2]
def get_num(arr)
  arr[CHOICE[rand(100)]]
end
  #=> {{:num=>2, :diff=>5} =>0.60029,
  #    {:num=>3, :diff=>25}=>0.35022,
  #    {:num=>1, :diff=>-29}=>0.04949}

注意:

[*[0]*5, *[1]*60, *[2]*35]

产生与

相同的数组
[[0]*5, [1]*60, [2]*35].flatten

*[0]*5 中的第一个*splat operator;第二个是方法Array#*[0]*5 #=&gt; [0,0,0,0,0] 首先被评估。

CHOICE 有 100 个元素。如果这三个概率分别是 0.0480.6040.348CHOICE 将有 10**3 #=&gt; 1_000 元素(48 零、604 一个和 348 二)。

【讨论】:

  • 谢谢卡里!我需要阅读第一个几次以确保我理解它。我喜欢方法 2 的简单性。
  • 我更喜欢@Stefan 的建议使用arr[CHOICE.sample] 而不是arr[CHOICE[rand(100)]],我想提请注意...
  • ...@steenstag 对 Stefan 回答的评论,这是一个很好的建议。
【解决方案2】:

这是 Cary 出色答案的一个小变化/补充。

您可以让 Ruby 根据初始概率为您构建它,而不是自己计算累积总和:

probs = [5, 60, 35]

sum = 0
sums = probs.map { |x| sum += x }
#=> [5, 65, 100]

我们现在可以计算一个介于 0 和总和之间的随机数,并找到对应的索引:

r = rand(sum)                  #=> 37
sums.find_index { |i| r < i }  #=> 1

请注意,初始概率的总和不必为 100。您也可以使用 [5, 60, 35] 代替:

probs = [1, 12, 7]

你可以把上面的代码包装成一个方法:

def random_index(*probs)
  sum = 0
  sums = probs.map { |x| sum += x }
  r = rand(sum)
  sums.find_index { |i| r < i }
end

random_index(5, 60, 35) #=> 1
random_index(5, 60, 35) #=> 1
random_index(5, 60, 35) #=> 2

您还可以让该方法返回一个可重复使用的 proc / lambda:

def random_index_proc(*probs)
  sum = 0
  sums = probs.map { |x| sum += x }
  -> {
    r = rand(sum)
    sums.find_index { |i| r < i }
  }
end

prc = random_index_proc(5, 60, 35)

prc.call #=> 1
prc.call #=> 1
prc.call #=> 0

最后同样重要的是,您还可以通过这种方式预填充数组:(使用 Cary 的命名约定)

CHOICE = [5, 60, 35].flat_map.with_index { |v, i| [i] * v }

并通过以下方式获取随机元素:

def get_num(arr)
  arr[CHOICE.sample]
end

为了保持数组较小,您应该更喜欢[1, 12, 7](20 个元素)而不是[5, 60, 35](100 个元素)。在gcd 的帮助下,您甚至不必自己计算:

probs = [5, 60, 35]

gcd = probs.reduce { |a, b| a.gcd(b) }
#=> 5

probs.map { |i| i / gcd }
#=> [1, 12, 7]

【讨论】:

  • 当有很多初始概率时,您甚至可以使用sums.bsearch_index { |i| r &lt; i } 进行二分搜索。
  • @Stefan 感谢您的详细解释!我会在我的项目中尝试一下 :) 谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-23
  • 2020-03-20
  • 1970-01-01
相关资源
最近更新 更多