请参阅以下更新...
我正在编写一个Python模拟,可以从任意目标池中为任意数量的虚构玩家分配一个目标。目标有两个不同级别或比例的稀缺性prop_high
和prop_low
,比例约为3:1。
例如,如果有16个玩家和4个目标,或8个玩家和4个目标,那么两个目标池将如下所示:
{'A': 6, 'B': 6, 'C': 2, 'D': 2}
{'A': 3, 'B': 3, 'C': 1, 'D': 1}
......目标A和B的出现次数是C和D的3倍.6 + 6 + 2 + 2 = 16,这对应于模拟中的参与者数量,这很好。
我希望有一个目标池,其数量与玩家数量相等,并且分配的目标大约是prop_high
个目标的三倍{/ 1}}。
根据粗略或近似比率构建分配算法的最佳方法是什么 - 可以处理舍入的内容?
的更新 的
假设有8名玩家,这里有2到8个目标的分布应该如何看待(prop_low
玩家加星标):
prop_high
这些数字与玩家不对应。例如,有5个目标和8个玩家,目标A和B在游泳池中占很高比例(分别为3和2),而目标C,D和E则更为罕见(每个1个)。
当目标数量奇数时, A B C D E F G H
2 6* 2
3 6* 1 1
4 3* 3* 1 1
5 3* 2* 1 1 1
6 2* 2* 1* 1 1 1
7 2* 1* 1* 1 1 1 1
8 1* 1* 1* 1* 1 1 1 1
的最后一个比其他目标少一个。随着目标数量接近玩家数量,每个prop_high
项目都会减少一个,直到最后,当池中的每个目标都有一个时。
我在下面所做的是将数量分配给池的高端和低端,然后调整高端,根据目标数量与玩家数量的接近程度减去值。它适用于8名玩家(游泳池中的目标数量总是等于8),但这就是全部。
我绝对相信有一种更好,更Pythonic的方式来处理这种算法,我很确定它是一种相对常见的设计模式。我只是不知道从哪里开始谷歌搜索找到一种更优雅的方式来处理这种结构(而不是我现在使用的蛮力方法)
prop_high
答案 0 :(得分:3)
前一段时间(好吧,两年半)我问a question我觉得这里有用。以下是我认为你可以使用的方法:首先,建立一个分配给每个目标的优先级列表。在您的示例中,目标池的前半部分(向下舍入)获得优先级3而其余部分获得优先级1,其中一种方法是
priorities = [3] * len(goals) / 2 + [1] * (len(goals) - len(goals) / 2)
当然,您可以以任何方式创建优先级列表;它不必是半个3s半。唯一的要求是所有条目都是正数。
获得列表后,将其标准化为总和等于玩家数量:
# Assuming num_players is already defined to be the number of players
normalized_priorities = [float(p) / sum(priorities) * num_players
for p in priorities]
然后应用我的问题中的一个算法将这些浮点数舍入为表示实际分配的整数。在给出的答案中,只有两种算法正确地进行舍入和满足最小方差标准:adjusted fractional distribution(包括“更新”段落)和minimizing roundoff error。方便的是,它们似乎都适用于非排序列表。以下是我的Python实现:
import math, operator
from heapq import nlargest
from itertools import izip
item1 = operator.itemgetter(1)
def floor(f):
return int(math.floor(f))
def frac(f):
return math.modf(f)[0]
def adjusted_fractional_distribution(fn_list):
in_list = [floor(f) for f in fn_list]
loss_list = [frac(f) for f in fn_list]
fsum = math.fsum(loss_list)
add_list = [0] * len(in_list)
largest = nlargest(int(round(fsum)), enumerate(loss_list),
key=lambda e: (e[1], e[0]))
for i, loss in largest:
add_list[i] = 1
return [i + a for i,a in izip(in_list, add_list)]
def minimal_roundoff_error(fn_list):
N = int(math.fsum(fn_list))
temp_list = [[floor(f), frac(f), i] for i, f in enumerate(fn_list)]
temp_list.sort(key = item1)
lower_sum = sum(floor(f) for f in fn_list)
difference = N - lower_sum
for i in xrange(len(temp_list) - difference, len(temp_list)):
temp_list[i][0] += 1
temp_list.sort(key = item2)
return [t[0] for t in temp_list]
在我的所有测试中,这两种方法完全相同,因此您可以选择使用其中一种方法。
这是一个用法示例:
>>> goals = 'ABCDE'
>>> num_players = 17
>>> priorities = [3,3,1,1,1]
>>> normalized_priorities = [float(p) / sum(priorities) * num_players
for p in priorities]
[5.666666..., 5.666666..., 1.888888..., 1.888888..., 1.888888...]
>>> minimal_roundoff_error(normalized_priorities)
[5, 6, 2, 2, 2]
如果你想将额外的玩家分配到同等优先级的一组中的第一个目标,而不是最后一个,可能最简单的方法是在应用舍入算法之前和之后反转列表。
>>> def rlist(l):
... return list(reversed(l))
>>> rlist(minimal_roundoff_error(rlist(normalized_priorities)))
[6, 5, 2, 2, 2]
现在,这可能与您期望的分布不完全匹配,因为在我的问题中,我指定了用于判断结果的“最小方差”标准。这可能不适合你的情况。您可以尝试"remainder distribution" algorithm而不是上面提到的两个中的一个,看看它是否适合您。
def remainder_distribution(fn_list):
N = math.fsum(fn_list)
rn_list = [int(round(f)) for f in fn_list]
remainder = N - sum(rn_list)
first = 0
last = len(fn_list) - 1
while remainder > 0 and last >= 0:
if abs(rn_list[last] + 1 - fn_list[last]) < 1:
rn_list[last] += 1
remainder -= 1
last -= 1
while remainder < 0 and first < len(rn_list):
if abs(rn_list[first] - 1 - fn_list[first]) < 1:
rn_list[first] -= 1
remainder += 1
first += 1
return rn_list
答案 1 :(得分:3)
我不是试图让分数正确,而是以适当的比例一次分配一个目标。这里'allocate_goals'生成器为每个低比率目标分配目标,然后分配给每个高比率目标(重复3次)。然后它重复。分配中的调用者使用itertools.islice以所需数量(播放器数量)切断此无限生成器。
import collections
import itertools
import string
def allocate_goals(prop_low, prop_high):
prop_high3 = prop_high * 3
while True:
for g in prop_low:
yield g
for g in prop_high3:
yield g
def allocate(goals, players):
letters = string.ascii_uppercase[:goals]
high_count = goals // 2
prop_high, prop_low = letters[:high_count], letters[high_count:]
g = allocate_goals(prop_low, prop_high)
return collections.Counter(itertools.islice(g, players))
for goals in xrange(2, 9):
print goals, sorted(allocate(goals, 8).items())
它产生了这个答案:
2 [('A', 6), ('B', 2)]
3 [('A', 4), ('B', 2), ('C', 2)]
4 [('A', 3), ('B', 3), ('C', 1), ('D', 1)]
5 [('A', 3), ('B', 2), ('C', 1), ('D', 1), ('E', 1)]
6 [('A', 2), ('B', 2), ('C', 1), ('D', 1), ('E', 1), ('F', 1)]
7 [('A', 2), ('B', 1), ('C', 1), ('D', 1), ('E', 1), ('F', 1), ('G', 1)]
8 [('A', 1), ('B', 1), ('C', 1), ('D', 1), ('E', 1), ('F', 1), ('G', 1), ('H', 1)]
这种方法的好处(除此之外,我认为,它很容易理解)是它很快就会变成一个随机版本。
只需将allocate_goals替换为:
def allocate_goals(prop_low, prop_high):
all_goals = prop_low + prop_high * 3
while True:
yield random.choice(all_goals)