【问题标题】:Multiplicative combination algorithm乘法组合算法
【发布时间】:2013-03-10 08:52:31
【问题描述】:

问题:

给定一个数字 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 问题,该问题涉及找到两个 3 位数字乘积的最大回文数。我的方法是从 999(最大的 3 位数字)用两个因素向下迭代,并找到每个组合的乘积,另外检查该数字是否是回文:

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())

请注意,示例中的第一个列表以与此代码完全相同的顺序迭代因子。我最初的假设是,由于我向下迭代,我发现的第一个回文将是最大的。显然情况并非如此,因为j 在递减i 之前一直迭代到100。

我正在寻找一种迭代方法,使产生的值按降序排列,因为这使我只需调用一次next(maxpal) 就可以得到答案,这样效率更高。

编辑:

为了不取消使用非 Python 语言的好答案的资格,我可以尝试使用任何语言,只要您解释它以便我(或其他任何人)能够充分理解它。

【问题讨论】:

  • 具体是哪个问题?
  • @johnthexiii Q4 我想。那是几天前的事,但我才起床到 11 点,所以是在那之前。
  • 如果这是第四季度,那么有一个更简单的方法来解决它。两个 k 位数字的乘积正好有 2×k 位。您只需要生成 2×k 位回文(并对它们进行一些过滤)。回文数很少,如果使用回文的区分特性,很容易确定。
  • @larsmans 这是我采取的第一种方法。我按降序生成了所有可能的回文,但随后意识到找到 n 位因子的成本非常高(首先找到素因子,然后找到给出 n 位因子的乘法组合)。魔鬼在(and do some filtering on them)
  • @Asad 现在翻译成 Python 为时已晚,所以 Haskell 实现可以接受吗?

标签: python algorithm iteration


【解决方案1】:

给定一个数字 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】:

您可以使用堆/优先级 Q。

从 (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()

【讨论】:

  • 由于 OP 想要两个元素的子集,最好在早期插入 (x-1,y) 和 (x-1,y-1) 以强制执行约束 x
  • @larsmans:我误读了您的评论(并删除了我之前的 cmets,如果这让您感到困惑 :-))。你可能是对的。
  • heapq 的根总是堆中的最小值。但是,最后一个值根本不能保证是最大值。您仍然需要对其进行排序以获得最大值。
  • @Drewk:把它想象成一个二维矩阵 M[n,n],其中 M[i,j] = i*j。这具有 Young 的 Tableau 属性:每一行和每一列都是排序的。现在你想按排序顺序遍历这个矩阵。一种方法是插入最大堆并按照我上面概述的方式进行提取/插入。我不明白你的反对意见。我正在回答主要问题,而不是背景项目欧拉问题。
  • 好吧,当我发表评论时你没有代码,所以这是一个实现评论。你的代码很棒。 +1
【解决方案3】:

你可以像这样生成集合:

>>> 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])

如果你真的想按照你提议的方式去做,你可以使用bisect

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

表明bisect 方法稍快,其次是生成器的最大值。 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   

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

【讨论】:

  • 但这实际上会生成所有值以找到最大值,因为它不会按降序生成它们。 (当然,对于这种大小的数字,这在现代硬件上不是问题。但它无法扩展。)
  • 这更接近,因为它在 4 次迭代中找到了所需的回文,但它仍然没有输出按乘积大小排序的列表。例如,上一次 sn-p 中生成器的前几个输出是:[888888, 861168, 886688, 906609, 824428, 819918, 828828, 855558, 840048, 853358]。它也不严格输出组合,因为某些产品将多次出现在列表中(由于第二个因素从 1000 倒数到 x 而不是从 x 到 100)。不过,+1。
  • 您使用堆的方式缺少需要迭代器的要点。请参阅我的答案中的示例代码。例如,您只需要查看大约 114,000 个数字即可获得第三大回文数,而在您的实现中,您查看大约一百万个,然后将回文数插入堆中。是否愿意将其与我的实现进行比较?
【解决方案4】:

你知道当 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)])

【讨论】:

    【解决方案5】:

    问题中的循环模式是这样的

    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 的倍数,因为任何数字形式为 abccba 的数字都等于 a*100001 + b*10010 + c*1100,并且 100001、10010 和 1100 中的所有三个都是 11 的倍数。其次,如果我们目前最好的结果具有 k 值,并且我们正在使用 i≤j 测试给定的 i 值,则无需测试任何 j &lt; k/i 或任何 j&lt;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)。

    【讨论】:

    • 这实际上是这里最快的一英里!
    • +1:但这是解决项目欧拉问题(在背景下提交:-)),而不是实际提出的问题(这本身就是一个有趣的问题)。跨度>
    猜你喜欢
    • 2022-11-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-31
    相关资源
    最近更新 更多