我正在编写一个代码,该代码基于两个都按升序排列的数字列表(a和b)以渐进方式生成子列表的列表。每个包含两个元素的子列表都可以视为这两个列表中元素的组合。第二个元素(来自列表b)必须大于第一个元素(来自列表a)。特别是,对于第二个元素,该值可能并不总是数字。子列表可以是[elem,None],这意味着列表b中没有匹配列表a中的“ elem”。最终输出中不应有任何重复项。如果您将输出想象成是在表中,则每个子列表将是一行,并且在两列中的每列中,元素都将以升序排列,第二列中为“无”。
由于我最后一个问题的友好回答,我很受启发,并编写了可以实现目标的代码。 (How to generate combinations with none values in a progressive manner)代码显示在这里。
import itertools as it
import time
start=time.time()
a=[1,5,6,7,8,10,11,13,15,16,20,24,25,27]
b=[2,8,9,10,11,12,13,14,17,18,21,26]
def create_combos(lst1, lst2): #a is the base list; l is the adjacent detector list
n = len(lst1)
x_ref = [None,None]
for i in range(1,n+1):
choices_index = it.combinations(range(n),i)
choices_value = list(it.combinations(lst2,i))
for choice in choices_index:
for values in choices_value:
x = [[elem,None] for elem in lst1]
for index,value in zip(choice,values): #Iterate over two lists in parallel
if value <= x[index][0]:
x[index][0] = None
break
else:
x[index][1] = value #over-write in appropriate location
if x_ref not in x:
yield x
count=0
combos=create_combos(a,b)
for combo in combos:
# print(combo)
count+=1
print('The number of combos is ',count)
end=time.time()
print('Run time is ',end-start)
这段代码是关于我在有限的python知识方面我所能获得的最好的代码。但是,由于列表a和b中的元素数量超过15个,运行仍然花费了很长时间。我知道这可能是因为组合的急剧增加。但是,我想知道是否可以进行任何改进以提高其效率,也许就组合的生成方式而言。而且,我正在生成所有可能的组合,然后将不适当的组合删除,我认为这可能还是无效的。
期望的结果是在合理的时间内处理每个列表中的大约30个元素。
编辑:由于一旦每个列表中的元素数量变大,连击数量也会急剧增加。因此,我想保留生成器,以便一次只允许一个组合占用内存。
如果我对以上任何陈述不清楚,请随时提问。谢谢:)
答案 0 :(得分:2)
编辑2:
好的,如果您做的更聪明,则可以更快地执行此操作。我现在将使用NumPy和Numba来真正加快速度。如果您不想使用Numba,则只需注释使用的部件,它仍然可以工作,但速度较慢。如果您不希望使用NumPy,则可以将其替换为列表或嵌套列表,但又可能会有明显的性能差异。
所以让我们看看。要更改的两个关键事项是:
要进行预分配,我们需要首先计算总共有多少组合。该算法是相似的,但是只是计数,如果您有一个用于部分计数的缓存,它实际上是相当快的。 Numba在这里确实产生了很大的变化,但是我已经使用了。
import numba as nb
def count_combos(a, b):
cache = np.zeros([len(a), len(b)], dtype=np.int32)
total = count_combos_rec(a, b, 0, 0, cache)
return total
@nb.njit
def count_combos_rec(a, b, i, j, cache):
if i >= len(a) or j >= len(b):
return 1
if cache[i][j] > 0:
return cache[i][j]
while j < len(b) and a[i] >= b[j]:
j += 1
count = 0
for j2 in range(j, len(b)):
count += count_combos_rec(a, b, i + 1, j2 + 1, cache)
count += count_combos_rec(a, b, i + 1, j, cache)
cache[i][j] = count
return count
现在我们可以为所有组合预分配一个大数组。而不是直接将组合存储在其中,我将使用一个整数数组来表示b
中元素的位置(a
中的元素被该位置隐含,而None
匹配由-1
表示。
为了重用组合,我们执行以下操作。每次我们需要查找某对i
/ j
的组合时,如果以前没有计算过,就进行计算,然后将位置保存在输出数组中这些组合具有的位置首次存储。下次当我们遇到相同的i
/ j
对时,我们只需要复制之前制作的相应零件即可。
总而言之,该算法以如下方式结束(本例中的结果是一个NumPy对象数组,第一列是a
中的元素,第二列是b
中的元素,但是您可以使用.tolist()
将其转换为常规的Python列表。
import numpy as np
import numba as nb
def generate_combos(a, b):
a = np.asarray(a)
b = np.asarray(b)
# Count combos
total = count_combos(a, b)
count_table = np.zeros([len(a), len(b)], np.int32)
# Table telling first position of a i/j match
ref_table = -np.ones([len(a), len(b)], dtype=np.int32)
# Preallocate result
result_idx = np.empty([total, len(a)], dtype=np.int32)
# Make combos
generate_combos_rec(a, b, 0, 0, result_idx, 0, count_table, ref_table)
# Produce matchings array
seconds = np.where(result_idx >= 0, b[result_idx], None)
firsts = np.tile(a[np.newaxis], [len(seconds), 1])
return np.stack([firsts, seconds], axis=-1)
@nb.njit
def generate_combos_rec(a, b, i, j, result, idx, count_table, ref_table):
if i >= len(a):
return idx + 1
if j >= len(b):
result[idx, i:] = -1
return idx + 1
elif ref_table[i, j] >= 0:
r = ref_table[i, j]
c = count_table[i, j]
result[idx:idx + c, i:] = result[r:r + c, i:]
return idx + c
else:
idx_ref = idx
j_ref = j
while j < len(b) and a[i] >= b[j]:
j += 1
for j2 in range(j, len(b)):
idx_next = generate_combos_rec(a, b, i + 1, j2 + 1, result, idx, count_table, ref_table)
result[idx:idx_next, i] = j2
idx = idx_next
idx_next = generate_combos_rec(a, b, i + 1, j, result, idx, count_table, ref_table)
result[idx:idx_next, i] = -1
idx = idx_next
ref_table[i, j_ref] = idx_ref
count_table[i, j_ref] = idx - idx_ref
return idx
让我们检查结果是否正确:
a = [1, 5, 6, 7, 8, 10, 11, 13, 15, 16, 20, 24, 25, 27]
b = [2, 8, 9, 10, 11, 12, 13, 14, 17, 18, 21, 26]
# generate_combos_prev is the previous recursive method
combos1 = list(generate_combos_prev(a, b))
# Note we do not need list(...) here because it is not a generator
combos = generate_combos(a, b)
print((combos1 == combos).all())
# True
好的,现在让我们来看看性能。
%timeit list(generate_combos_prev(a, b))
# 3.7 s ± 17.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit generate_combos(a, b)
# 593 ms ± 2.66 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
好!那快6倍!除了附加的依赖项之外,唯一可能的缺点是,我们一次制作所有组合,而不是迭代地制作(因此您将在内存中一次拥有所有组合),并且我们需要一个表格来存储大小为O({ {1}}。
这是做您正在做的事的更快方法:
len(a) * len(b)
此算法的唯一区别在于,它生成的组合比您的组合多(在您的代码中,最终计数为1262169),即def generate_combos(a, b):
# Assumes a and b are already sorted
yield from generate_combos_rec(a, b, 0, 0, [])
def generate_combos_rec(a, b, i, j, current):
# i and j are the current indices for a and b respectively
# current is the current combo
if i >= len(a):
# Here a copy of current combo is yielded
# If you are going to use only one combo at a time you may skip the copy
yield list(current)
else:
# Advance j until we get to a value bigger than a[i]
while j < len(b) and a[i] >= b[j]:
j += 1
# Match a[i] with every possible value from b
for j2 in range(j, len(b)):
current.append((a[i], b[j2]))
yield from generate_combos_rec(a, b, i + 1, j2 + 1, current)
current.pop()
# Match a[i] with None
current.append((a[i], None))
yield from generate_combos_rec(a, b, i + 1, j, current)
current.pop()
a = [1, 5, 6, 7, 8, 10, 11, 13, 15, 16, 20, 24, 25, 27]
b = [2, 8, 9, 10, 11, 12, 13, 14, 17, 18, 21, 26]
count = 0
combos = generate_combos(a, b)
for combo in combos:
count += 1
print('The number of combos is', count)
# 1262170
中的每个元素都与a
匹配的组合。这始终是最后一个生成的组合,因此,您可以根据需要忽略该组合。
编辑:如果愿意,可以将None
中的# Match a[i] with None
块移到generate_combos_rec
循环和while
循环之间,然后与for
中与a
匹配的每个值将是第一个而不是最后一个。这样可以使其更容易跳过。或者,您可以将None
替换为:
yield list(current)
为避免生成额外的组合(以对每个生成的组合进行额外检查为代价)。
编辑2:
这里是经过稍微修改的版本,通过在递归中仅携带一个布尔值指示符来避免多余的组合。
if any(m is not None for _, m in current):
yield list(current)