esets-lib 0.1.0__tar.gz → 0.3.2__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.
- {esets_lib-0.1.0 → esets_lib-0.3.2}/PKG-INFO +29 -1
- {esets_lib-0.1.0 → esets_lib-0.3.2}/README.md +28 -0
- {esets_lib-0.1.0 → esets_lib-0.3.2}/esets/besets.py +1 -4
- {esets_lib-0.1.0 → esets_lib-0.3.2}/esets/cesets.py +56 -11
- {esets_lib-0.1.0 → esets_lib-0.3.2}/esets/ecombinatorics.py +150 -28
- {esets_lib-0.1.0 → esets_lib-0.3.2}/esets/eset.py +3 -1
- {esets_lib-0.1.0 → esets_lib-0.3.2}/esets/numeric.py +3 -1
- {esets_lib-0.1.0 → esets_lib-0.3.2}/pyproject.toml +1 -1
- {esets_lib-0.1.0 → esets_lib-0.3.2}/.gitignore +0 -0
- {esets_lib-0.1.0 → esets_lib-0.3.2}/LICENSE +0 -0
- {esets_lib-0.1.0 → esets_lib-0.3.2}/esets/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: esets-lib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Lazy, index-addressable enumerated sets, including a full combinatorics family (permutations, combinations, arrangements, subsets, partitions, derangements, and Cartesian products)
|
|
5
5
|
Project-URL: Homepage, https://github.com/ffavela/esets
|
|
6
6
|
Project-URL: Repository, https://github.com/ffavela/esets
|
|
@@ -50,6 +50,34 @@ still:
|
|
|
50
50
|
>>>
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
## The docs, if you want to skip ahead
|
|
54
|
+
|
|
55
|
+
This README is a long, informal walkthrough. If you'd rather get to
|
|
56
|
+
the substance directly:
|
|
57
|
+
|
|
58
|
+
* [COMBINATORICS.md](https://github.com/ffavela/esets/blob/main/COMBINATORICS.md)
|
|
59
|
+
-- the combinatorics family: permutations, combinations,
|
|
60
|
+
arrangements, subsets, integer partitions, set partitions,
|
|
61
|
+
derangements, and Cartesian products, all addressable by index with
|
|
62
|
+
no enumeration involved.
|
|
63
|
+
* [POKER.md](https://github.com/ffavela/esets/blob/main/POKER.md) --
|
|
64
|
+
several of those put to work together on one running example, a
|
|
65
|
+
deck of cards.
|
|
66
|
+
* [COMBINATORIALDB.md](https://github.com/ffavela/esets/blob/main/COMBINATORIALDB.md)
|
|
67
|
+
-- a second worked example, a shop database: comparing a raw
|
|
68
|
+
purchase log against a `set`-based, an arrangement-indexed, and a
|
|
69
|
+
combination-indexed representation, for both correctness and size.
|
|
70
|
+
* [INCLUSIONEXCLUSION.md](https://github.com/ffavela/esets/blob/main/INCLUSIONEXCLUSION.md)
|
|
71
|
+
-- the library's own counting recursion benchmarked against
|
|
72
|
+
inclusion-exclusion, with the real performance fixes that
|
|
73
|
+
benchmarking turned up.
|
|
74
|
+
* [FLOAT64S.md](https://github.com/ffavela/esets/blob/main/FLOAT64S.md)
|
|
75
|
+
-- an eset that enumerates all 64-bit floats.
|
|
76
|
+
* [TEXTENCODE.md](https://github.com/ffavela/esets/blob/main/TEXTENCODE.md)
|
|
77
|
+
-- character and word alphabets built from a small text file, encoded
|
|
78
|
+
with `Arranger` and with `Combinator`+`Permutator` by hand, as a
|
|
79
|
+
first step toward larger-text, whole-language "compression."
|
|
80
|
+
|
|
53
81
|
## What is it? (An informal introduction)
|
|
54
82
|
|
|
55
83
|
**esets** stands for enumerated sets. They can handle arbitrarily
|
|
@@ -26,6 +26,34 @@ still:
|
|
|
26
26
|
>>>
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
## The docs, if you want to skip ahead
|
|
30
|
+
|
|
31
|
+
This README is a long, informal walkthrough. If you'd rather get to
|
|
32
|
+
the substance directly:
|
|
33
|
+
|
|
34
|
+
* [COMBINATORICS.md](https://github.com/ffavela/esets/blob/main/COMBINATORICS.md)
|
|
35
|
+
-- the combinatorics family: permutations, combinations,
|
|
36
|
+
arrangements, subsets, integer partitions, set partitions,
|
|
37
|
+
derangements, and Cartesian products, all addressable by index with
|
|
38
|
+
no enumeration involved.
|
|
39
|
+
* [POKER.md](https://github.com/ffavela/esets/blob/main/POKER.md) --
|
|
40
|
+
several of those put to work together on one running example, a
|
|
41
|
+
deck of cards.
|
|
42
|
+
* [COMBINATORIALDB.md](https://github.com/ffavela/esets/blob/main/COMBINATORIALDB.md)
|
|
43
|
+
-- a second worked example, a shop database: comparing a raw
|
|
44
|
+
purchase log against a `set`-based, an arrangement-indexed, and a
|
|
45
|
+
combination-indexed representation, for both correctness and size.
|
|
46
|
+
* [INCLUSIONEXCLUSION.md](https://github.com/ffavela/esets/blob/main/INCLUSIONEXCLUSION.md)
|
|
47
|
+
-- the library's own counting recursion benchmarked against
|
|
48
|
+
inclusion-exclusion, with the real performance fixes that
|
|
49
|
+
benchmarking turned up.
|
|
50
|
+
* [FLOAT64S.md](https://github.com/ffavela/esets/blob/main/FLOAT64S.md)
|
|
51
|
+
-- an eset that enumerates all 64-bit floats.
|
|
52
|
+
* [TEXTENCODE.md](https://github.com/ffavela/esets/blob/main/TEXTENCODE.md)
|
|
53
|
+
-- character and word alphabets built from a small text file, encoded
|
|
54
|
+
with `Arranger` and with `Combinator`+`Permutator` by hand, as a
|
|
55
|
+
first step toward larger-text, whole-language "compression."
|
|
56
|
+
|
|
29
57
|
## What is it? (An informal introduction)
|
|
30
58
|
|
|
31
59
|
**esets** stands for enumerated sets. They can handle arbitrarily
|
|
@@ -17,10 +17,7 @@ class BEvens(BEset):
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class BWholesSHA256s(BEset):
|
|
20
|
-
"""A blind eset of SHA256s of the Whole numbers. It uses ascii
|
|
21
|
-
encoding since only decimal numbers are expected.
|
|
22
|
-
|
|
23
|
-
"""
|
|
20
|
+
"""A blind eset of SHA256s of the Whole numbers. It uses ascii encoding since only decimal numbers are expected."""
|
|
24
21
|
|
|
25
22
|
def direct_function(self, i):
|
|
26
23
|
return hashlib.sha256(str(i).encode('ascii')).hexdigest()
|
|
@@ -109,7 +109,9 @@ class Distinct_Permutator(Distinct_PermutatorABCMixin):
|
|
|
109
109
|
"""Delegating to eset_obj like EMixinABC, but also carrying
|
|
110
110
|
self.elements along when slicing."""
|
|
111
111
|
if isinstance(key, slice):
|
|
112
|
-
|
|
112
|
+
result = type(self)(xtra_params=(self.eset_obj[key], self.elements))
|
|
113
|
+
result.sliced = True
|
|
114
|
+
return result
|
|
113
115
|
return super().__getitem__(key)
|
|
114
116
|
|
|
115
117
|
|
|
@@ -289,9 +291,11 @@ class Permutator(PermutatorABCMixin):
|
|
|
289
291
|
"""Delegating to eset_obj like EMixinABC, but also carrying
|
|
290
292
|
self.elements/self.classes along when slicing."""
|
|
291
293
|
if isinstance(key, slice):
|
|
292
|
-
|
|
294
|
+
result = type(self)(
|
|
293
295
|
xtra_params=(self.eset_obj[key], self.elements, self.classes)
|
|
294
296
|
)
|
|
297
|
+
result.sliced = True
|
|
298
|
+
return result
|
|
295
299
|
return super().__getitem__(key)
|
|
296
300
|
|
|
297
301
|
|
|
@@ -422,7 +426,9 @@ class Distinct_Combinator(Distinct_CombinatorABCMixin):
|
|
|
422
426
|
|
|
423
427
|
def __getitem__(self, key):
|
|
424
428
|
if isinstance(key, slice):
|
|
425
|
-
|
|
429
|
+
result = type(self)(xtra_params=(self.eset_obj[key], self.elements, self.k))
|
|
430
|
+
result.sliced = True
|
|
431
|
+
return result
|
|
426
432
|
return super().__getitem__(key)
|
|
427
433
|
|
|
428
434
|
|
|
@@ -592,9 +598,11 @@ class Combinator(CombinatorABCMixin):
|
|
|
592
598
|
|
|
593
599
|
def __getitem__(self, key):
|
|
594
600
|
if isinstance(key, slice):
|
|
595
|
-
|
|
601
|
+
result = type(self)(
|
|
596
602
|
xtra_params=(self.eset_obj[key], self.elements, self.k, self.classes)
|
|
597
603
|
)
|
|
604
|
+
result.sliced = True
|
|
605
|
+
return result
|
|
598
606
|
return super().__getitem__(key)
|
|
599
607
|
|
|
600
608
|
|
|
@@ -768,7 +776,9 @@ class Distinct_Arranger(Distinct_ArrangerABCMixin):
|
|
|
768
776
|
|
|
769
777
|
def __getitem__(self, key):
|
|
770
778
|
if isinstance(key, slice):
|
|
771
|
-
|
|
779
|
+
result = type(self)(xtra_params=(self.eset_obj[key], self.elements, self.r))
|
|
780
|
+
result.sliced = True
|
|
781
|
+
return result
|
|
772
782
|
return super().__getitem__(key)
|
|
773
783
|
|
|
774
784
|
|
|
@@ -798,6 +808,29 @@ class Natural_Multiset_Arranger(Eset):
|
|
|
798
808
|
arrangement #0 agree with Natural_Arranger's ordering when every
|
|
799
809
|
capacity is 1, and with Natural_Multiset_Permutator's ordering
|
|
800
810
|
when r equals the full multiset size.
|
|
811
|
+
|
|
812
|
+
That position-by-position shape is also why ranking's recursion
|
|
813
|
+
depth is O(classes * r) in the worst case, not O(classes) --
|
|
814
|
+
each of the r positions can try up to `classes` candidates before
|
|
815
|
+
landing on the right one. A prototype class-by-class ranking
|
|
816
|
+
(decide how many of the smallest remaining class, and which of
|
|
817
|
+
the still-open positions they occupy, via plain get_combination/
|
|
818
|
+
get_combination_number over positions, then recurse into the next
|
|
819
|
+
class for the remaining positions) was benchmarked and verified
|
|
820
|
+
as a correct bijection: it cuts depth to O(classes) and wins big
|
|
821
|
+
on many-classes/tight-capacity shapes (~470x faster before the
|
|
822
|
+
memo fix below existed, still much shallower after), but costs
|
|
823
|
+
more wall-clock time than the position-by-position approach on
|
|
824
|
+
few-classes/large-r shapes (get_combination_number's own O(r)
|
|
825
|
+
candidate search per class-step isn't free), and it ranks
|
|
826
|
+
arrangements in a different order than this class does today, so
|
|
827
|
+
switching isn't behavior-preserving the way the memo fix is.
|
|
828
|
+
Shipping it for real would mean dispatching between the two by
|
|
829
|
+
shape -- the same treatment multiset_combination_count already
|
|
830
|
+
gives inclusion-exclusion -- plus its own benchmark file, not a
|
|
831
|
+
blanket replacement. Not attempted here; left as a known, deferred
|
|
832
|
+
option for whoever next needs ranking to survive a much larger
|
|
833
|
+
number of classes than it currently does.
|
|
801
834
|
"""
|
|
802
835
|
|
|
803
836
|
def __init__(self, *args, **kwargs):
|
|
@@ -929,9 +962,11 @@ class Arranger(ArrangerABCMixin):
|
|
|
929
962
|
|
|
930
963
|
def __getitem__(self, key):
|
|
931
964
|
if isinstance(key, slice):
|
|
932
|
-
|
|
965
|
+
result = type(self)(
|
|
933
966
|
xtra_params=(self.eset_obj[key], self.elements, self.r, self.classes)
|
|
934
967
|
)
|
|
968
|
+
result.sliced = True
|
|
969
|
+
return result
|
|
935
970
|
return super().__getitem__(key)
|
|
936
971
|
|
|
937
972
|
|
|
@@ -1047,7 +1082,9 @@ class Distinct_Powerset(Distinct_PowersetABCMixin):
|
|
|
1047
1082
|
|
|
1048
1083
|
def __getitem__(self, key):
|
|
1049
1084
|
if isinstance(key, slice):
|
|
1050
|
-
|
|
1085
|
+
result = type(self)(xtra_params=(self.eset_obj[key], self.elements))
|
|
1086
|
+
result.sliced = True
|
|
1087
|
+
return result
|
|
1051
1088
|
return super().__getitem__(key)
|
|
1052
1089
|
|
|
1053
1090
|
|
|
@@ -1188,9 +1225,11 @@ class Powerset(PowersetABCMixin):
|
|
|
1188
1225
|
|
|
1189
1226
|
def __getitem__(self, key):
|
|
1190
1227
|
if isinstance(key, slice):
|
|
1191
|
-
|
|
1228
|
+
result = type(self)(
|
|
1192
1229
|
xtra_params=(self.eset_obj[key], self.elements, self.classes)
|
|
1193
1230
|
)
|
|
1231
|
+
result.sliced = True
|
|
1232
|
+
return result
|
|
1194
1233
|
return super().__getitem__(key)
|
|
1195
1234
|
|
|
1196
1235
|
|
|
@@ -1349,7 +1388,9 @@ class Set_Partitioner(Set_PartitionerABCMixin):
|
|
|
1349
1388
|
|
|
1350
1389
|
def __getitem__(self, key):
|
|
1351
1390
|
if isinstance(key, slice):
|
|
1352
|
-
|
|
1391
|
+
result = type(self)(xtra_params=(self.eset_obj[key], self.elements))
|
|
1392
|
+
result.sliced = True
|
|
1393
|
+
return result
|
|
1353
1394
|
return super().__getitem__(key)
|
|
1354
1395
|
|
|
1355
1396
|
|
|
@@ -1508,7 +1549,9 @@ class Distinct_Derangement(Distinct_DerangementABCMixin):
|
|
|
1508
1549
|
|
|
1509
1550
|
def __getitem__(self, key):
|
|
1510
1551
|
if isinstance(key, slice):
|
|
1511
|
-
|
|
1552
|
+
result = type(self)(xtra_params=(self.eset_obj[key], self.elements))
|
|
1553
|
+
result.sliced = True
|
|
1554
|
+
return result
|
|
1512
1555
|
return super().__getitem__(key)
|
|
1513
1556
|
|
|
1514
1557
|
|
|
@@ -1633,5 +1676,7 @@ class Cartesian_Product(Cartesian_ProductABCMixin):
|
|
|
1633
1676
|
|
|
1634
1677
|
def __getitem__(self, key):
|
|
1635
1678
|
if isinstance(key, slice):
|
|
1636
|
-
|
|
1679
|
+
result = type(self)(xtra_params=(self.eset_obj[key], self.sources))
|
|
1680
|
+
result.sliced = True
|
|
1681
|
+
return result
|
|
1637
1682
|
return super().__getitem__(key)
|
|
@@ -153,8 +153,54 @@ def get_combination_number(combination: tuple[int], n: int) -> int | None:
|
|
|
153
153
|
return rank(list(range(n)), list(combination))
|
|
154
154
|
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
# Below this many remaining classes, inclusion-exclusion's 2**len(rem)
|
|
157
|
+
# subsets are cheap regardless of capacity size -- and above this many
|
|
158
|
+
# remaining classes, that same sum stops being cheap. Below this
|
|
159
|
+
# capacity, the branching recursion's own cost (tied to capacity, not
|
|
160
|
+
# class count) is already small enough that switching buys nothing.
|
|
161
|
+
# Both are load-bearing together, not independently: inclusion-exclusion
|
|
162
|
+
# has its own weak spot (a target near half of a subproblem's own total
|
|
163
|
+
# capacity prunes poorly, regardless of how large that capacity is), so
|
|
164
|
+
# this only dispatches when there are few *enough* classes left that even
|
|
165
|
+
# that weak spot stays cheap -- checked up to 15 classes at capacity 100,
|
|
166
|
+
# worst case (target at exactly half), before shipping this threshold.
|
|
167
|
+
_INCLUSION_EXCLUSION_CLASS_THRESHOLD = 15
|
|
168
|
+
_INCLUSION_EXCLUSION_CAPACITY_THRESHOLD = 15
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _inclusion_exclusion_combination_count(
|
|
172
|
+
rem: tuple[int, ...], remaining_k: int
|
|
173
|
+
) -> int:
|
|
174
|
+
m = len(rem)
|
|
175
|
+
total_answer = 0
|
|
176
|
+
|
|
177
|
+
def rec(i: int, excess: int, sign: int) -> None:
|
|
178
|
+
nonlocal total_answer
|
|
179
|
+
if excess > remaining_k:
|
|
180
|
+
return
|
|
181
|
+
if i == m:
|
|
182
|
+
total_answer += sign * comb(m - 1 + remaining_k - excess, m - 1)
|
|
183
|
+
return
|
|
184
|
+
rec(i + 1, excess, sign)
|
|
185
|
+
rec(i + 1, excess + rem[i] + 1, -sign)
|
|
186
|
+
|
|
187
|
+
rec(0, 0, 1)
|
|
188
|
+
return total_answer
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def multiset_combination_count(
|
|
192
|
+
multiplicities: tuple[int, ...],
|
|
193
|
+
k: int,
|
|
194
|
+
_memo: dict[tuple[tuple[int, ...], int], int] | None = None,
|
|
195
|
+
) -> int:
|
|
196
|
+
# get_multiset_combination/get_multiset_combination_number call this
|
|
197
|
+
# once per candidate tried at every class, and get_multiset_subset/
|
|
198
|
+
# get_multiset_subset_number call it once per size -- both pass their
|
|
199
|
+
# own long-lived _memo through every call instead of letting each one
|
|
200
|
+
# start over from empty, since overlapping subproblems (same
|
|
201
|
+
# sorted-suffix, same remaining_k) come up constantly across those
|
|
202
|
+
# calls.
|
|
203
|
+
memo: dict[tuple[tuple[int, ...], int], int] = {} if _memo is None else _memo
|
|
158
204
|
|
|
159
205
|
def count(rem: tuple[int, ...], remaining_k: int) -> int:
|
|
160
206
|
key = (rem, remaining_k)
|
|
@@ -164,24 +210,63 @@ def multiset_combination_count(multiplicities: tuple[int, ...], k: int) -> int:
|
|
|
164
210
|
result = 1
|
|
165
211
|
elif not rem:
|
|
166
212
|
result = 0
|
|
167
|
-
elif remaining_k <= min(rem):
|
|
168
|
-
# Capacities can't bind: plain stars-and-bars (mset's C1 shortcut).
|
|
169
|
-
result = comb(len(rem) + remaining_k - 1, remaining_k)
|
|
170
213
|
else:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
214
|
+
total = sum(rem)
|
|
215
|
+
if remaining_k > total:
|
|
216
|
+
# Even every remaining class at full capacity can't reach
|
|
217
|
+
# remaining_k: no need to branch to find that out.
|
|
218
|
+
result = 0
|
|
219
|
+
elif remaining_k <= min(rem):
|
|
220
|
+
# Capacities can't bind: plain stars-and-bars (mset's C1 shortcut).
|
|
221
|
+
result = comb(len(rem) + remaining_k - 1, remaining_k)
|
|
222
|
+
elif total - remaining_k <= min(rem):
|
|
223
|
+
# Gaps can't bind: x_i <-> c_i - x_i is a bijection between
|
|
224
|
+
# assignments summing to remaining_k and ones summing to
|
|
225
|
+
# total - remaining_k (unused capacity), so the same
|
|
226
|
+
# "can't bind" shortcut applies to that leftover instead.
|
|
227
|
+
# Only fires when the leftover slack could fit inside a
|
|
228
|
+
# single remaining class's own capacity -- when even the
|
|
229
|
+
# smallest remaining capacity is less than the slack, this
|
|
230
|
+
# doesn't trigger and the recursion still has to walk down
|
|
231
|
+
# to the original shortcut above, one class at a time.
|
|
232
|
+
gap = total - remaining_k
|
|
233
|
+
result = comb(len(rem) + gap - 1, gap)
|
|
234
|
+
elif (
|
|
235
|
+
len(rem) <= _INCLUSION_EXCLUSION_CLASS_THRESHOLD
|
|
236
|
+
and min(rem) >= _INCLUSION_EXCLUSION_CAPACITY_THRESHOLD
|
|
237
|
+
):
|
|
238
|
+
# Few classes, each with plenty of room: this is exactly
|
|
239
|
+
# where this recursion's own branching factor (tied to
|
|
240
|
+
# capacity size, not class count) stops being competitive
|
|
241
|
+
# with inclusion-exclusion's 2**len(rem), which doesn't
|
|
242
|
+
# care how large the capacities are at all.
|
|
243
|
+
result = _inclusion_exclusion_combination_count(rem, remaining_k)
|
|
244
|
+
else:
|
|
245
|
+
# t (how many go to this class) can't be so small that the
|
|
246
|
+
# rest of the classes, even maxed out, can't cover what's
|
|
247
|
+
# left -- that's a hopeless branch just as much as t being
|
|
248
|
+
# too large is, so bound the loop on both ends instead of
|
|
249
|
+
# generating and then discarding those calls.
|
|
250
|
+
tail_capacity = total - rem[0]
|
|
251
|
+
lo = max(0, remaining_k - tail_capacity)
|
|
252
|
+
hi = min(rem[0], remaining_k)
|
|
253
|
+
result = sum(count(rem[1:], remaining_k - t) for t in range(lo, hi + 1))
|
|
175
254
|
memo[key] = result
|
|
176
255
|
return result
|
|
177
256
|
|
|
178
|
-
|
|
257
|
+
# The count is invariant under reordering the classes (it depends only
|
|
258
|
+
# on the multiset of capacities, not their positions), but the
|
|
259
|
+
# recursion's cost isn't: ascending order lets the "capacities can't
|
|
260
|
+
# bind" shortcut above trigger as soon as only large-capacity classes
|
|
261
|
+
# are left, instead of branching on them first.
|
|
262
|
+
return count(tuple(sorted(multiplicities)), k)
|
|
179
263
|
|
|
180
264
|
|
|
181
265
|
def get_multiset_combination(
|
|
182
266
|
val: int, multiplicities: tuple[int, ...], k: int
|
|
183
267
|
) -> tuple[int] | None:
|
|
184
|
-
|
|
268
|
+
memo: dict[tuple[tuple[int, ...], int], int] = {}
|
|
269
|
+
total = multiset_combination_count(multiplicities, k, memo)
|
|
185
270
|
if val >= total or val < 0:
|
|
186
271
|
return None
|
|
187
272
|
|
|
@@ -211,7 +296,7 @@ def get_multiset_combination(
|
|
|
211
296
|
retlist: list[int],
|
|
212
297
|
t: int,
|
|
213
298
|
) -> list[int]:
|
|
214
|
-
block = multiset_combination_count(classes[1:], remaining_k - t)
|
|
299
|
+
block = multiset_combination_count(classes[1:], remaining_k - t, memo)
|
|
215
300
|
if resval < block:
|
|
216
301
|
return place(
|
|
217
302
|
resval,
|
|
@@ -237,6 +322,8 @@ def get_multiset_combination_number(
|
|
|
237
322
|
):
|
|
238
323
|
return None
|
|
239
324
|
|
|
325
|
+
memo: dict[tuple[tuple[int, ...], int], int] = {}
|
|
326
|
+
|
|
240
327
|
def rank(
|
|
241
328
|
classes: tuple[int, ...], remaining_k: int, class_id: int, seq: list[int]
|
|
242
329
|
) -> int:
|
|
@@ -257,7 +344,7 @@ def get_multiset_combination_number(
|
|
|
257
344
|
t: int,
|
|
258
345
|
actual: int,
|
|
259
346
|
) -> int:
|
|
260
|
-
block = multiset_combination_count(classes[1:], remaining_k - t)
|
|
347
|
+
block = multiset_combination_count(classes[1:], remaining_k - t, memo)
|
|
261
348
|
if t == actual:
|
|
262
349
|
return rank(classes[1:], remaining_k - t, class_id + 1, seq[t:])
|
|
263
350
|
return block + locate(classes, remaining_k, class_id, seq, t - 1, actual)
|
|
@@ -365,8 +452,18 @@ def get_arrangement_number(arrangement: tuple[int], n: int) -> int | None:
|
|
|
365
452
|
return get_number(list(arrangement), r)
|
|
366
453
|
|
|
367
454
|
|
|
368
|
-
def multiset_arrangement_count(
|
|
369
|
-
|
|
455
|
+
def multiset_arrangement_count(
|
|
456
|
+
multiplicities: tuple[int, ...],
|
|
457
|
+
r: int,
|
|
458
|
+
_memo: dict[tuple[tuple[int, ...], int], int] | None = None,
|
|
459
|
+
) -> int:
|
|
460
|
+
# get_multiset_arrangement/get_multiset_arrangement_number call this
|
|
461
|
+
# once per candidate tried at every position, so they pass their own
|
|
462
|
+
# long-lived _memo through every call instead of letting each call
|
|
463
|
+
# start over from an empty one -- the two share overlapping
|
|
464
|
+
# subproblems (same sorted-suffix, same remaining_r) constantly, and
|
|
465
|
+
# without a shared memo those get recomputed from scratch every time.
|
|
466
|
+
memo: dict[tuple[tuple[int, ...], int], int] = {} if _memo is None else _memo
|
|
370
467
|
|
|
371
468
|
def g(rem: tuple[int, ...], remaining_r: int) -> int:
|
|
372
469
|
key = (rem, remaining_r)
|
|
@@ -377,23 +474,40 @@ def multiset_arrangement_count(multiplicities: tuple[int, ...], r: int) -> int:
|
|
|
377
474
|
elif not rem:
|
|
378
475
|
result = 0
|
|
379
476
|
else:
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
477
|
+
total = sum(rem)
|
|
478
|
+
if remaining_r > total:
|
|
479
|
+
# Even every remaining class at full capacity can't reach
|
|
480
|
+
# remaining_r: no need to branch to find that out.
|
|
481
|
+
result = 0
|
|
482
|
+
elif remaining_r <= min(rem):
|
|
483
|
+
# Capacities can't bind: every one of the remaining_r ordered
|
|
484
|
+
# slots can freely pick any of the len(rem) remaining classes.
|
|
485
|
+
result = len(rem) ** remaining_r
|
|
486
|
+
else:
|
|
487
|
+
# x (how many go to this class) can't be so small that the
|
|
488
|
+
# rest of the classes, even maxed out, can't cover what's
|
|
489
|
+
# left -- bound the loop on both ends instead of generating
|
|
490
|
+
# and then discarding those calls.
|
|
491
|
+
tail_capacity = total - rem[0]
|
|
492
|
+
lo = max(0, remaining_r - tail_capacity)
|
|
493
|
+
hi = min(rem[0], remaining_r)
|
|
494
|
+
result = sum(
|
|
495
|
+
comb(remaining_r, x) * g(rem[1:], remaining_r - x)
|
|
496
|
+
for x in range(lo, hi + 1)
|
|
497
|
+
)
|
|
387
498
|
memo[key] = result
|
|
388
499
|
return result
|
|
389
500
|
|
|
390
|
-
|
|
501
|
+
# See multiset_combination_count: the count doesn't depend on class
|
|
502
|
+
# order, but ascending order lets the shortcut above trigger sooner.
|
|
503
|
+
return g(tuple(sorted(multiplicities)), r)
|
|
391
504
|
|
|
392
505
|
|
|
393
506
|
def get_multiset_arrangement(
|
|
394
507
|
val: int, multiplicities: tuple[int, ...], r: int
|
|
395
508
|
) -> tuple[int] | None:
|
|
396
|
-
|
|
509
|
+
memo: dict[tuple[tuple[int, ...], int], int] = {}
|
|
510
|
+
total = multiset_arrangement_count(multiplicities, r, memo)
|
|
397
511
|
if val >= total or val < 0:
|
|
398
512
|
return None
|
|
399
513
|
|
|
@@ -419,7 +533,7 @@ def get_multiset_arrangement(
|
|
|
419
533
|
new_mult = list(remaining_mult)
|
|
420
534
|
new_mult[label] -= 1
|
|
421
535
|
rest_mult = tuple(new_mult)
|
|
422
|
-
block = multiset_arrangement_count(rest_mult, remaining_r - 1)
|
|
536
|
+
block = multiset_arrangement_count(rest_mult, remaining_r - 1, memo)
|
|
423
537
|
if resval < block:
|
|
424
538
|
return place(resval, rest_mult, remaining_r - 1, retlist + [label])
|
|
425
539
|
return try_candidate(
|
|
@@ -440,6 +554,8 @@ def get_multiset_arrangement_number(
|
|
|
440
554
|
):
|
|
441
555
|
return None
|
|
442
556
|
|
|
557
|
+
memo: dict[tuple[tuple[int, ...], int], int] = {}
|
|
558
|
+
|
|
443
559
|
def rank(remaining_mult: tuple[int, ...], remaining_r: int, seq: list[int]) -> int:
|
|
444
560
|
if not seq:
|
|
445
561
|
return 0
|
|
@@ -456,7 +572,7 @@ def get_multiset_arrangement_number(
|
|
|
456
572
|
new_mult = list(remaining_mult)
|
|
457
573
|
new_mult[label] -= 1
|
|
458
574
|
rest_mult = tuple(new_mult)
|
|
459
|
-
block = multiset_arrangement_count(rest_mult, remaining_r - 1)
|
|
575
|
+
block = multiset_arrangement_count(rest_mult, remaining_r - 1, memo)
|
|
460
576
|
if label == seq[0]:
|
|
461
577
|
return rank(rest_mult, remaining_r - 1, seq[1:])
|
|
462
578
|
return block + locate(remaining_mult, remaining_r, seq, candidates[1:])
|
|
@@ -503,8 +619,10 @@ def get_multiset_subset(val: int, multiplicities: tuple[int, ...]) -> tuple[int]
|
|
|
503
619
|
if val >= total or val < 0:
|
|
504
620
|
return None
|
|
505
621
|
|
|
622
|
+
memo: dict[tuple[tuple[int, ...], int], int] = {}
|
|
623
|
+
|
|
506
624
|
def try_size(resval: int, k: int) -> tuple[int]:
|
|
507
|
-
block = multiset_combination_count(multiplicities, k)
|
|
625
|
+
block = multiset_combination_count(multiplicities, k, memo)
|
|
508
626
|
if resval < block:
|
|
509
627
|
return get_multiset_combination(resval, multiplicities, k)
|
|
510
628
|
return try_size(resval - block, k + 1)
|
|
@@ -519,10 +637,14 @@ def get_multiset_subset_number(
|
|
|
519
637
|
if combo_rank is None:
|
|
520
638
|
return None
|
|
521
639
|
|
|
640
|
+
memo: dict[tuple[tuple[int, ...], int], int] = {}
|
|
641
|
+
|
|
522
642
|
def offset(size: int) -> int:
|
|
523
643
|
if size == 0:
|
|
524
644
|
return 0
|
|
525
|
-
return multiset_combination_count(multiplicities, size - 1) + offset(
|
|
645
|
+
return multiset_combination_count(multiplicities, size - 1, memo) + offset(
|
|
646
|
+
size - 1
|
|
647
|
+
)
|
|
526
648
|
|
|
527
649
|
return offset(len(subset)) + combo_rank
|
|
528
650
|
|
|
@@ -470,7 +470,9 @@ class EABCMixinFactory(abc.ABC):
|
|
|
470
470
|
def __getitem__(self, key):
|
|
471
471
|
"""Delegating __getitem__ to the eset_obj"""
|
|
472
472
|
if isinstance(key, slice):
|
|
473
|
-
|
|
473
|
+
result = type(self)(xtra_params=(self.eset_obj[key],))
|
|
474
|
+
result.sliced = True
|
|
475
|
+
return result
|
|
474
476
|
elif isinstance(key, int):
|
|
475
477
|
mix_val = self.get_mix_val(self.eset_obj[key])
|
|
476
478
|
return mix_val
|
|
@@ -386,7 +386,9 @@ class Float64s(Eset):
|
|
|
386
386
|
|
|
387
387
|
"""
|
|
388
388
|
if isinstance(key, slice):
|
|
389
|
-
|
|
389
|
+
result = Float64s(xtra_params=(self.f64tpls[key],))
|
|
390
|
+
result.sliced = True
|
|
391
|
+
return result
|
|
390
392
|
elif isinstance(key, int):
|
|
391
393
|
return tpl2float(self.f64tpls[key])
|
|
392
394
|
raise ValueError('Need a slice or an integer')
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "esets-lib"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Lazy, index-addressable enumerated sets, including a full combinatorics family (permutations, combinations, arrangements, subsets, partitions, derangements, and Cartesian products)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|