给定除数列表的有效反向分解

问题描述

给出一个数字n一个除数列表A,我如何才能有效地找到所有除数的组合,它们相乘后就得出该数字?

例如

n = 12 
A = [2,3,4]

输出

[[3,2,2],[2,3],[4,[3,4]]

到目前为止,这是我设法做的(我从关于stackoverflow的许多查找质因数问题之一重新适应的代码):

def products(n,A):
    if n == 1:
        yield []
    for each_divisor in A:
        if n % each_divisor == 0:
            for new_product in products(n // each_divisor,A):
                yield new_product + [each_divisor]

代码似乎可以正常运行,但是速度很慢,如果我尝试使用备忘录(将A作为元组传递给函数,以避免无法散列的类型错误),则该代码将无法提供正确的结果。

关于如何提高此代码效率的任何建议?

我尝试的记忆代码如下:

class Memoize:
    def __init__(self,fun):
        self.fun = fun
        self.memo = {}

    def __call__(self,*args):
        if args not in self.memo:
            self.memo[args] = self.fun(*args)
        return self.memo[args]

@Memoize
def products(n,A): [as above]

使用上面定义的参数n,A调用函数时:

>>> list(products(12,(2,4)))
[[3,2]]

没有备注,相同代码输出为:

[[3,4]]

请注意,其他记忆化功能(例如来自functools软件包@functools.lru_cache(maxsize=128)的记忆化功能)也会导致相同的问题。

解决方法

您可以将问题分解为一个递归部分以查找所有唯一组合,并为一部分查找每种排列的组合,而不是使用备忘录。那会大大减少您的搜索空间,并且只会置换实际可用的选项。

要实现此目的,应对A进行排序。

第1部分:

在可能的因式分解图上执行DFS。仅通过选择每个因子大于或等于其前身的顺序来截断冗余分支的搜索。例如:

                   12
                /  |  \
               /   |   \
              /    |    \
          2(x6)  3(x4)   4(x3)
         /  |      |  \
     2(x3) 3(x2)   3   4(x1)
    /  |
   2  3(x1)

粗体节点是成功进行因式分解的路径。 Struck 节点是导致冗余分支的节点,因为除以因子后剩余的n小于因子。括号中未显示剩余值的节点根本不会导致分解。没有比当前因素低的分支尝试:当我们尝试3时,永远不会再访问2,只有3和4,依此类推。

在代码中:

A.sort()

def products(n,A):
    def inner(n,A,L):
        for i in range(len(A)):
            factor = A[i]
            if n % factor: continue

            k = n // factor
            if k < factor:
                if k == 1:
                    yield L + [factor]
                elif n in A:
                    yield L + [n]
                break  # Following k guaranteed to be even smaller
                       # until k == 1,which elif shortcuts

            yield from inner(k,A[i:],L + [factor])

    yield from inner(n,[])

这非常快。在您的特定情况下,它仅检查4个节点,而不是大约30个。实际上,您可以证明它检查了可能的绝对最小节点数。您可能获得的唯一改进是通过使用迭代而不是递归,我怀疑这样做是否会有帮助。

第2部分:

现在,您只需生成结果的每个元素的排列。 Python提供了直接在标准库中执行此操作的工具:

from itertools import chain,permutations

chain.from_iterable(map(permutations,products(n,A)))

您可以将其输入products的最后一行,

yield from chain.from_iterable(map(permutations,inner(n,[])))

运行list(products(12,A))可以使我的计算机以这种方式(5.2µs vs 4.0µs)改善20-30%。以list(products(2 * 3 * 4 * 5 * 5 * 7 * 11,[2,3,4,5,6,7,8,9,10,11,12,14,22]))之类的更复杂的示例运行显示出了更大的进步:7ms vs 42ms。

第2b部分:

您可以使用与显示的here类似的方法(无耻塞子)过滤掉因重复因素而发生的重复排列。适应于我们总是处理有序整数的初始列表这一事实,可以这样写:

def perm_dedup(tup):
    maximum = (-1,) * len(tup)
    for perm in permutations(tup):
        if perm <= maximum: continue
        maximum = perm
        yield perm

现在,您可以在最后一行中使用以下内容:

yield from chain.from_iterable(map(perm_dedup,[])))

时序定律非常支持这种完整的方法:问题的时间分别为5.2µs与4.9µs和6.5ms与42ms(对于较长的示例)。实际上,如果有的话,避免重复排列似乎可以进一步减少时间安排。

TL; DR

一种更高效的实现,它仅使用标准库并且仅搜索唯一因式分解的唯一排列:

from itertools import chain,permutations

def perm_dedup(tup):
    maximum = (-1,) * len(tup)
    for perm in permutations(tup):
        if perm <= maximum: continue
        maximum = perm
        yield perm

def products(n,A):
    A = sorted(set(A))
    def inner(n,L + [factor])

    yield from chain.from_iterable(map(perm_dedup,[])))