【问题标题】:Improve the efficiency of this search to check if any two numbers in this list sum to another?提高此搜索的效率以检查此列表中的任何两个数字是否相加?
【发布时间】:2019-12-19 03:22:47
【问题描述】:

我正在尝试找到最有效的方法来使用 Python 检查此列表中的任何两个数字是否与列表中的另一个数字相加。我决定添加一些上下文以使其更清晰,并且可能更容易优化。这是我的代码:

import numpy as np
from collections import Counter
from collections import deque


def gen_prim_pyth_trips(limit=None):
    u = np.mat(' 1  2  2; -2 -1 -2; 2 2 3')
    a = np.mat(' 1  2  2;  2  1  2; 2 2 3')
    d = np.mat('-1 -2 -2;  2  1  2; 2 2 3')
    uad = np.array([u, a, d])
    m = np.array([3, 4, 5])
    while m.size:
        m = m.reshape(-1, 3)
        if limit:
            m = m[m[:, 2] <= limit]
        yield from m
        m = np.dot(m, uad)

def find_target(values, target):

    dq = deque(sorted([(val, idx) for idx, val in enumerate(values)]))

    while True:
        if len(dq) < 2:
            return -1

        s =  dq[0][0] + dq[-1][0]

        if s > target:
            dq.pop()
        elif s < target:
            dq.popleft()
        else:
            break
    return dq[0], dq[-1]


ratioList = []

MAX_NUM = 500000

for i in list(gen_prim_pyth_trips(MAX_NUM)):
    ratioList.append((i[0]*i[1])/i[2]**2)
    if find_target(ratioList, (i[0]*i[1])/i[2]**2) != -1:
        print(find_target(ratioList, (i[0]*i[1])/i[2]**2))

gen_prim_pyth_trips() 函数来自here。 “慢”部分出现在三元组生成之后。 find_target 来自here

它目前工作正常,但我正在尝试找到一种方法来加快速度,或者找到一种更快的全新方法。

在 cmets 中,人们说这是 3SUM 问题的一个变体,根据维基百科页面可以在 O(n^2) 中完成,其中 n 是数字的数量(即我的比率数) .我还没有找到一种在 Python 中实现这一点的方法。

任何加速都会有所帮助;它不必只是一个更好的算法(库等)。我相信目前这比 O(n^3) 略好?

另外,对于 MAX_NUM = 100,000,它还不算太糟糕(大约 4 分钟),但对于 500,000,它非常糟糕(还没有停止运行)。

最终我想做 MAX_NUM = 1,000,000 或更多。

编辑

我希望看到像 O(n^2) 这样更快的算法,或者大幅提高速度。

【问题讨论】:

  • 这是3SUM 问题的一个变体,它已被广泛研究。但是,您确定要使用浮点数执行此操作吗?浮点舍入使简单的相等比较成为问题(而简单的容差比较只是以不同的方式出现问题)。
  • 鉴于您正在处理浮点数,这要困难得多;例如,0.1 + 0.7 == 0.8False。您可以使用Decimal 还是只使用整数?
  • @PatrickMaynard Python 中有一个Decimal 数据类型用于精确计算。
  • @PatrickMaynard,见floating-point-gui.de
  • @PatrickMaynard 你是在“搜索”?这意味着什么?如果您关心效率,您是否使用 NumPy?

标签: python algorithm performance time-complexity


【解决方案1】:

比您的速度快数百倍,而且没有浮点问题。
比 kaya3 的 O(n²) 解决方案快数千倍。
我运行它直到 MAX_NUM = 4,000,000 并没有发现任何结果。花了大约 12 分钟。

利用特殊数字。

只是一个普通的 3SUM。这些数字很特殊,我们可以利用它。它们的形式为 ab/c²,其中 (a,b,c) 是原始毕达哥拉斯三元组。

假设我们有一个数字 x=ab/c²,我们想找到另外两个这样的数字加起来为 x:

取消后,分母 c² 和 (fi)² 变为 c²/k 和 (fi)²/m(对于某些整数 k 和 m),我们有 c²/k = (fi) ²/米。令 p 为 c²/k 的最大素因子。然后 p 也除以 (fi)²/m,从而得到 f 或 i。所以至少有一个数字 de/f² 和 gh/i² 有一个可以被 p 整除的分母。我们称它为 y,而另一个称为 z。

那么对于某个 x,我们如何找到拟合的 y 和 z?我们不必为 y 和 z 尝试所有个数字。对于 y,我们只尝试那些分母可以被 p 整除的。对于z?我们将其计算为 x-y 并检查我们是否有该数字(在哈希集中)。

它有多大帮助?如果您天真地尝试所有(小于 x)数字,我的解决方案会计算有多少 y 候选人,以及我的方式有多少 y 候选人以及少多少:

  MAX_NUM         naive           mine      % less
--------------------------------------------------
   10,000         1,268,028        17,686   98.61
  100,000       126,699,321       725,147   99.43
  500,000     3,166,607,571     9,926,863   99.69
1,000,000    12,662,531,091    30,842,188   99.76
2,000,000    50,663,652,040    96,536,552   99.81
4,000,000   202,640,284,036   303,159,038   99.85

伪代码

以上代码形式的说明:

h = hashset(numbers)
for x in the numbers:
    p = the largest prime factor in the denominator of x
    for y in the numbers whose denominator is divisible by p:
      z = x - y
      if z is in h:
        output (x, y, z)

基准测试

各种 MAX_NUM 的时间(以秒为单位)及其结果 n:

         MAX_NUM:    10,000   100,000   500,000  1,000,000  2,000,000  4,000,000
            => n:     1,593    15,919    79,582    159,139    318,320    636,617
--------------------------------------------------------------------------------
Original solution       1.6     222.3         -          -          -          -
My solution             0.05      1.6      22.1       71.0      228.0      735.5
kaya3's solution       29.1    2927.1         -          -          -          -

复杂性

这是 O(n²),实际上可能更好。我不太了解数字的性质,无法对它们进行推理,但上述基准确实使它看起来比 O(n²) 好得多。对于二次运行时,从 n=318,320 到 n=636,617,您预计运行时间增加因子 (636,617/318,320)² ≈ 4.00,但实际增加仅为 735.5/228.0 ≈ 3.23。

我没有针对所有尺寸运行您的,但由于您的增长至少是二次方,因此在 MAX_NUM=4,000,000 时,您的解决方案至少需要 222.3 * (636,617/15,919)² = 355,520 秒,比我的慢 483 倍.同样,kaya3 的速度会比我的慢 6365 倍。

用这个奇怪的把戏浪费时间

Python 的 Fraction 类很简洁,但也很慢。特别是它的散列。转换为元组并散列该元组的速度大约快 34 倍:

>set SETUP="import fractions; f = fractions.Fraction(31459, 271828)"

>python -m timeit -s %SETUP% -n 100000 "hash(f)"
100000 loops, best of 5: 19.8 usec per loop

>python -m timeit -s %SETUP% -n 100000 "hash((f.numerator, f.denominator))"
100000 loops, best of 5: 581 nsec per loop

Its code 说:

[...] 这种方法很昂贵 [...] 为了确保 Fraction 的哈希值与数字相等的整数、浮点数或小数实例的哈希值一致,我们遵循数字哈希的规则文档中概述。

其他操作也有点慢,所以除了输出我不使用Fraction。我改用 (numerator, denominator) 元组。

解决方案代码

from math import gcd

def solve_stefan(triples):

    # Prime factorization stuff
    largest_prime_factor = [0] * (MAX_NUM + 1)
    for i in range(2, MAX_NUM+1):
        if not largest_prime_factor[i]:
            for m in range(i, MAX_NUM+1, i):
                largest_prime_factor[m] = i
    def prime_factors(k):
        while k > 1:
            p = largest_prime_factor[k]
            yield p
            while k % p == 0:
                k //= p

    # Lightweight fractions, represented as tuple (numerator, denominator)
    def frac(num, den):
        g = gcd(num, den)
        return num // g, den // g
    def sub(frac1, frac2):
        a, b = frac1
        c, d = frac2
        return frac(a*d - b*c, b*d)
    class Key:
        def __init__(self, triple):
            a, b, c = map(int, triple)
            self.frac = frac(a*b, c*c)
        def __lt__(self, other):
            a, b = self.frac
            c, d = other.frac
            return a*d < b*c

    # The search. See notes under the code.
    seen = set()
    supers = [[] for _ in range(MAX_NUM + 1)]
    for triple in sorted(triples, key=Key):
        a, b, c = map(int, triple)
        x = frac(a*b, c*c)
        denominator_primes = [p for p in prime_factors(c) if x[1] % p == 0]
        for y in supers[denominator_primes[0]]:
            z = sub(x, y)
            if z in seen:
                yield tuple(sorted(Fraction(*frac) for frac in (x, y, z)))
        seen.add(x)
        for p in denominator_primes:
            supers[p].append(x)

注意事项:

  • 我通过增加分数值的三元组,即增加 x 值。
  • 我的denominator_primes 是x 分母的质因数列表。请记住,这是 c²/k,因此它的质因数也必须是 c 的质因数。但是 k 可能已经取消了一些,所以我检查了 c 的质因数并检查它们是否除分母。为什么如此“复杂”而不是仅仅查找 c²/k 的素数?因为这可能非常大。
  • denominator_primes 是递减的,所以 p 就是 denominator_primes[0]。顺便说一句,为什么要使用最大的?因为更大意味着更稀有意味着更少的 y 候选意味着更快。
  • supers[p] 列出了其分母可以被 p 整除的数字。它用于获取 y 候选。
  • 用完 x 后,我使用 denominator_primes 将 x 放入 supers 列表中,因此它可以作为未来 x 值的 y。
  • 我在循环期间(而不是之前)构建seensupers 以保持它们的小。毕竟,对于 x=y+z 的正数,y 和 z 必须小于 x,因此寻找更大的将是浪费。

验证

如果没有结果,您如何验证结果?据我所知,我们的解决方案都没有找到。所以没有什么可以比较的,除了虚无,这并不完全令人信服。好吧,我的解决方案不依赖于毕达哥拉斯,所以我创建了一组原始三元组并检查了我的解决方案的结果。它计算了与参考实现相同的 25,336 个结果:

def solve_reference(triples):
    fractions = {Fraction(int(a) * int(b), int(c)**2)
                 for a, b, c in triples}
    for x, y in combinations_with_replacement(sorted(fractions), 2):
        z = x + y
        if z in fractions:
            yield x, y, z

MIN_NUM = 2
MAX_NUM = 25
def triples():
    return list((a, b, c)
                for a, b, c in combinations(range(MIN_NUM, MAX_NUM+1), 3)
                if gcd(a, gcd(b, c)) == 1)
print(len(triples()), 'input triples')
expect = set(solve_reference(triples()))
print(len(expect), 'results')
output = set(solve_stefan(triples()))
print('output is', ('wrong', 'correct')[output == expect])

输出:

1741 input triples
25336 results
output is correct

【讨论】:

  • @גלעדברקן 看看,谢谢。完全不明白,有什么特别值得我看的东西对我有帮助吗?
  • 我不知道那里有没有什么特别有用的东西。这里的练习似乎是为了支持或调查那里的猜想。对于我认为您可能已经使用的分母,似乎至少有一个必要条件,还有一个有趣的通用公式,即理性的非线性丢番图方程。
  • @גלעדברקן 在此处问题下的 cmets 中,Patrick 表示他将很快发布一个猜想并正在寻找证据。所以我想这将是对他原作的更新。好的,我现在看到了这些条件,它们确实与我所做的相似,但更笼统。我还寻找了更通用的东西,并且我对如何实现它有一个粗略的想法。我不确定 mathlove 的条件是否也适用于三元组,例如三元组 (4,3,2) 的 gcd(4,3,2)=1 但 (4⋅3)/2²=3 所以分母丢失了完全的主要因素。
  • @PatrickMaynard 我认为它应该适用于你不正方形的时候,是的。我刚试了一下,发现 3*4/5 + 20*21/29 = 17*144/145,是你的意思吗?对于 (4,3,2),我不是指毕达哥拉斯。我以为我见过毕达哥拉斯式的,但我把它和我的验证测试中的一个混淆了。很容易证明它不会发生在毕达哥拉斯学中,所以我的 k 永远是 1。将简化我的代码/文本。
【解决方案2】:

你提到朴素算法是 O(n³),但是如果你可以使用 hashtable,比如 Python 集,O(n²) 算法也很简单:

MAX_NUM = 500000

from fractions import Fraction
from itertools import combinations_with_replacement

def solve(numbers):
    for a, b in combinations_with_replacement(numbers, 2):
        c = a + b
        if c in numbers:
            yield (a, b, c)

ratio_set = {
    Fraction(int(p) * int(q), int(r) ** 2)
    for p, q, r in gen_prim_pyth_trips(MAX_NUM)
}

for a, b, c in solve(ratio_set):
    print(a, '+', b, '=', c)

这使用了Fraction 类,因此浮点运算不精确并没有什么有趣的事情,因此+== 在假设您的数字有界的情况下在恒定时间内完成。在这种情况下,运行时间为 O(n²),因为:

  • 插入哈希表需要 O(1) 时间,因此构建集合需要 O(n) 时间。
  • for a, b in ... 循环迭代 O(n²) 对,每个集合成员资格测试为 O(1)。

集合的空间复杂度为 O(n)。

如果我们考虑算术和比较的成本,运行时间是 O(n² log MAX_NUM) 其中MAX_NUM 是整数的最大绝对值,因为+== 在 Python 的任意大整数需要对数时间。


我们能做得比这更好吗?正如您在问题中指出的那样,这个问题是经过充分研究的3SUM 问题的变体,有时称为 3SUM'(三和素数)。标准 3SUM 问题要求 a + b + c = 0。3SUM' 问题要求 a + b = c

已知具有相同的困难,即如果有一种算法可以在某个渐近时间内求解 3SUM,那么就有一种算法可以在相同的渐近时间内求解 3SUM',反之亦然。 (参考these lecture notes by Adler, Gurram & Lincoln。)

根据维基百科,最知名的 3SUM 算法是由于Timothy M. Chan (2018)

我们提出了一种算法,可以在 O((n² / log² n)(log log n)^O(1)) 时间内解决 n 个实数的 3SUM 问题,将之前的解决方案改进了大约一个对数因子。

复杂度 O((n² / log² n)(log log n)^O(1)) 小于 O(n²),但相差不大,并且增益可能会被输入的常数因子抵消任何实用尺寸。对于 c

【讨论】:

  • 说实话,我不确定问题中的算法是如何工作的,也没有花时间去分析。我有兴趣看到解释。​​
  • 这真的很简单,你应该知道,所以我建议你花点时间:-P
  • 如果他保持排序,那么每个排序将是 O(n)。它的优点是只使用 O(n) 空间。当 n 为一百万时,尝试构建一个 Θ(n²) 大小的哈希表,祝你好运:-)。顺便说一句,如果他不使用“n”来指代他的限制和结果数字的数量,我会更喜欢:-(
  • Python 的排序在这种情况下确实是线性的,因为它识别已经排序的运行(在这种情况下最多两个)并将它们合并。见en.wikipedia.org/wiki/Timsort。虽然bisect.insort 会更好。我只是做了最小的改变。
  • 由于您现在正尝试使用Fraction 使其正确,您可能还希望将三元组从numpy.int64 转换为int,以免溢出。跨度>
【解决方案3】:

我希望看到像 O(n^2) 这样更快的算法

ratioList.append(...) 之后执行ratioList.sort() 和 tadaa... 你有 O(n^2)。

你已经是 O(n^2 log n) 并且日志只是一直从头开始。

这样,您在我的 PC 上 MAX_NUM = 100,000 的运行时间从 222 秒缩短到 116 秒。

【讨论】:

  • 谢谢你!我非常感谢我已经测试过它并且它运行得更快我目前正在优化我的 python 并探索更快的库。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-11
  • 1970-01-01
  • 1970-01-01
  • 2020-10-05
  • 1970-01-01
  • 2010-09-28
相关资源
最近更新 更多