改进使用笛卡尔积的算法

时间:2017-08-25 13:24:01

标签: python algorithm optimization cartesian-product

问题

假设您有n个整数列表,其中每个列表仅包含1n范围内的整数。例如,对于n = 4,我们可能会:

a_1 = [1, 2]
a_2 = [3]
a_3 = [4, 1, 1]
a_4 = [2, 3]

现在我的问题是:我可以勾选那些1列表中nn之间的所有整数,但是我找到了一个号码,我不能再使用该列表来查找后续号码了吗?

例如,在上面的n = 4示例中,我可以从a_1中选择1,从a_4中选择2,从a_2中选择3,在{{1}中选择4因此,我填写了从1到4的所有数字,但仅使用每个列表一次

我无法找到范围(因此应该返回a_3)的示例是:

False

原因是因为如果我从a_1中选择1,我将无法从任何列表中选择2。

方法

这是我目前直截了当的方法。我制作列表的笛卡尔积,并检查是否有任何,排序,将是一个范围。

a_1 = [1, 2]
a_2 = [3, 3, 5]
a_3 = [4]
a_4 = [5]
a_5 = [3, 4, 5]

问题

虽然我的方法有效,但对于大型列表来说,它变得有点低效。有关如何改进此算法的任何想法?

谢谢!

5 个答案:

答案 0 :(得分:2)

不是按顺序测试所有组合,而是先测试最受约束的列表,然后在向解决方案集中添加元素时更新其他列表中的替代项,从而加快速度。通过这种方式,您可以解决"你的例子都没有回溯一次。

def search(n, lists):
    if n == 0:
        yield []
    else:
        lists = [l for l in lists if l != []]
        if len(lists) >= n:
            least = min(lists, key=len)
            for val in least:
                new = [[x for x in lst if x != val] for lst in lists if lst is not least]
                for res in search(n-1, new):
                    yield [val] + res

这里有两个示例的调试/跟踪输出,以帮助理解。第一个值是n,然后是lists,最后是之前选择的val

4 [[1, 2], [3], [4, 1, 1], [2, 3]] None
3 [[1, 2], [4, 1, 1], [2]] 3
2 [[1], [4, 1, 1]] 2
1 [[4]] 1
0 [] 4 --> solution
[3, 2, 1, 4]

5 [[1, 2], [3, 3, 5], [4], [5], [3, 4, 5]] None
4 [[1, 2], [3, 3, 5], [5], [3, 5]] 4
3 [[1, 2], [3, 3], [3]] 5
2 [[1, 2], []] 3 --> abort

如果你还想要从列表中获取元素的索引,那么代码会变得更复杂,但并不多:

def search(n, lists):
    if n == 0:
        yield []
    else:
        if sum(1 for l in lists if l) >= n:
            i = min(range(len(lists)), key=lambda x: (lists[x] == [], len(lists[x])))
            for val in lists[i]:
                new = [[x for x in lst if x != val] if lst is not lists[i] else [] for lst in lists]
                for res in search(n-1, new):
                    yield [(i, val)] + res

您的第一个示例的结果是[(1, 3), (3, 2), (0, 1), (2, 4)]

答案 1 :(得分:2)

您可以将此公式表示为二分图中的最大流问题,其中左节点对应于列表,右节点对应于整数1到n。

如果整数在相应的列表中,则图中有一条边。

图表中的所有容量均等于1.

如果你能从左侧到右侧找到大小为n的流量,那么这个问题就可以解决了。

Python代码执行此操作:

import networkx as nx

a_1 = [1, 2]
a_2 = [2]
a_3 = [4, 1, 1]
a_4 = [2, 3]
A = [a_1,a_2,a_3,a_4]
n = 4

G=nx.DiGraph()
for i,a in enumerate(A):
    for j in set(a):
        l = 'list'+str(i)
        G.add_edge(l,j,capacity=1)
        G.add_edge('start',l,capacity=1)
for j in range(1,n+1):
    G.add_edge(j,'dest',capacity=1)
v,flow = nx.maximum_flow(G,'start','dest')
if v<n:
    print 'Impossible'
else:
    for i,a in enumerate(A):
        for j in set(a):
            if flow['list'+str(i)][j]>0:
                print 'Use',j,'from list',a

打印:

Use 1 from list [1, 2]
Use 2 from list [2]
Use 4 from list [4, 1, 1]
Use 3 from list [2, 3]

答案 2 :(得分:1)

笛卡儿产品对我来说似乎最直接。我会采取以下措施来简化您的代码:

  • 正如我在评论中提到的那样从您的any表达式中删除[]

  • 在计算笛卡尔积之前将所有输入列表折叠为集合 - 处理来自同一列表的重复值没有意义

  • range(1, len(lists)+1)保存到局部变量并与之进行比较,而不是每次都重新创建范围(这是一种常见的优化技术,称为“不变提升”,其中计算表达式不会改变在循环期间被“提升”出循环并且只计算一次)

但最终,计算输入列表中 a 笛卡儿的基本算法,然后查找任何值为1-n的值仍然是您最初编写的。

def fillRange(lists):
  cartesian = itertools.product(*(set(x) for x in lists))
  target = list(range(1, len(lists) + 1))
  return any(sorted(comb) == target for comb in cartesian)

答案 3 :(得分:1)

这可以被视为matching in a bipartite graph的问题。事实证明,Hall's marriage theorem告诉你答案(即匹配是否存在,而不是匹配本身)。这是一个可能的实现(为方便起见使用NumPy):

from itertools import chain, combinations
import numpy as np

# Recipe from itertools docs: https://docs.python.org/3/library/itertools.html#itertools-recipes
def powerset(iterable):
    s = list(iterable)
    return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))


def has_matching(lists):
    n = len(lists)
    m = np.array([np.logical_or.reduce(np.arange(1, n + 1)[:, np.newaxis] == lst, axis=1)
                  for lst in lists])
    m = m.astype(int)
    for s in powerset(range(n)):
        if np.sum(np.logical_or.reduce(m[s, :], axis=0)) < len(s):
            return False
    return True


lists1 = [[1, 2],
          [3],
          [4, 1, 1],
          [2, 3]]
print(has_matching(lists1))
>>> True

lists2 = [[1, 2],
          [3, 3, 5],
          [4],
          [5],
          [3, 4, 5]]
print(has_matching(lists2))
>>> False

但是,这需要您浏览{1, ..., n}的每个子集,所以我猜算法是O(2 N )。不是很好,但可能比通过整个笛卡尔积更好,我想这将是O( N N )。

答案 4 :(得分:0)

您可以尝试映射哪些值出现在哪个列表中,并从那里分解您的问题。此代码构建了这种反向查找:

In[38]: from collections import defaultdict
In[39]: occurrences = defaultdict(set)
In[40]: for i,ll in enumerate(lists):
   ...:     for x in ll:
   ...:         occurrences[x].add(i)
   ...:         
In[41]: print(occurrences)
defaultdict(<class 'set'>, {1: {0, 2}, 2: {0, 3}, 3: {1, 3}, 4: {2}})
In[42]: print(dict(occurrences.items()))
{1: {0, 2}, 2: {0, 3}, 3: {1, 3}, 4: {2}}

例如,您可以一目了然地看到,4仅存在于list[2](原始问题中为a_3)。从那里,如果从其他值中消除2,则1仅存在于list[0]中。消除0表示2仅存在于list[3]中,而3则只能从list[1]获得。如果在进行这种连续消除时,任何要选择的集合都变为空,那么就没有解决方案。