乘法组合算法

11

问题:

给定一个数字n,是否有一种有效的算法来获取集合{1...n}中的2个组合列表,并按照组合的乘积值排序?

我需要这个列表以确定满足某些条件的两个*位数数的最大乘积。如果列表未排序,则必须先确定满足条件的所有组合,然后迭代这些组合以找到乘积最大的组合,这是低效的。

例如,给定n = 3,可能的组合如下:

Combination:      Product:
   3, 3              9
   3, 2              6
   3, 1              3
   2, 2              4
   2, 1              2
   1, 1              1

按产品价值降序排列,结果如下:

Combination:      Product:
   3, 3              9
   2, 3              6
   2, 2              4
   1, 3              3
   1, 2              2
   1, 1              1

背景信息:

我刚刚解决了一个Project Euler问题,该问题要求找到由两个三位数乘积得出的最大回文数。我的方法是从999(最大的三位数)开始向下迭代两个因数,并找到每个组合的乘积,同时检查该数字是否是回文:

def maxpal():
    for i in reversed(range(100,1000)):

        # Since we only want unique combinations, we only
        # need to iterate up to i

        for j in reversed(range(100,i)):   
            if str(i*j) == str(i*j)[::-1]:
                yield i*j

print max(maxpal())

需要注意的是,示例中的第一个列表以与此代码完全相同的顺序迭代因子。我的最初假设是,由于我正在向下迭代,因此我找到的第一个回文字符串将是最大的回文字符串。显然不是这种情况,因为ji递减之前一直迭代到100。

我正在寻找一种迭代的方法,使得产生的值按降序排列,因为这使我可以通过一次调用next(maxpal)来简单地获取答案,这更加高效。

编辑:

为了不在不是Python的语言中淘汰一个好的答案,只要您解释清楚,我(或任何其他人)就可以充分理解它,我可以接受任何语言的尝试。


2
这是具体的哪个问题? - John
@larsmans - 两个k位数的乘积最多有2×k位数字,而不是恰好(例如:2×3 = 6; 20×25 = 500)。 - Ted Hopp
@larsmans 那你还有一些 2*k-1 位数的乘积。 - Daniel Fischer
1
@Asad 现在对我来说翻译成Python已经太晚了,那么Haskell实现是否可行? - Daniel Fischer
@jwpat7:drewk似乎误解了我的答案,他试图实现基于堆的解决方案的代码并不是我回答的问题。 - Knoothe
显示剩余9条评论
5个回答

8
您可以使用堆/优先队列。
从(n,n)开始,将其插入堆中。您的比较函数=比较产品。
每当提取(x,y)时,如果需要,插入(x-1,y)和(x,y-1)(如果想要,您可以维护一个哈希表以检查重复项)。
以下是一些快速(看起来不太好看)的代码,用于演示上述内容。请注意,这是一个懒惰迭代器,允许我们进行下一步,并在满足条件时停止。(注:使用larsman的建议(下面的评论)将使它更好,但思路类似)。
import heapq

def mult_comb(n):
    heap = []
    visited = {}
    visited[n*n] = True
    prod = n*n
    heapq.heappush(heap, (-prod, n, n))
    while prod > 1:
        (prod,x,y) = heapq.heappop(heap)
        yield -prod,x,y
        prod = -prod

        prod1 = (x-1)*y
        prod2 = x*(y-1)
        if not prod1 in visited:
            heapq.heappush(heap, (-prod1, x-1,y))
            visited[prod1] = True
        if not prod2 in visited:
            heapq.heappush(heap, (-prod2, x,y-1))
            visited[prod2] = True

def main():
    for tup in mult_comb(10):
        print tup

if __name__ == "__main__":
    main()

2
由于OP想要两个元素的子集,最好在早期插入(x-1,y)和(x-1,y-1)以强制执行约束条件x<=y。 - Fred Foo
@larsmans:我误读了你的评论(如果这让你感到困惑,我删除了以前的评论 :-))。你可能是对的。 - Knoothe
heapq 始终将其根节点设置为堆中最小的值。然而,最后一个值并不保证是最大的值。您仍需要对其进行排序才能获得最大值。 - dawg
@Drewk:把它看作一个二维矩阵M[n,n],其中M[i,j]=i*j。这具有杨表特性:每行和每列都是排序的。现在你想按排序顺序遍历这个矩阵。一种方法是插入到最大堆中,并按照我上面概述的方式进行提取/插入。我不明白你的反对意见。我正在回答主要问题,而不是背景项目欧拉问题。 - Knoothe
@Knoothe:我计时了一下(在我的答案底部),找到第一个回文数大约快4倍,但是找到整个列表要慢2倍。我学到了新东西。谢谢! - dawg
显示剩余3条评论

3

问题中的循环模式如下:

for i in reversed(range(100,1000)):
    for j in reversed(range(100,i)):   
        if str(i*j) is palindromic, yield i*j

所需解决方案是以降序方式传递与该循环测试相同的数字。以上代码生成404550个i,j对;其中1231对是回文的;2180对中的数字大于最终结果906609 = 913 * 993。

到目前为止建议的方法可能会生成所有或许多可能的对;生成少量可能的对的方法仍然比必要的测试更多的数字对。

相比之下,以下代码仅测试了572个对,其中3个是回文。它主要依赖两个观察结果:首先,任何六位回文数都是11的倍数,因为任何数字的形式都等于a*100001 + b*10010 + c*1100,而100001、10010和1100的三个数字都是11的倍数。其次,如果我们迄今为止找到的最佳结果具有值k,并且我们正在测试一个给定值i,其中i≤j,则没有必要测试任何j < k / i或任何j < i。

def pal():
    nTop = 1000;    best, jin, jpal = 0, 0, 0
    # Test pairs (i, j) with i <= j
    for i in range(nTop, nTop//10-1, -1):
        jDel = 11 if i%11 else 1
        jHi = (nTop//jDel)*jDel
        jLo = max(i, best//i) - 1;
        for j in range(jHi, jLo, -jDel):
            jin += 1
            if str(i*j)==str(i*j)[::-1] :
                jpal += 1
                best = max(best, i*j)
    return (best, jin, jpal)

通过上面的代码,pal()返回元组(906609,572,3)。

这实际上是迄今为止最快的! - dawg
+1:但这个是解决项目欧拉问题(归类为背景),并不是实际提出的问题(这个问题本身也很有趣)。 - Knoothe

1
您可以这样生成集合:
>>> n=3
>>> s={(min(x,y),max(x,y)) for x in range(1,n+1) for y in range(1,n+1)}
>>> s
set([(1, 2), (1, 3), (3, 3), (2, 3), (2, 2), (1, 1)])

并按照以下方式排序:

>>> sorted(s,key=lambda t: -t[0]*t[1])
[(3, 3), (2, 3), (2, 2), (1, 3), (1, 2), (1, 1)]

但你完全不需要这样做。只需使用嵌套推导式:

>>> [(x,y) for x in range(3,0,-1) for y in range(3,x-1,-1)]
[(3, 3), (2, 3), (2, 2), (1, 3), (1, 2), (1, 1)]

这导致了一个特定问题的一行解决方案:
print max(x*y for x in range(1000,100,-1) for y in range(1000,x-1,-1) 
          if str(x*y)==str(x*y)[::-1])

如果你真的想按照你提出的方式去做,你可以使用。
def PE4():
    import bisect

    def ispal(n):
        return str(n)==str(n)[::-1]

    r=[]
    for x in xrange(1000,100,-1):
        for y in xrange(1000,x-1,-1):
            if ispal(x*y): bisect.insort(r,(x*y,x,y))

    return r[-1]

列表r最终以递增顺序结束,因为这是bisect支持的唯一顺序。
您还可以使用heapq:
def PE4_4():
    import heapq

    def ispal(n): return str(n)==str(n)[::-1]

    r=[]
    for x in xrange(100,1001):
        for y in xrange(x,1001):
            if ispal(x*y): heapq.heappush(r,(-x*y,x,y))     

    return (-r[0][0],r[0][1],r[0][2])   

如果我计时这些:

import timeit

def PE4_1():
    def ispal(n): return str(n)==str(n)[::-1]
    return max((x*y,x,y) for x in xrange(1000,99,-1) for y in xrange(1000,x-1,-1) if ispal(x*y))

def PE4_2():
    import bisect
    def ispal(n): return str(n)==str(n)[::-1]
    r=[]
    for x in xrange(1000,99,-1):
        for y in xrange(1000,x-1,-1):
            if ispal(x*y): bisect.insort(r,(x*y,x,y))

    return r[-1]

def PE4_3():
    import bisect
    def ispal(n): return str(n)==str(n)[::-1]
    r=[]
    for x in xrange(100,1001):
        for y in xrange(x,1001):
            if ispal(x*y): bisect.insort(r,(x*y,x,y))

    return r[-1]

def PE4_4():
    import heapq
    def ispal(n): return str(n)==str(n)[::-1]
    r=[]
    for x in xrange(100,1001):
        for y in xrange(x,1001):
            if ispal(x*y): heapq.heappush(r,(-x*y,x,y))     

    return (-r[0][0],r[0][1],r[0][2])         

n=25
for f in (PE4_1,PE4_2,PE4_3,PE4_4):
    fn=f.__name__
    print fn+':'
    print '\t',f()
    res=str(timeit.timeit('{}()'.format(fn),setup="from __main__ import {}".format(fn), number=n))
    print '\t'+res+' seconds\n'

它打印:

PE4_1:
    (906609, 913, 993)
    10.9998581409 seconds

PE4_2:
    (906609, 913, 993)
    10.5356709957 seconds

PE4_3:
    (906609, 913, 993)
    10.9682159424 seconds

PE4_4:
    (906609, 913, 993)
    11.3141870499 seconds

显示出方法略微更快,其次是生成器的最大值。heapq方法是最慢的(略微)。
长话短说,但可能产生你想要的列表顺序的最佳方法是按照这种方式进行排序:

我测试了Knooth的解决方案,它在寻找满足约束条件的第一个数字方面表现出巨大的优势:

def PE4_6():
    def ispal(n): return str(n)==str(n)[::-1]
    def gen(n=1000):
        heap=[]
        visited=set([n*n])
        prod=n*n
        heapq.heappush(heap,(-prod,n,n))
        while abs(prod)>1:
            (prod,x,y)=heapq.heappop(heap)
            yield -prod,x,y
            p1,p2=(x-1)*y, x*(y-1)
            if p1 not in visited:
                heapq.heappush(heap, (-p1, x-1,y))
                visited.add(p1)
            if p2 not in visited:
                heapq.heappush(heap, (-p2, x,y-1))
                visited.add(p2)

    it=iter(gen())
    t=next(it)
    while not ispal(t[0]):
        t=next(it)

    return t   

但是查找整个列表的速度较慢。


1
但是,这实际上是生成所有值以便找到最大值,因为它不会按照降序生成它们。(当然,对于这么小的数字,在现代硬件上并不是问题。但它不可扩展。) - rici
这个代码更接近目标,因为它只需要4次迭代就能找到所需的回文数,但它仍然没有按照乘积大小排序输出列表。例如,在您上一个片段中生成器的前几个输出是:[888888, 861168, 886688, 906609, 824428, 819918, 828828, 855558, 840048, 853358]。此外,它也不严格地输出组合,因为某些乘积将在列表中多次出现(这是由于第二个因子从1000递减到x而不是从x到100)。尽管如此,还是要点赞。 - Asad Saeeduddin
你使用堆的方式忽略了需要迭代器的要点。请参考我的答案中的示例代码。例如,为了找到第三大的回文数,你只需要查看约114,000个数字,而在你的实现中,你需要查看大约一百万个数字,然后将回文数插入堆中。愿意和我的实现进行比较吗? - Knoothe

0
给定一个数n,是否存在一种有效算法来获取由集合{1...n}的2个组合组成的列表,并按照组合乘积的值排序?
不太确定您需要什么,但以下是用Python编写它的简单方法:
n = SOME_INTEGER
from itertools import combinations
sorted(combinations(set(xrange(1,n+1)),2),key=lambda x: x[0]*x[1])

或者,以最大乘积为首选:

sorted(combinations(set(xrange(1,n+1)),2),key=lambda x: x[0]*x[1],reverse=True)

2
这里的问题在于你首先生成了一个未排序的组合列表,然后对它们进行排序。思路是生成一个已排序的列表。 - Asad Saeeduddin
啊,现在我明白你想要什么了……至少它紧凑,虽然不一定高效 ;) - isedev

0
你知道当b > c时,(a, b)总是在(a, c)之前出现。因此,你只需要保留每个类别的一个代表[(a, b), (a, b-1), (a, b-2), ...],然后在其中进行选择。使用一个堆来实现这个过程。这个实现的时间复杂度是O(n^2*log(n)),空间复杂度是O(n)。
import heapq

def combinations_prod_desc(n):
    h = [(-i*i, i, i) for i in xrange(1, n+1)]
    h.reverse()

    while len(h) > 0:
        u = h[0]
        yield u
        b = u[2]
        if b <= 1:
            heapq.heappop(h)
            continue
        a = u[1]
        b -= 1
        heapq.heappushpop(h, (-a*b, a, b))
    return

自从 Python 2.6 版本,heapq 模块内置了合并算法。利用它,我们可以得到同样算法的一行实现:

def combinations_prod_desc_compact(n):
    return heapq.merge(*[(lambda a : ((-a*b, a, b) for b in xrange(a, 0, -1)))(a) for a in xrange(1, n+1)])

上述代码的以下简单版本由于Python推导式语义的奇特性而无法正常工作。如果有人对探索Python语言规范感兴趣,那么查找以下代码为什么不能给出我们想要的结果,即使它看起来“应该”是这样的,将会很有趣:

def combinations_prod_desc_nonworking(n):
    return heapq.merge(*[((-a*b, a, b) for b in xrange(a, 0, -1)) for a in xrange(1, n+1)])

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