Ответ 1
Я думаю, вы можете сделать это с помощью двухэтапного процесса. Первым шагом является то, что Михай Марусеак описал в своем (теперь удаленном) ответе, чтобы найти размер набора, итерации по возможным размерам, пока не найдете подходящий. Здесь код для этого:
def find_size(n, i):
"""Return a tuple, (k, i), where s is the size of the i-1'th set in the
cardinally-ordered powerset of {0..n-1}, and i is the remaining index
within the combinations of that size."""
if not 0 <= i < 2**n:
raise ValueError('index is too large or small')
for k in range(n+1):
c = comb(n, k)
if c > i:
return k, i
else:
i -= c
Как только вы определили размер, вы можете использовать комбинаторную систему чисел, чтобы найти правильную k-комбинацию из лексикографического заказа:
def pick_set(n, i):
"""Return the i-1'th set in the cardinally-ordered powerset of {0..n-1}"""
s, i = find_size(n, i)
result = []
for k in range(s, 0, -1):
prev_c = 0
for v in range(k, n+1):
c = comb(v, k)
if i < c:
result.append(v-1)
i -= prev_c
break
prev_c = c
return tuple(result)
Обе эти функции требуют функции для вычисления числа k-комбинаций для набора размеров n, n C k (который я назвал comb
). В этом другом вопросе есть несколько предлагаемых решений для поиска этого значения, включая scipy.misc.comb
, gmpy.comb
и несколько реализаций pure-python. Или, поскольку он неоднократно вызывался с последовательно возрастающими значениями (например, comb(n, 0)
, comb(n, 1)
и т.д. Или comb(k, k)
, comb(k+1, k)
и т.д.), Вы могли вместо этого использовать встроенный расчет, который использует ранее рассчитанное значение, чтобы дать более высокая производительность.
Пример использования (с использованием функции comb
, минимально адаптированной из J.F. Sebastian answer в связанном выше вопросе):
>>> for i in range(2**4):
print(i, pick_set(4, i))
0 ()
1 (0,)
2 (1,)
3 (2,)
4 (3,)
5 (1, 0)
6 (2, 0)
7 (2, 1)
8 (3, 0)
9 (3, 1)
10 (3, 2)
11 (2, 1, 0)
12 (3, 1, 0)
13 (3, 2, 0)
14 (3, 2, 1)
15 (3, 2, 1, 0)
Обратите внимание, что если вы планируете итерацию по комбинациям (как это было в примере), вы можете сделать это более эффективно, чем при запуске полного алгоритма, так как есть более эффективные алгоритмы для поиска следующей комбинации заданного размера (хотя вам понадобится немного дополнительной логики, чтобы подпрыгнуть до следующего большего размера комбинаций, когда вы исчерпали начальный размер).
Изменить: Вот некоторые из описанных выше оптимизаций:
Прежде всего, генераторы, которые эффективно вычисляют значения комбинации для диапазонов значений n
или k
:
def comb_n_range(start_n, stop_n, k):
c = comb(start_n, k)
yield start_n, c
for n in range(start_n+1, stop_n):
c = c * n // (n - k)
yield n, c
def comb_k_range(n, start_k, end_k):
c = comb(n, start_k)
yield start_k, c
for k in range(start_k+1, end_k):
c = c * (n - k + 1) // k
yield k, c
Биты for ... in range(...): c = comb(...); ...
в приведенном выше коде могут быть скорректированы для их использования, что должно быть немного быстрее.
Далее, функция, которая возвращает следующую комбинацию в лексикографическом порядке:
def next_combination(n, c):
if c[-1] == n-len(c)+1:
raise ValueError("no more combinations")
for i in range(len(c)-1, -1, -1):
if i == 0 or c[i] < c[i-1] - 1:
return c[:i] + (c[i] + 1,) + tuple(range(len(c)-2-i,-1,-1))
И генератор, который использует next_combination
для получения диапазона значений из набора параметров, определенного объектом slice
:
def powerset_slice(n, s):
start, stop, step = s.indices(2**n)
if step < 1:
raise ValueError("invalid step size (must be positive)")
if start == 0:
c = ()
else:
c = pick_set(n, start)
for _ in range(start, stop, step):
yield c
for _ in range(step):
try:
c = next_combination(n, c)
except ValueError:
if len(c) == n:
return
c = tuple(range(len(c), -1, -1))
Вы можете интегрировать это в класс, который используете, делая __getitem__
возвращать генератор, если ему передан объект slice
, а не int
. Это позволит вам быстрее сделать __iter__
, просто превратив его тело в: return self[:]
.