weighted-random 0.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- weighted_random/__init__.py +17 -0
- weighted_random/binary_index_tree.py +110 -0
- weighted_random/tree.py +226 -0
- weighted_random/utils.py +157 -0
- weighted_random/weighted_random.py +186 -0
- weighted_random-0.0.4.dist-info/METADATA +171 -0
- weighted_random-0.0.4.dist-info/RECORD +10 -0
- weighted_random-0.0.4.dist-info/WHEEL +5 -0
- weighted_random-0.0.4.dist-info/licenses/LICENSE +21 -0
- weighted_random-0.0.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .binary_index_tree import BinaryIndexTree
|
|
2
|
+
from .tree import (
|
|
3
|
+
Node,
|
|
4
|
+
WeightedWheelTree,
|
|
5
|
+
flatten,
|
|
6
|
+
)
|
|
7
|
+
from .utils import random_int, random_pick
|
|
8
|
+
from .weighted_random import (
|
|
9
|
+
WeightedRandom,
|
|
10
|
+
WeightedRandomBulk,
|
|
11
|
+
WeightedWheel,
|
|
12
|
+
WeightedWheelBulk,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__version__ = "0.0.4"
|
|
16
|
+
|
|
17
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from .utils import random_int
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"BinaryIndexTree",
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _low_bit(x):
|
|
9
|
+
# 获取最低位. 假定 `x` > 0.
|
|
10
|
+
return x & (-x)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BinaryIndexTree:
|
|
14
|
+
""" 接受一个整数数组, 生成对应的 前缀和 树状数组.
|
|
15
|
+
支持单点更新, 区间求和.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
data (list[int]): 原始整数列表. 保证不会对其进行修改.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
origin (list[int]): 原始数组. 方便快速更新数据.
|
|
22
|
+
data (list[int]): 内部维护的树状数组.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__slots__ = ("data", "ori")
|
|
27
|
+
|
|
28
|
+
def __init__(self, data: list[int]):
|
|
29
|
+
n = len(data)
|
|
30
|
+
arr = [0] * (n + 1)
|
|
31
|
+
|
|
32
|
+
for i in range(1, n + 1):
|
|
33
|
+
arr[i] += data[i - 1]
|
|
34
|
+
j = _low_bit(i) + i
|
|
35
|
+
if j <= n:
|
|
36
|
+
arr[j] += arr[i]
|
|
37
|
+
|
|
38
|
+
self.data = arr
|
|
39
|
+
self.ori = data
|
|
40
|
+
|
|
41
|
+
def query(self, right: int) -> int:
|
|
42
|
+
""" 查询区间 `[0, right]` 的前缀和. 含 `right` 位置.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
right (int): 区间右端点(0 base, 闭区间);
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
int: 区间 `[0, right]` 的前缀和.
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
right += 1 # 内部实际 1 base
|
|
52
|
+
ans = 0
|
|
53
|
+
while right > 0:
|
|
54
|
+
ans += self.data[right]
|
|
55
|
+
right &= right - 1
|
|
56
|
+
return ans
|
|
57
|
+
|
|
58
|
+
def __len__(self) -> int:
|
|
59
|
+
return len(self.ori)
|
|
60
|
+
|
|
61
|
+
def set(self, index: int, value: int):
|
|
62
|
+
""" 更新位于索引 `index` 的值为 `value`.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
index (int): 原始数据所在索引. 0 base.
|
|
66
|
+
value (int): 需要更新的目标值.
|
|
67
|
+
"""
|
|
68
|
+
ori = self.ori[index]
|
|
69
|
+
diff = value - ori
|
|
70
|
+
index += 1 # 内部实际 1 base
|
|
71
|
+
n = len(self.data)
|
|
72
|
+
while index < n:
|
|
73
|
+
self.data[index] += diff
|
|
74
|
+
index += _low_bit(index)
|
|
75
|
+
|
|
76
|
+
def pick(self, count: int) -> list[int]:
|
|
77
|
+
"""
|
|
78
|
+
依据权重, 选择 `count` 个位置.
|
|
79
|
+
|
|
80
|
+
Note:
|
|
81
|
+
当 `count` 大于非0权重位置个数时, 将使用位置0填充.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
count (int): 需要选择的位置个数.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
list[int]: 选择的位置列表.
|
|
88
|
+
"""
|
|
89
|
+
data = self.data[:]
|
|
90
|
+
n = len(self.ori)
|
|
91
|
+
ans = [0] * count
|
|
92
|
+
total = self.query(n - 1)
|
|
93
|
+
|
|
94
|
+
for i in range(count):
|
|
95
|
+
if total <= 0:
|
|
96
|
+
break
|
|
97
|
+
r = random_int(total) + 1
|
|
98
|
+
left, right = 0, n
|
|
99
|
+
while left < right:
|
|
100
|
+
mid = (left + right) // 2
|
|
101
|
+
if self.query(mid) < r:
|
|
102
|
+
left = mid + 1
|
|
103
|
+
else:
|
|
104
|
+
right = mid
|
|
105
|
+
ans[i] = left
|
|
106
|
+
total -= self.ori[left]
|
|
107
|
+
self.set(left, 0)
|
|
108
|
+
self.data = data
|
|
109
|
+
return ans
|
|
110
|
+
|
weighted_random/tree.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from collections.abc import Hashable
|
|
3
|
+
from math import gcd
|
|
4
|
+
from typing import Any, NamedTuple, Optional
|
|
5
|
+
|
|
6
|
+
from .weighted_random import WeightedWheel
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Node",
|
|
10
|
+
"flatten",
|
|
11
|
+
"WeightedWheelTree",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Node(NamedTuple):
|
|
16
|
+
""" 嵌套权重配置中的一个节点.
|
|
17
|
+
|
|
18
|
+
Note:
|
|
19
|
+
转换后的扁平权重是逐层通分得到的, 与"先按本层权重选分支, 再在分支内部
|
|
20
|
+
按子权重选择"的结果完全等价(全程整数, 无浮点误差).
|
|
21
|
+
**不能**把各层权重直接相乘, 因为不同分支的本层总权重不同, 分母并不统一.
|
|
22
|
+
|
|
23
|
+
``key`` 在**叶子**节点上代表最终结果; 在**内部**节点上仅作为层级路径的
|
|
24
|
+
标签, 不参与结果输出.
|
|
25
|
+
``children`` 为 ``None`` 或空列表时, 该节点视为叶子.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
key (Hashable): 节点标识.
|
|
29
|
+
weight (int): 该节点在**当前层级**中的权重, 要求非负整数.
|
|
30
|
+
children (Optional[list]): 子节点列表, 元素同样接受 `Node`/`dict`/`tuple`.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
key: Hashable
|
|
35
|
+
weight: int
|
|
36
|
+
children: Optional[list] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _path_text(path: tuple) -> str:
|
|
40
|
+
""" 把层级路径格式化为便于阅读的字符串. """
|
|
41
|
+
return "/".join(str(key) for key in path) if path else "<root>"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _as_node(item) -> Node:
|
|
45
|
+
""" 把 `Node`/`dict`/`tuple` 统一转换为 `Node`. """
|
|
46
|
+
if isinstance(item, Node):
|
|
47
|
+
return item
|
|
48
|
+
|
|
49
|
+
if isinstance(item, dict):
|
|
50
|
+
try:
|
|
51
|
+
key, weight = item["key"], item["weight"]
|
|
52
|
+
except KeyError as err:
|
|
53
|
+
raise KeyError(f"node dict must contain 'key' and 'weight', got: {sorted(item)}") from err
|
|
54
|
+
return Node(key, weight, item.get("children"))
|
|
55
|
+
|
|
56
|
+
if isinstance(item, (tuple, list)):
|
|
57
|
+
if len(item) == 2:
|
|
58
|
+
return Node(item[0], item[1], None)
|
|
59
|
+
if len(item) == 3:
|
|
60
|
+
return Node(item[0], item[1], item[2])
|
|
61
|
+
raise ValueError(f"node tuple must have 2 or 3 items, got {len(item)}: {item!r}")
|
|
62
|
+
|
|
63
|
+
raise TypeError(f"cannot parse node from {type(item).__name__}: {item!r}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _build_group(group, prefix: tuple) -> tuple[list[tuple], int]:
|
|
67
|
+
""" 展开一个兄弟节点组.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
tuple: 叶子条目列表 ``[(key, path, weight), ...]`` 与该组的总权重.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
parts = []
|
|
74
|
+
weight_sum = 0
|
|
75
|
+
for item in group:
|
|
76
|
+
node = _as_node(item)
|
|
77
|
+
path = prefix + (node.key, )
|
|
78
|
+
weight = node.weight
|
|
79
|
+
if not isinstance(weight, int):
|
|
80
|
+
raise TypeError(f"weight must be int, got {type(weight).__name__} at {_path_text(path)}")
|
|
81
|
+
if weight < 0:
|
|
82
|
+
raise ValueError(f"negative weight at {_path_text(path)}")
|
|
83
|
+
if weight == 0:
|
|
84
|
+
continue
|
|
85
|
+
weight_sum += weight
|
|
86
|
+
|
|
87
|
+
if node.children:
|
|
88
|
+
entries, subtotal = _build_group(node.children, path)
|
|
89
|
+
else:
|
|
90
|
+
entries, subtotal = [(node.key, path, 1)], 1
|
|
91
|
+
parts.append((weight, entries, subtotal))
|
|
92
|
+
|
|
93
|
+
if weight_sum <= 0:
|
|
94
|
+
raise ValueError(f"sum of weights must be positive at {_path_text(prefix)}")
|
|
95
|
+
|
|
96
|
+
# 各子树返回的是"组内相对权重", 分母为各自的 subtotal;
|
|
97
|
+
# 取这些 subtotal 的最小公倍数作为公共分母 common(用 lcm 而非乘积, 抑制权重膨胀),
|
|
98
|
+
# 每个子树再乘 common // subtotal 折算到同一分母, 之后才能乘上本层权重合并;
|
|
99
|
+
common = 1
|
|
100
|
+
for _weight, _entries, subtotal in parts:
|
|
101
|
+
common = common // gcd(common, subtotal) * subtotal
|
|
102
|
+
|
|
103
|
+
out = []
|
|
104
|
+
for weight, entries, subtotal in parts:
|
|
105
|
+
factor = weight * (common // subtotal)
|
|
106
|
+
out.extend((key, path, value * factor) for key, path, value in entries)
|
|
107
|
+
|
|
108
|
+
# 整体约分, 抑制权重膨胀;
|
|
109
|
+
divisor = 0
|
|
110
|
+
for _key, _path, value in out:
|
|
111
|
+
divisor = gcd(divisor, value)
|
|
112
|
+
if divisor > 1:
|
|
113
|
+
out = [(key, path, value // divisor) for key, path, value in out]
|
|
114
|
+
|
|
115
|
+
return out, sum(value for _key, _path, value in out)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _flatten(config) -> tuple[list[tuple], int]:
|
|
119
|
+
""" 展开嵌套配置, 返回 ``[(key, path, weight), ...]`` 与总权重. """
|
|
120
|
+
if not isinstance(config, (list, tuple)):
|
|
121
|
+
raise TypeError(f"config must be a list of nodes, got {type(config).__name__}")
|
|
122
|
+
if len(config) == 0:
|
|
123
|
+
raise ValueError("empty config!")
|
|
124
|
+
return _build_group(config, ())
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def flatten(config) -> tuple[list, list[int]]:
|
|
128
|
+
""" 把嵌套权重配置展开为扁平的 key 列表与总权重列表.
|
|
129
|
+
|
|
130
|
+
Note:
|
|
131
|
+
同一个 key 出现在多个分支时, 会在结果中重复出现, 权重各自独立.
|
|
132
|
+
展开后的结果可直接用于 `WeightedWheel`, 一次取样即可得到最终结果.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
config (list): 嵌套配置, 元素接受 `Node`/`dict`/`tuple`.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
tuple: ``(keys, weights)``, 两者一一对应且等长.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
TypeError: ``config`` 不是列表, 节点格式无法解析, 或权重不是整数.
|
|
142
|
+
ValueError: ``config`` 为空, 节点元组长度不为2/3, 出现负权重, 或某一层权重全为0.
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
entries, _total = _flatten(config)
|
|
146
|
+
return [key for key, _path, _weight in entries], [weight for _key, _path, weight in entries]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _check_duplicate(entries: list[tuple]):
|
|
150
|
+
""" 校验所有叶子 key 全局唯一.
|
|
151
|
+
|
|
152
|
+
Note:
|
|
153
|
+
key 不可哈希时无法校验, 给出 UserWarning 后放行.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: 存在重复的 key, 信息中列出该 key 及它出现的所有路径.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
seen = {}
|
|
160
|
+
try:
|
|
161
|
+
for key, path, _weight in entries:
|
|
162
|
+
seen.setdefault(key, []).append(path)
|
|
163
|
+
except TypeError: # key 不可哈希, 无法校验;
|
|
164
|
+
warnings.warn("keys are not hashable, duplicate keys cannot be checked", stacklevel=3)
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
duplicates = [(key, paths) for key, paths in seen.items() if len(paths) > 1]
|
|
168
|
+
if duplicates:
|
|
169
|
+
detail = "; ".join(
|
|
170
|
+
f"{key!r} at {', '.join(_path_text(path) for path in paths)}"
|
|
171
|
+
for key, paths in duplicates
|
|
172
|
+
)
|
|
173
|
+
raise ValueError(f"duplicate keys found: {detail}")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class WeightedWheelTree(WeightedWheel):
|
|
177
|
+
""" 接受嵌套的权重配置, 展开为扁平的带权列表后取样.
|
|
178
|
+
`WeightedWheel` 的嵌套版本.
|
|
179
|
+
|
|
180
|
+
Note:
|
|
181
|
+
展开过程全程使用整数, 与逐层多次取样完全等价; 之后每次 `get` 的
|
|
182
|
+
时间复杂度仍为 O(1).
|
|
183
|
+
|
|
184
|
+
默认要求所有叶子 key 全局唯一, 出现重复 key 时抛出 ValueError;
|
|
185
|
+
若同一个 key 确实需要在多个分支出现, 设置 ``allow_duplicate = True``
|
|
186
|
+
保留每个分支一份(此时 `pick` 可能返回重复的 key).
|
|
187
|
+
key 不可哈希时无法校验唯一性, 会给出 UserWarning.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
config (list): 嵌套配置, 元素接受 `Node`/`dict`/`tuple`.
|
|
191
|
+
pick (bool): 是否需要支持无重复取样. 默认为 `False`.
|
|
192
|
+
allow_duplicate (bool): 是否允许出现重复的 key. 默认为 `False`.
|
|
193
|
+
|
|
194
|
+
Attributes:
|
|
195
|
+
keys (list): 参与取样的 key 列表.
|
|
196
|
+
weights (list[int]): 与 `keys` 一一对应的绝对权重列表.
|
|
197
|
+
paths (list[tuple]): 与 `keys` 一一对应的层级路径.
|
|
198
|
+
total (int): 总权重.
|
|
199
|
+
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
__slots__ = ("keys", "weights", "paths", "total")
|
|
203
|
+
|
|
204
|
+
def __init__(self, config, pick: bool = False, allow_duplicate: bool = False):
|
|
205
|
+
entries, total = _flatten(config)
|
|
206
|
+
if not allow_duplicate:
|
|
207
|
+
_check_duplicate(entries)
|
|
208
|
+
|
|
209
|
+
keys, paths, weights = map(list, zip(*entries))
|
|
210
|
+
self.keys = keys
|
|
211
|
+
self.weights = weights
|
|
212
|
+
self.paths = paths
|
|
213
|
+
self.total = total
|
|
214
|
+
super().__init__(keys, weights, pick)
|
|
215
|
+
|
|
216
|
+
def draw(self) -> tuple[Any, tuple]:
|
|
217
|
+
""" 随机获取一个 key, 同时返回其层级路径(例如 ``("SSR", "UP", "角色A")``). """
|
|
218
|
+
index = self._get_index()
|
|
219
|
+
return self.wheel[index], self.paths[index]
|
|
220
|
+
|
|
221
|
+
def __repr__(self) -> str:
|
|
222
|
+
title = f"{self.__class__.__name__}:"
|
|
223
|
+
total = f"\ttotal: {self.total}"
|
|
224
|
+
keys = f"\tkeys: {self.keys}"
|
|
225
|
+
weights = f"\tweights: {self.weights}"
|
|
226
|
+
return "\n".join([title, total, keys, weights])
|
weighted_random/utils.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import random
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"create_table",
|
|
7
|
+
"random_int",
|
|
8
|
+
"random_pick",
|
|
9
|
+
"random_by_table",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_table(weight: list[int]) -> tuple[int, list[int], list[int]]:
|
|
14
|
+
"""
|
|
15
|
+
依据传入的权重, 创建用于方便索引的数据表.
|
|
16
|
+
|
|
17
|
+
Note:
|
|
18
|
+
不允许包含负数权重. 不允许列表为空或权重全部为0.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
weight (list[int]): 每一个位置的权重列表.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
tuple: avg, probs, alias.
|
|
25
|
+
- avg (int): 计算后的权重均值.
|
|
26
|
+
- probs (list[int]): 每一个位置自身占据的权重.
|
|
27
|
+
- alias (list[int]): 如果自身没有占满权重, 则为补充权重的来源位置.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
AssertError: 在非release模式下, ``weight``符合以下任意条件:
|
|
31
|
+
- 包含负权重
|
|
32
|
+
- 列表长度为0
|
|
33
|
+
- 总权重为0
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
assert len(weight) > 0, "empty weight!"
|
|
38
|
+
assert all(v >= 0 for v in weight), "negative weight found!"
|
|
39
|
+
total = sum(weight)
|
|
40
|
+
assert total > 0, "zero weight!"
|
|
41
|
+
|
|
42
|
+
length = len(weight)
|
|
43
|
+
factor = math.gcd(total, length) # 最大公因数
|
|
44
|
+
multi = length // factor # 需要乘以的倍数, 确保权重是整数
|
|
45
|
+
less, more = [], []
|
|
46
|
+
alias = [0] * length # 别名位置;
|
|
47
|
+
probs = [0] * length # 自身概率;
|
|
48
|
+
avg = total // factor # 均值, 判断放入大还是小; (toatl * multi / length)
|
|
49
|
+
for i, x in enumerate(weight):
|
|
50
|
+
x *= multi
|
|
51
|
+
probs[i] = x
|
|
52
|
+
alias[i] = i;
|
|
53
|
+
if x < avg:
|
|
54
|
+
less.append(i)
|
|
55
|
+
elif avg < x:
|
|
56
|
+
more.append(i)
|
|
57
|
+
|
|
58
|
+
# 平均化;
|
|
59
|
+
while less and more:
|
|
60
|
+
l, m = less.pop(), more.pop()
|
|
61
|
+
probs[m] -= avg - probs[l] # 空缺的一定要填满, 因为只能填一个;
|
|
62
|
+
alias[l] = m # 标记对应别名;
|
|
63
|
+
# 检查剩余部分;
|
|
64
|
+
if probs[m] < avg:
|
|
65
|
+
less.append(m)
|
|
66
|
+
elif avg < probs[m]:
|
|
67
|
+
more.append(m)
|
|
68
|
+
|
|
69
|
+
return avg, probs, alias
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def random_int(to: int) -> int:
|
|
73
|
+
""" 快速获取位于 [0, to) 的随机整数.
|
|
74
|
+
|
|
75
|
+
Note:
|
|
76
|
+
需要 ``to`` > 0 . 否则会出现无限循环;
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
to (int): 产生随机数的上界, 不包含在内.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
int: [0, to)范围的随机数.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
AssertError: 在非release模式下, ``to`` > 0 不满足时.
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
assert to > 0
|
|
89
|
+
|
|
90
|
+
k = (to - 1).bit_length() # 需要python >= 3.9;
|
|
91
|
+
r = random.getrandbits(k)
|
|
92
|
+
while r >= to:
|
|
93
|
+
r = random.getrandbits(k)
|
|
94
|
+
return r
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def random_by_table(avg: int, probs: list[int], alias: list[int]) -> int:
|
|
98
|
+
""" 依据权重随机获取一个位置.
|
|
99
|
+
|
|
100
|
+
Note:
|
|
101
|
+
参数需要为 `create_table` 返回的结果.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
avg (int): 平均权重;
|
|
105
|
+
probs (list[int]): 每一个位置自身占据的权重.
|
|
106
|
+
alias (list[int]): 如果自身没有占满权重, 则为补充权重的来源位置.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
int: 随机选择的位置
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
length = len(probs)
|
|
113
|
+
# 1. 选择基础位置
|
|
114
|
+
pos = random_int(length)
|
|
115
|
+
# 2. 判断是否需要替换
|
|
116
|
+
r = random_int(avg)
|
|
117
|
+
if r >= probs[pos]:
|
|
118
|
+
pos = alias[pos]
|
|
119
|
+
return pos
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def random_pick(arr: list[Any], n: int) -> list[Any]:
|
|
123
|
+
""" 简易获取n个不同的下标对应的元素. 尽可能少使用 `random` .
|
|
124
|
+
|
|
125
|
+
Note:
|
|
126
|
+
最多输出 min(n, len(arr)) 个元素.
|
|
127
|
+
内部使用整数数组存储下标,
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
arr (list): 待选择的元素列表;
|
|
131
|
+
n (int): 需要选择的个数
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
list: 实际选择的元素列表
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
m = len(arr)
|
|
138
|
+
n = min(n, m)
|
|
139
|
+
if n == m:
|
|
140
|
+
# 快速路径: 全部取出, 直接打乱: min(n, m) 次 random.
|
|
141
|
+
result = arr[:]
|
|
142
|
+
random.shuffle(result)
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
swap = {} # 改动过的位置;
|
|
146
|
+
result = [None] * n
|
|
147
|
+
right = m
|
|
148
|
+
for i in range(n):
|
|
149
|
+
idx = random_int(right)
|
|
150
|
+
right -= 1
|
|
151
|
+
tgt = swap.get(idx, idx) # 实际位置;
|
|
152
|
+
tail = swap.get(right, right) # 尾部备用位置;
|
|
153
|
+
swap[idx] = tail # 写回实际交换后位置;
|
|
154
|
+
result[i] = arr[tgt]
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import bisect
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from .binary_index_tree import BinaryIndexTree
|
|
4
|
+
from .utils import create_table, random_int, random_by_table
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class WeightedRandom:
|
|
8
|
+
""" 接受给定权重列表, 依据权重每次随机获取一个对应的位置.
|
|
9
|
+
|
|
10
|
+
Note:
|
|
11
|
+
权重要求是整数.
|
|
12
|
+
权重列表不能为空, 不能包含负权重; 总权重不能为0.
|
|
13
|
+
如果需要支持pick, 则接受权重后, 原始权重不能修改.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
weight (list[int]): 权重列表.
|
|
17
|
+
pick (bool): 是否需要支持无重复取样. 默认为 `False`.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
avg (int): 计算后的权重均值.
|
|
21
|
+
probs (list[int]): 每一个位置自身占据的权重.
|
|
22
|
+
alias (list[int]): 如果自身没有占满权重, 则为补充权重的来源位置.
|
|
23
|
+
bit (object): 树状数组. 支持快速无重复取样.
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
__slots__ = ("avg", "probs", "alias", "bit")
|
|
28
|
+
|
|
29
|
+
def __init__(self, weight: list[int], pick: bool = False):
|
|
30
|
+
avg, probs, alias = create_table(weight)
|
|
31
|
+
self.avg = avg
|
|
32
|
+
self.probs = probs
|
|
33
|
+
self.alias = alias
|
|
34
|
+
if pick:
|
|
35
|
+
self.bit = BinaryIndexTree(weight)
|
|
36
|
+
else:
|
|
37
|
+
self.bit = None
|
|
38
|
+
|
|
39
|
+
def _get_index(self) -> int:
|
|
40
|
+
""" 依据权重随机获取一个位置.
|
|
41
|
+
|
|
42
|
+
Note:
|
|
43
|
+
内部使用. 与 `get` 分开是为了避免子类重写 `get` 后, 父类内部调用
|
|
44
|
+
到子类版本(子类 `get` 返回的是值, 不是位置).
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
return random_by_table(self.avg, self.probs, self.alias)
|
|
48
|
+
|
|
49
|
+
def get(self) -> int:
|
|
50
|
+
""" 依据权重随机获取一个位置 """
|
|
51
|
+
return self._get_index()
|
|
52
|
+
|
|
53
|
+
def pick(self, count: int) -> list[int]:
|
|
54
|
+
""" 依据权重, 选择 `count` 个位置.
|
|
55
|
+
|
|
56
|
+
Note:
|
|
57
|
+
当 初始化时 `pick` 为 False(默认) 时, 无法使用此函数(抛出ValueError).
|
|
58
|
+
当 `count` 大于非0权重位置个数时, 将使用位置0填充.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
count (int): 需要选择的位置个数.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
list[int]: 选择的位置列表.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
if self.bit is None:
|
|
68
|
+
raise ValueError(f"if you want to use `pick`, set `pick` = True when init {self.__class__.__name__}.")
|
|
69
|
+
|
|
70
|
+
if count == 1:
|
|
71
|
+
# 单次取样直接返回, 避免树状数组的 O(n) 复制;
|
|
72
|
+
return [self._get_index()]
|
|
73
|
+
|
|
74
|
+
return self.bit.pick(count)
|
|
75
|
+
|
|
76
|
+
def __repr__(self) -> str:
|
|
77
|
+
title = f"{self.__class__.__name__}:"
|
|
78
|
+
avg = f"\tavg: {self.avg}"
|
|
79
|
+
probs = f"\tprobs: {self.probs}"
|
|
80
|
+
alias = f"\talias: {self.alias}"
|
|
81
|
+
return "\n".join([title, avg, probs, alias])
|
|
82
|
+
|
|
83
|
+
def __call__(self) -> int:
|
|
84
|
+
return self.get()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class WeightedRandomBulk:
|
|
88
|
+
""" 接受一组权重列表. `WeightedRandom` 的批量版本. """
|
|
89
|
+
|
|
90
|
+
__slots__ = ("info", "bits")
|
|
91
|
+
|
|
92
|
+
def __init__(self, weight_list: list[list[int]], pick: bool = False):
|
|
93
|
+
self.info = [create_table(w) for w in weight_list]
|
|
94
|
+
if pick:
|
|
95
|
+
self.bits = [BinaryIndexTree(w) for w in weight_list]
|
|
96
|
+
else:
|
|
97
|
+
self.bits = None
|
|
98
|
+
|
|
99
|
+
def _get_index(self, index: int = 0) -> int:
|
|
100
|
+
""" 根据指定的权重选项, 返回随机选择的位置.
|
|
101
|
+
|
|
102
|
+
Note:
|
|
103
|
+
内部使用. 与 `get` 分开的原因同 `WeightedRandom`.
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
avg, probs, alias = self.info[index]
|
|
107
|
+
return random_by_table(avg, probs, alias)
|
|
108
|
+
|
|
109
|
+
def get(self, index: int = 0) -> int:
|
|
110
|
+
""" 根据指定的权重选项, 返回随机选择的位置 """
|
|
111
|
+
return self._get_index(index)
|
|
112
|
+
|
|
113
|
+
def pick(self, count: int, index: int = 0) -> list[int]:
|
|
114
|
+
"""
|
|
115
|
+
根据指定的权重选择, 返回无重复选择的位置.
|
|
116
|
+
同样的, 如果初始化时 `pick` 为 False, 则无法使用此函数.
|
|
117
|
+
"""
|
|
118
|
+
if self.bits is None:
|
|
119
|
+
raise ValueError(f"if you want to use `pick`, set `pick` = True when init {self.__class__.__name__}.")
|
|
120
|
+
|
|
121
|
+
if count == 1:
|
|
122
|
+
# 单次取样直接返回, 避免树状数组的 O(n) 复制;
|
|
123
|
+
return [self._get_index(index)]
|
|
124
|
+
|
|
125
|
+
return self.bits[index].pick(count)
|
|
126
|
+
|
|
127
|
+
def __call__(self, index: int = 0) -> int:
|
|
128
|
+
return self.get(index)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class WeightedWheel(WeightedRandom):
|
|
132
|
+
"""
|
|
133
|
+
接受权重, 以及每一个位置上对应的值.
|
|
134
|
+
`WeightedRandom` 的简易包装版本.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
__slots__ = ("wheel", )
|
|
138
|
+
|
|
139
|
+
def __init__(self, wheel: list, weight: list[int], pick: bool = False):
|
|
140
|
+
super().__init__(weight, pick)
|
|
141
|
+
self.wheel = wheel
|
|
142
|
+
|
|
143
|
+
def get(self):
|
|
144
|
+
""" 根据权重, 返回随机选择的值. """
|
|
145
|
+
index = super().get()
|
|
146
|
+
return self.wheel[index]
|
|
147
|
+
|
|
148
|
+
def pick(self, count: int) -> list:
|
|
149
|
+
"""
|
|
150
|
+
依据权重, 选择 `count` 个(无重复位置)随机选择的 `wheel` 值.
|
|
151
|
+
要求初始化时 `pick` 为 True
|
|
152
|
+
"""
|
|
153
|
+
indexs = super().pick(count)
|
|
154
|
+
return [self.wheel[index] for index in indexs]
|
|
155
|
+
|
|
156
|
+
def __call__(self):
|
|
157
|
+
return self.get()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class WeightedWheelBulk(WeightedRandomBulk):
|
|
161
|
+
"""
|
|
162
|
+
接受权重列表, 以及每一个位置上对应的值.
|
|
163
|
+
`WeightedRandomBulk` 的简易包装版本.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
__slots__ = ("wheel", )
|
|
167
|
+
|
|
168
|
+
def __init__(self, wheel: list, weight_list: list[list[int]], pick: bool = False):
|
|
169
|
+
super().__init__(weight_list, pick)
|
|
170
|
+
self.wheel = wheel
|
|
171
|
+
|
|
172
|
+
def get(self, index: int = 0):
|
|
173
|
+
""" 根据指定的权重选项, 返回随机选择的值 """
|
|
174
|
+
pos = super().get(index)
|
|
175
|
+
return self.wheel[pos]
|
|
176
|
+
|
|
177
|
+
def pick(self, count: int, index: int = 0) -> list:
|
|
178
|
+
"""
|
|
179
|
+
根据指定权重选项, 选择 `count` 个(无重复位置)随机选择的 wheel 值.
|
|
180
|
+
要求初始化时 `pick` 为 True
|
|
181
|
+
"""
|
|
182
|
+
indexs = super().pick(count, index)
|
|
183
|
+
return [self.wheel[index] for index in indexs]
|
|
184
|
+
|
|
185
|
+
def __call__(self, index: int = 0):
|
|
186
|
+
return self.get(index)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: weighted_random
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: A module for fast and explicit random with weight.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Source code, https://github.com/no-study-no-life/weighted_random
|
|
7
|
+
Project-URL: Issue tracker, https://github.com/no-study-no-life/weighted_random/issues
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# WeightedRandom · 带权重的随机数生成工具
|
|
23
|
+
|
|
24
|
+
[](https://github.com/no-study-no-life/weighted_random/actions/workflows/test.yml)
|
|
25
|
+
[](https://www.python.org/)
|
|
26
|
+
[](LICENSE)
|
|
27
|
+
|
|
28
|
+
一个简易的 Python 随机库: 根据给定的非负整数权重, 随机选择对应位置的元素.
|
|
29
|
+
|
|
30
|
+
尽量减少 `random` 的调用.
|
|
31
|
+
|
|
32
|
+
内部 **完全使用整数** 实现, 避免浮点数误差.
|
|
33
|
+
|
|
34
|
+
目前支持:
|
|
35
|
+
- 单次带权取样.
|
|
36
|
+
- 无重复多次带权/均匀取样.
|
|
37
|
+
- 嵌套权重的一次性展开取样(如: 先选星级, 再选 UP/非UP, 最后选角色).
|
|
38
|
+
|
|
39
|
+
## 时空复杂度
|
|
40
|
+
|
|
41
|
+
#### 无重复多次均匀取样(function: random_pick):
|
|
42
|
+
|
|
43
|
+
单次调用时间复杂度 $\mathcal{O}(n)$ , 空间复杂度 $\mathcal{O}(n)$ .
|
|
44
|
+
至多调用 `n` 次 `random_int`.
|
|
45
|
+
|
|
46
|
+
#### 无需多次带权取样(初始化参数 `pick = False`):
|
|
47
|
+
|
|
48
|
+
初始化时间复杂度 $\mathcal{O}(n)$ , 空间复杂度 $\mathcal{O}(n)$ .
|
|
49
|
+
之后每次调用获取一个随机数的时间复杂度为 $\mathcal{O}(1)$ .
|
|
50
|
+
|
|
51
|
+
#### 需要使用多次带权取样(初始化参数 `pick = True`):
|
|
52
|
+
|
|
53
|
+
初始化时间复杂度 $\mathcal{O}(n \cdot \log n)$ , 空间复杂度 $\mathcal{O}(n)$ .
|
|
54
|
+
|
|
55
|
+
获取一个随机数的时间复杂度不变, 为 $\mathcal{O}(1)$ .
|
|
56
|
+
获取 `k` 个无重复位置的时间复杂度为 $\mathcal{O}(n + k \cdot \log^2 n)$ .
|
|
57
|
+
|
|
58
|
+
## Examples
|
|
59
|
+
|
|
60
|
+
#### 快速生成随机数
|
|
61
|
+
```python
|
|
62
|
+
from weighted_random import random_int
|
|
63
|
+
|
|
64
|
+
result = random_int(10) # 需要确保参数大于0.
|
|
65
|
+
print(result) # 随机输出 0 - 9的整数
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### 均匀随机取样
|
|
69
|
+
```python
|
|
70
|
+
from weighted_random import random_pick
|
|
71
|
+
|
|
72
|
+
src = ["red", "orange", "yellow", "green"]
|
|
73
|
+
n = 2
|
|
74
|
+
result = random_pick(src, n)
|
|
75
|
+
print(result) # src 中任意 2 个元素的列表. 不检查 src 内是否有重复.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### 按单个权重抽取
|
|
79
|
+
```python
|
|
80
|
+
from weighted_random import WeightedWheel
|
|
81
|
+
|
|
82
|
+
wheel = ["red", "orange", "yellow", "green"]
|
|
83
|
+
weight = [0, 3, 1, 2]
|
|
84
|
+
m = WeightedWheel(wheel, weight)
|
|
85
|
+
|
|
86
|
+
result = m.get() # or: result = m()
|
|
87
|
+
# result为 "orange"/"yellow"/"green"中的一个;
|
|
88
|
+
# 由于"red"是0权重, 不会被选择.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### 多个权重选择一个抽取
|
|
92
|
+
```python
|
|
93
|
+
from weighted_random import WeightedWheelBulk
|
|
94
|
+
|
|
95
|
+
wheel = [0, 1, 2, 3, 4, 5]
|
|
96
|
+
weights = [
|
|
97
|
+
[1, 1, 1, 1, 1, 1],
|
|
98
|
+
[0, 1, 2, 3, 4, 5],
|
|
99
|
+
[1, 0, 0, 0, 0, 1],
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
m = WeightedWheelBulk(wheel, weights)
|
|
103
|
+
|
|
104
|
+
result1 = m.get(0) # result1将在0-5随机选取;
|
|
105
|
+
result2 = m.get(1) # result2将在1-4选取, 且选择的概率逐渐增加
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### 单权重无重复选取
|
|
109
|
+
```python
|
|
110
|
+
from weighted_random import WeightedWheel
|
|
111
|
+
|
|
112
|
+
wheel = ["red", "orange", "yellow", "green"]
|
|
113
|
+
weight = [0, 3, 1, 2]
|
|
114
|
+
m = WeightedWheel(wheel, weight, pick=True)
|
|
115
|
+
|
|
116
|
+
result = m.pick(2)
|
|
117
|
+
# result是一个列表, 可能包含 "orange"/"yellow"/"green" 的任意两个
|
|
118
|
+
# 出现概率为对应权重.
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### 嵌套权重一次性抽取
|
|
122
|
+
```python
|
|
123
|
+
from weighted_random import WeightedWheelTree
|
|
124
|
+
|
|
125
|
+
# 先按星级选, 再按 UP/非UP 选, 最后按角色权重选;
|
|
126
|
+
# 每一项是 (key, 当前层级权重, 子节点列表), 没有子节点则为叶子(即最终结果).
|
|
127
|
+
config = [
|
|
128
|
+
("SSR", 1, [
|
|
129
|
+
("UP", 7, [("角色A", 1), ("角色B", 1)]),
|
|
130
|
+
("非UP", 3, [("角色C", 1), ("角色D", 3), ("角色E", 6)]),
|
|
131
|
+
]),
|
|
132
|
+
("SR", 9, [("角色F", 1), ("角色G", 1)]),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
m = WeightedWheelTree(config)
|
|
136
|
+
result = m.get() # 一次取样即可得到最终角色;
|
|
137
|
+
key, path = m.draw() # 同时拿到层级路径(如 ("SSR", "UP", "角色A")), 可判断是否歪了;
|
|
138
|
+
|
|
139
|
+
print(m.total) # 总权重;
|
|
140
|
+
print(m.paths) # 与 keys 一一对应的层级路径;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
> 默认要求所有叶子 `key` **全局唯一**, 出现重复 `key` 会抛出 `ValueError`(信息中会列出
|
|
144
|
+
> 该 key 及它出现的所有路径). 若同一个 `key` 确实需要出现在多个分支, 传 `allow_duplicate=True`,
|
|
145
|
+
> 此时每个分支各保留一份, 权重各自独立. `key` 不可哈希时无法校验唯一性, 会给出 `UserWarning`.
|
|
146
|
+
|
|
147
|
+
也可以只生成扁平的 `key` 列表与总权重列表, 自行取样:
|
|
148
|
+
```python
|
|
149
|
+
import bisect
|
|
150
|
+
from weighted_random import flatten, random_int
|
|
151
|
+
|
|
152
|
+
keys, weights = flatten(config)
|
|
153
|
+
# flatten 只做数学展开, 不校验唯一性: 同一个 key 出现在多个分支时会重复出现;
|
|
154
|
+
# 上例中 keys = ["角色A", "角色B", "角色C", "角色D", "角色E", "角色F", "角色G"]
|
|
155
|
+
# weights = [35, 35, 3, 9, 18, 450, 450], 总权重 1000;
|
|
156
|
+
# 注意 SR 分支被通分放大: 450/1000 = (9/10) * (1/2), 正是它真实的概率.
|
|
157
|
+
|
|
158
|
+
prefix = []
|
|
159
|
+
total = 0
|
|
160
|
+
for weight in weights:
|
|
161
|
+
total += weight
|
|
162
|
+
prefix.append(total)
|
|
163
|
+
|
|
164
|
+
r = random_int(total)
|
|
165
|
+
result = keys[bisect.bisect_right(prefix, r)]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
配置项同样接受 `dict` 形式: `{"key": "SSR", "weight": 1, "children": [...]}`.
|
|
169
|
+
|
|
170
|
+
> **注意**: 嵌套权重**不能**把各层权重直接相乘. 不同分支的本层总权重不同,
|
|
171
|
+
> 分母并不统一, 必须逐层通分. 本库内部使用整数通分, 与"逐层多次取样"完全等价.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
weighted_random/__init__.py,sha256=aSVp1yrRWfGRdIIli7Cs5k5nNyzgQRATnvc6I20jvqA,282
|
|
2
|
+
weighted_random/binary_index_tree.py,sha256=EEAHR5UGlx1HfeX7AgYW4D-uaz2N29c6A5hH2EJ2PPs,2230
|
|
3
|
+
weighted_random/tree.py,sha256=fMrs0jtE6kojgZpvsAgOltRjp2_AYSFMySJKjp__zV4,7441
|
|
4
|
+
weighted_random/utils.py,sha256=NYYYbfwUPC8FPupr0Z9VTg7vOKJbXPREAPSXWzrUxIE,3675
|
|
5
|
+
weighted_random/weighted_random.py,sha256=2HOXxdSkdjTRciZkHZK7-Ac1to3kCH1fbmlw10JiT1g,5289
|
|
6
|
+
weighted_random-0.0.4.dist-info/licenses/LICENSE,sha256=IU7e6OK27StJrC9_-uMQOvfI6Sam07l0IrDY9qmI25g,1067
|
|
7
|
+
weighted_random-0.0.4.dist-info/METADATA,sha256=L1OajxrTvOeJs2bI3pDrwVDEvwX4G9WnrXAVg26Rw4Y,6002
|
|
8
|
+
weighted_random-0.0.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
9
|
+
weighted_random-0.0.4.dist-info/top_level.txt,sha256=2FbhCSIITpNO18upwJgN3-RSCPQMZCj7sjyBZ2k6Tik,16
|
|
10
|
+
weighted_random-0.0.4.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Little Guy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
weighted_random
|