Python - 从大量组合中构建符合特定条件的子列表

3

我已经阅读了许久,这是我第一次找不到我正在研究的东西的答案。

我有一个包含93个字符串的列表,每个字符串都是6个字符长。从这93个字符串中,我想找到一组符合与其他字符串相对特定标准的20个字符串。虽然itertools.combinations可以给我所有可能的组合,但并非所有条件都值得检查。

例如,如果 [list[0],list[1]等] 失败,因为 list[0] 和 list[1] 不能在一起,无论其他18个字符串是什么,该组将每次失败,这是大量的浪费检查。

目前我使用了20个嵌套的for循环来处理它,但似乎必须有更好/更快的方法来完成它。

for n1 in bclist:
    building = [n1]
    n2bclist = [bc for bc in bclist if bc not in building]
    for n2 in n2bclist:              #this is the start of what gets repeated 19 times
        building.append(n2)
        if test_function(building): #does set fail? (counter intuitive, True when fail, False when pass)
            building.remove(n2)
            continue
        n3bclist = [bc for bc in bclist if bc not in building]
        #insert the additional 19 for loops, with n3 in n3, n4 in n4, etc
        building.remove(n2)

第20个循环中有打印语句,用于提示我是否存在一组20个偶数。这些for循环至少使我能够在单个加法失败时提前跳过集合,但是没有记住更大的组合何时失败。例如,[list[0], list[1]] 失败了,所以跳到 [list[0], [list[2]] 它通过了。接下来是 [list[0], list[2], list[1]],它将失败因为0和1再次在一起,所以它会移动到 [list[0], list[2], list[3]],可能会通过,也可能不会。我的担忧是,最终它也会测试以下组合:
  • [list[0], list[3], list[2]]
  • [list[2], list[0], list[3]]
  • [list[2], list[3], list[0]]
  • [list[3], list[0], list[2]]
  • [list[3], list[2], list[0]]
所有这些组合将具有与先前组合相同的结果。基本上,我要么使用itertools.combinations测试所有集合的组合,而我知道它们失败是因为早期值的失败,要么使用for循环的恶魔,当我不关心它们的顺序时,将值的顺序视为因素。这两种方法都会极大地增加代码完成所需的时间。有没有什么想法可以摆脱这些恶魔将不胜感激。

1
itertools.combinations 不会重复元素。itertools.permutations 执行的是你认为 combinations 应该执行的操作。combinations(range(4), 3) --> 012 013 023 123 - GP89
顺序重要吗?例如,[list[0],list[3],list[2]] 可以通过,但 [list[2],list[3],list[0]] 会失败吗? - Stuart
@Stuart 不,顺序无关紧要。你的两个例子都会失败或者都会通过。 - ded
@GP89 抱歉,我可能没有表达清楚。我的for循环目前模仿了itertools.permutations,这是一个问题,因为顺序并不重要,所以测试了许多不需要测试的东西。itertools.combinations不会重复元素,但是会测试012和013,即使01再次导致失败,也会测试不需要测试的内容。 - ded
4个回答

1

使用您目前的方法,但同时跟踪索引,以便在内部循环中可以跳过已经检查过的元素:

bcenum = list(enumerate(bclist))
for i1, n1 in bcenum:
    building = [n1]
    for i2, n2 in bcenum[i1+1:]:              #this is the start of what gets repeated 19 times
        building.append(n2)
        if test_function(building): #does set fail? (counter intuitive, True when fail, False when pass)
            building.remove(n2)
            continue
        for i3, n3 in bcenum[i2+1:]:
            # more nested loops
        building.remove(n2)

到目前为止,我很喜欢这个解决方案。据我所知,它提供了非冗余的测试(即不会同时测试[0,2,3,4]和[4,3,2,0]),并且不会浪费在失败集上的测试(即[0,1]=失败,因此不会测试[0,1,2])。仍在评估其他答案。 - ded
进行了一些额外的改进,但在合理的时间内完成了工作,并且对现有代码进行了最少的更改。 其他更改: - ded
如果(len(building) + len(bcenum[i#:]) < 20: break 在测试函数test之前添加。 而不是添加,测试和移除: 如果test_function(building + [n#]): 继续 - ded

1
def gen(l, n, test, prefix=()):
  if n == 0:
    yield prefix
  else:
    for i, el in enumerate(l):
      if not test(prefix + (el,)):
        for sub in gen(l[i+1:], n - 1, test, prefix + (el,)):
          yield sub

def test(l):
  return sum(l) % 3 == 0 # just a random example for testing

print list(gen(range(5), 3, test))

这将从l中选择基数为n的子集,使得test(subset) == False

它试图避免不必要的工作。然而,考虑到从93个元素中选择20个元素有1e20种方法,您可能需要重新思考您的整体方法。


0

您可以利用问题的两个方面:

  1. 顺序不重要
  2. 如果test_function(L)True,则L的任何子列表的test_function也将是True

您还可以通过处理0-92索引而不是list[0]-list[92]来简化事情 - 只有在test_function中我们可能关心列表的内容。

以下代码首先找到可行的对,然后是四个,八个和十六个的集合。最后,它找到了一个十六个和一个四个的所有可行组合,以得到您的20个列表。但是,超过100,000个八个集合,因此速度仍然太慢,我放弃了。也许您可以沿着同样的路线做些事情,但使用itertools加速,但可能不足够。

target = range(5, 25)
def test_function(L):
    for i in L:
        if not i in target:
            return True
def possible_combos(A, B):
    """
    Find all possible pairings of a list within A and a list within B
    """
    R = []
    for i in A:
        for j in B:
            if i[-1] < j[0] and not test_function(i + j):
                R.append(i + j)
    return R
def possible_doubles(A):
    """
    Find all possible pairings of two lists within A
    """
    R = []
    for n, i in enumerate(A):
        for j in A[n + 1:]:
            if i[-1] < j[0] and not test_function(i + j):
                R.append(i + j)
    return R
# First, find all pairs that are okay
L = range(92) 
pairs = []
for i in L:
    for j in L[i + 1:]:
        if not test_function([i, j]):
            pairs.append([i, j])

# Then, all pairs of pairs
quads = possible_doubles(pairs)
print "fours", len(quads), quads[0]
# Then all sets of eight, and sixteen
eights = possible_doubles(quads)
print "eights", len(eights), eights[0]
sixteens = possible_doubles(eights)
print "sixteens", len(sixteens), sixteens[0]

# Finally check all possible combinations of a sixteen plus a four
possible_solutions = possible_combos(sixteens, fours)
print len(possible_solutions), possible_solutions[0]

编辑:我找到了一个更好的解决方案。首先,识别在范围(0-92)内符合test_function的所有值对,并按顺序保留这些值对。假设第一个值对的第一个值必须是解决方案的第一个值,最后一个值对的第二个值必须是解决方案的最后一个值(但要检查...这个假设对于test_function是否成立?如果这不是一个安全的假设,那么您需要为起点和终点的所有可能值重复find_paths)。然后找到一条从第一个到最后一个值的路径,该路径长度为20,并且也符合test_function

def test_function(S):
    for i in S:
        if not i in target:
            return True
    return False

def find_paths(p, f):
    """ Find paths from end of p to f, check they are the right length,
        and check they conform to test_function
    """
    successful = []
    if p[-1] in pairs_dict:
        for n in pairs_dict[p[-1]]:
            p2 = p + [n]
            if (n == f and len(p2) == target_length and
                not test_function(p2)):
                successful.append(p2)
            else:
                successful += find_paths(p2, f)
    return successful

list_length = 93              # this is the number of possible elements
target = [i * 2 for i in range(5, 25)] 
    # ^ this is the unknown target list we're aiming for...
target_length = len(target)   # ... we only know its length
L = range(list_length - 1)
pairs = []
for i in L:
    for j in L[i + 1:]:
        if not test_function([i, j]):
            pairs.append([i, j])
firsts = [a for a, b in pairs]
nexts = [[b for a, b in pairs if a == f] for f in firsts]
pairs_dict = dict(zip(firsts, nexts))
print "Found solution(s):", find_paths([pairs[0][0]], pairs[-1][1])

0

你应该基于 itertools.combinations 来构建你的解决方案,因为它会处理好排序问题;短路过滤相对容易解决。

递归解决方案

让我们快速回顾一下如何实现 combinations;最简单的方法是采用嵌套 for 循环的方式,并将其转换为递归风格:

def combinations(iterable, r):
    pool = tuple(iterable)
    for i in range(0, len(pool)):
        for j in range(i + 1, len(pool)):
            ...
                yield (i, j, ...)

转换为递归形式:

def combinations(iterable, r):
    pool = tuple(iterable)
    def inner(start, k, acc):
        if k == r:
            yield acc
        else:
            for i in range(start, len(pool)):
                for t in inner(i + 1, k + 1, acc + (pool[i], )):
                    yield t
    return inner(0, 0, ())

现在应用过滤器非常容易:

def combinations_filterfalse(predicate, iterable, r):
    pool = tuple(iterable)
    def inner(start, k, acc):
        if predicate(acc):
            return
        elif k == r:
            yield acc
        else:
            for i in range(start, len(pool)):
                for t in inner(i + 1, k + 1, acc + (pool[i], )):
                    yield t
    return inner(0, 0, ())

让我们来检查一下:

>>> list(combinations_filterfalse(lambda t: sum(t) % 2 == 1, range(5), 2))
[(0, 2), (0, 4), (2, 4)]

迭代解决方案

itertools.combinations 的实际实现在文档中列出了一个迭代循环:

def combinations(iterable, r):
    pool = tuple(iterable)
    n = len(pool)
    if r > n:
        return
    indices = range(r)
    yield tuple(pool[i] for i in indices)
    while True:
        for i in reversed(range(r)):
            if indices[i] != i + n - r:
                break
        else:
            return
        indices[i] += 1
        for j in range(i+1, r):
            indices[j] = indices[j-1] + 1
        yield tuple(pool[i] for i in indices)

为了优雅地适应谓词,需要稍微重新排列循环:
def combinations_filterfalse(predicate, iterable, r):
    pool = tuple(iterable)
    n = len(pool)
    if r > n or predicate(()):
        return
    elif r == 0:
        yield ()
        return
    indices, i = range(r), 0
    while True:
        while indices[i] + r <= i + n:
            t = tuple(pool[k] for k in indices[:i+1])
            if predicate(t):
                indices[i] += 1
            elif len(t) == r:
                yield t
                indices[i] += 1
            else:
                indices[i+1] = indices[i] + 1
                i += 1
        if i == 0:
            return
        i -= 1
        indices[i] += 1

再次检查:

>>> list(combinations_filterfalse(lambda t: sum(t) % 2 == 1, range(5), 2))
[(0, 2), (0, 4), (2, 4)]
>>> list(combinations_filterfalse(lambda t: t == (1, 4), range(5), 2))
[(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (2, 3), (2, 4), (3, 4)]
>>> list(combinations_filterfalse(lambda t: t[-1] == 3, range(5), 2))
[(0, 1), (0, 2), (0, 4), (1, 2), (1, 4), (2, 4)]
>>> list(combinations_filterfalse(lambda t: False, range(5), 2))
[(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
>>> list(combinations_filterfalse(lambda t: False, range(5), 0))
[()]

比较

事实证明,递归解决方案不仅更简单,而且速度更快:

In [33]: timeit list(combinations_filterfalse_rec(lambda t: False, range(20), 5))
10 loops, best of 3: 24.6 ms per loop

In [34]: timeit list(combinations_filterfalse_it(lambda t: False, range(20), 5))
10 loops, best of 3: 76.6 ms per loop

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接