weighted-random 0.0.4__tar.gz

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.
@@ -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,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
+ [![Tests](https://github.com/no-study-no-life/weighted_random/actions/workflows/test.yml/badge.svg)](https://github.com/no-study-no-life/weighted_random/actions/workflows/test.yml)
25
+ [![Python Version](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://www.python.org/)
26
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,150 @@
1
+ # WeightedRandom · 带权重的随机数生成工具
2
+
3
+ [![Tests](https://github.com/no-study-no-life/weighted_random/actions/workflows/test.yml/badge.svg)](https://github.com/no-study-no-life/weighted_random/actions/workflows/test.yml)
4
+ [![Python Version](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://www.python.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+
7
+ 一个简易的 Python 随机库: 根据给定的非负整数权重, 随机选择对应位置的元素.
8
+
9
+ 尽量减少 `random` 的调用.
10
+
11
+ 内部 **完全使用整数** 实现, 避免浮点数误差.
12
+
13
+ 目前支持:
14
+ - 单次带权取样.
15
+ - 无重复多次带权/均匀取样.
16
+ - 嵌套权重的一次性展开取样(如: 先选星级, 再选 UP/非UP, 最后选角色).
17
+
18
+ ## 时空复杂度
19
+
20
+ #### 无重复多次均匀取样(function: random_pick):
21
+
22
+ 单次调用时间复杂度 $\mathcal{O}(n)$ , 空间复杂度 $\mathcal{O}(n)$ .
23
+ 至多调用 `n` 次 `random_int`.
24
+
25
+ #### 无需多次带权取样(初始化参数 `pick = False`):
26
+
27
+ 初始化时间复杂度 $\mathcal{O}(n)$ , 空间复杂度 $\mathcal{O}(n)$ .
28
+ 之后每次调用获取一个随机数的时间复杂度为 $\mathcal{O}(1)$ .
29
+
30
+ #### 需要使用多次带权取样(初始化参数 `pick = True`):
31
+
32
+ 初始化时间复杂度 $\mathcal{O}(n \cdot \log n)$ , 空间复杂度 $\mathcal{O}(n)$ .
33
+
34
+ 获取一个随机数的时间复杂度不变, 为 $\mathcal{O}(1)$ .
35
+ 获取 `k` 个无重复位置的时间复杂度为 $\mathcal{O}(n + k \cdot \log^2 n)$ .
36
+
37
+ ## Examples
38
+
39
+ #### 快速生成随机数
40
+ ```python
41
+ from weighted_random import random_int
42
+
43
+ result = random_int(10) # 需要确保参数大于0.
44
+ print(result) # 随机输出 0 - 9的整数
45
+ ```
46
+
47
+ #### 均匀随机取样
48
+ ```python
49
+ from weighted_random import random_pick
50
+
51
+ src = ["red", "orange", "yellow", "green"]
52
+ n = 2
53
+ result = random_pick(src, n)
54
+ print(result) # src 中任意 2 个元素的列表. 不检查 src 内是否有重复.
55
+ ```
56
+
57
+ #### 按单个权重抽取
58
+ ```python
59
+ from weighted_random import WeightedWheel
60
+
61
+ wheel = ["red", "orange", "yellow", "green"]
62
+ weight = [0, 3, 1, 2]
63
+ m = WeightedWheel(wheel, weight)
64
+
65
+ result = m.get() # or: result = m()
66
+ # result为 "orange"/"yellow"/"green"中的一个;
67
+ # 由于"red"是0权重, 不会被选择.
68
+ ```
69
+
70
+ #### 多个权重选择一个抽取
71
+ ```python
72
+ from weighted_random import WeightedWheelBulk
73
+
74
+ wheel = [0, 1, 2, 3, 4, 5]
75
+ weights = [
76
+ [1, 1, 1, 1, 1, 1],
77
+ [0, 1, 2, 3, 4, 5],
78
+ [1, 0, 0, 0, 0, 1],
79
+ ]
80
+
81
+ m = WeightedWheelBulk(wheel, weights)
82
+
83
+ result1 = m.get(0) # result1将在0-5随机选取;
84
+ result2 = m.get(1) # result2将在1-4选取, 且选择的概率逐渐增加
85
+ ```
86
+
87
+ #### 单权重无重复选取
88
+ ```python
89
+ from weighted_random import WeightedWheel
90
+
91
+ wheel = ["red", "orange", "yellow", "green"]
92
+ weight = [0, 3, 1, 2]
93
+ m = WeightedWheel(wheel, weight, pick=True)
94
+
95
+ result = m.pick(2)
96
+ # result是一个列表, 可能包含 "orange"/"yellow"/"green" 的任意两个
97
+ # 出现概率为对应权重.
98
+ ```
99
+
100
+ #### 嵌套权重一次性抽取
101
+ ```python
102
+ from weighted_random import WeightedWheelTree
103
+
104
+ # 先按星级选, 再按 UP/非UP 选, 最后按角色权重选;
105
+ # 每一项是 (key, 当前层级权重, 子节点列表), 没有子节点则为叶子(即最终结果).
106
+ config = [
107
+ ("SSR", 1, [
108
+ ("UP", 7, [("角色A", 1), ("角色B", 1)]),
109
+ ("非UP", 3, [("角色C", 1), ("角色D", 3), ("角色E", 6)]),
110
+ ]),
111
+ ("SR", 9, [("角色F", 1), ("角色G", 1)]),
112
+ ]
113
+
114
+ m = WeightedWheelTree(config)
115
+ result = m.get() # 一次取样即可得到最终角色;
116
+ key, path = m.draw() # 同时拿到层级路径(如 ("SSR", "UP", "角色A")), 可判断是否歪了;
117
+
118
+ print(m.total) # 总权重;
119
+ print(m.paths) # 与 keys 一一对应的层级路径;
120
+ ```
121
+
122
+ > 默认要求所有叶子 `key` **全局唯一**, 出现重复 `key` 会抛出 `ValueError`(信息中会列出
123
+ > 该 key 及它出现的所有路径). 若同一个 `key` 确实需要出现在多个分支, 传 `allow_duplicate=True`,
124
+ > 此时每个分支各保留一份, 权重各自独立. `key` 不可哈希时无法校验唯一性, 会给出 `UserWarning`.
125
+
126
+ 也可以只生成扁平的 `key` 列表与总权重列表, 自行取样:
127
+ ```python
128
+ import bisect
129
+ from weighted_random import flatten, random_int
130
+
131
+ keys, weights = flatten(config)
132
+ # flatten 只做数学展开, 不校验唯一性: 同一个 key 出现在多个分支时会重复出现;
133
+ # 上例中 keys = ["角色A", "角色B", "角色C", "角色D", "角色E", "角色F", "角色G"]
134
+ # weights = [35, 35, 3, 9, 18, 450, 450], 总权重 1000;
135
+ # 注意 SR 分支被通分放大: 450/1000 = (9/10) * (1/2), 正是它真实的概率.
136
+
137
+ prefix = []
138
+ total = 0
139
+ for weight in weights:
140
+ total += weight
141
+ prefix.append(total)
142
+
143
+ r = random_int(total)
144
+ result = keys[bisect.bisect_right(prefix, r)]
145
+ ```
146
+
147
+ 配置项同样接受 `dict` 形式: `{"key": "SSR", "weight": 1, "children": [...]}`.
148
+
149
+ > **注意**: 嵌套权重**不能**把各层权重直接相乘. 不同分支的本层总权重不同,
150
+ > 分母并不统一, 必须逐层通分. 本库内部使用整数通分, 与"逐层多次取样"完全等价.
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "weighted_random"
3
+ description = "A module for fast and explicit random with weight."
4
+
5
+ readme = "README.md"
6
+ license = "MIT"
7
+
8
+ # Classifiers for your package. PyPI displays these on its page.
9
+ # See https://pypi.org/classifiers/ for all of the possibilities.
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Programming Language :: Python",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.9",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ ]
21
+
22
+ requires-python = ">= 3.9"
23
+
24
+ # State that the version will be defined dynamically below.
25
+ dynamic = ["version"]
26
+
27
+ [project.urls]
28
+ "Source code" = "https://github.com/no-study-no-life/weighted_random"
29
+ "Issue tracker" = "https://github.com/no-study-no-life/weighted_random/issues"
30
+
31
+ [tool.setuptools.dynamic]
32
+ version.attr = "weighted_random.__version__"
33
+
34
+ [build-system]
35
+ requires = ["setuptools>=80"]
36
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+