问题

项目Euler 1是现场最常见的问题之一。但是,我想解决除法的更一般的问题。


列表的倍数

如果我们列出所有低于10的自然数,即3的倍数或
5,我们得到3、5、6和9。这些倍数的总和为23。

从开始到结束查找列表中所有倍数的总和,其中
start和stop是自然数。


在经典问题中,此列表为[3, 5]。但是我想创建一个可以解决[3, 2][3, 2, 7]以及实际上任何数字组合的代码。朴素的解决方案如下所示。我不想对此代码发表评论,但可以将其包含在内,以测试我的实现的正确性。上面的代码是正确的,但是具有线性\ $ \ mathcal {O}(n)\ $运行时间。我想更快地创建一些东西。
但是我确实遇到了一些问题,这些问题使我的代码比需要的更长。

改进的算法

要找到被kstart之间的某个数stop整除的数字数,我使用

def sum_divisible_naive(divisors=[3, 5], start=0, stop=100):
    count = 0
    for num in xrange(start, stop):
        for d in divisors:
            if num % d == 0:
                count += num
                break
    return count


这只是前n个自然数之和的略微修改版本。以[2, 3]为例。如果要这样做

def divisible_by_k(k, start=0, limit=100):
    stop = int((limit-1)/float(k))
    return k*(stop+start)*(stop-start+1)/2


由于两个列表中都存在一些数字:[3, 6, 9, 12, 15][5, 10, 15],我会计算太多的数字。要纠正此问题,我们必须减去\ $ 15 \ $的所有倍数。

另一个问题是[6, 9],如果像上面那样做,我们将删除\ $ 6 * 9 = 54 \ $的所有倍数。 ,但是\ $ 18 \ $是两个列表中出现的第一个数字。解决方案是将乘积除以gcd。例如\ $ 6 \ cdot9 / \ gcd(6,9)= 56/3 = 18 \ $根据需要。

total = divisible_by_k(3) + divisible_by_k(5)


我想进一步概括一下,以便列出这些数字。假设我们有[2, 3, 8],则不需要包含\ $ 8 \ $,因为\ $ 8 \ $的所有倍数已经计入\ $ 2 \ $。我使用以下代码删除了不必要的倍数

total = divisible_by_k(a) + divisible_by_k(b) - sum_divisible_by_k(a*b/gcd(a, b))


所以remove_multiples([2, 3, 6, 8)返回[2, 3]

[2, 3, 5]为例。现在,我们必须减去\ $ 2 \ cdot 3 \ $,\ $ 2 \ cdot5 \ $和\ $ 3 \ cdot 5 \ $,以避免重复计算。但是,我们现在删除了太多数字,必须包含\ $ 2 \ cdot 3 \ cdot 5 \ $。希望在研究以下图像时,这一点会变得更清楚



通过研究前面的示例,可以找到n编号的一般情况。我们可以使用k<=ncombinations模块获取collections编号的组合。然后,我只需要在加减之间切换。

这意味着我还必须找到列表的gcd,而不仅是一对数字。列表的gcd被多次计算,因为我们经历了每种组合。我认为制定一个命令是减少工作量的好主意。由于对perm进行了排序,因此a将始终小于b。由于代码不够完整,因此我在命名中也没有添加太多内容,也没有添加文档字符串或注释。

问题:


有什么办法可以使代码更清晰,更短?

代码

def remove_multiples(divisors):
    i = 0
    divisors = sorted(set(divisors))
    divisors_len = len(divisors)
    while i < divisors_len:
        j = i + 1
        while j < divisors_len:
            if divisors[j] % divisors[i] == 0:
                divisors.remove(divisors[j])
                divisors_len -= 1
            else:
                j += 1
        i += 1
    return divisors


评论

恭喜,您已经发现了包含-排除原则!

#1 楼

背景

让\ $ N \ $为范围的大小[开始,停止)。 \ $ N = \ textrm {停止}-\ textrm {开始} \ $。

让\ $ M \ $为除数列表的大小。 \ $ M \ $ = \ $ \ lvert \ textrm {除数} \ rvert \ $

天真的方法是\ $ \ Omega(N)\ $,而您的方法是\ $ \ mathcal {O }(2 ^ M)\ $,因为您必须迭代除数列表的幂集。

变量命名

我知道k只是一个通用的迭代器名称,但以下代码令我感到困惑:

total += k*sum_divisible_by_k(product, start, stop)


为符号转换迭代器使用其他通用名称,例如jsk以外的其他名称。 br />
错误

sum_divisible_by_k中直接使用start是不正确的。相反,您应该计算\ $ \ lceil \ frac {start} {k} \ rceil \ $。另外,对于stop,您可以只使用整数除法\ $ \ lfloor \ frac {limit-1} {k} \ rfloor \ $。

反模式

迭代在我知道的大多数语言中,删除值时使用列表是一种反模式。如此处另一个答案所述,我建议您建立一个新列表。

记忆化

您先前计算出的gcd的dict在两个方面都不理想。


您只存储gcd的先前值,因此仍然需要对每个除数组合执行乘法。例如,在除数集中\ $ \ {a,b,c,d,e \} \ $中,您将计算\ $ a \ cdot b \ textrm {,} a \ cdot b \ cdot c \ textrm {,} a \ cdot b \ cdot c \ cdot d \ textrm {,} a \ cdot b \ cdot c \ cdot d \ cdot e \ $。您应该存储实际的lcm,而不仅仅是存储gcd的中间步骤。

\ $ \ textrm {lcm}(a,b,c)= \ textrm {lcm}(\ textrm {lcm}(a,b),c)\ $我相信您知道。但是,在计算\ $ \ textrm {lcm}(a,b,c,d,e)\ $时,您正在执行\ $ \ textrm {lcm}(a,b)\ $的查找,然后是\ $ \ textrm {lcm}(\ textrm {lcm}(a,b),c)\ $然后是\ $ \ textrm {lcm}(\ textrm {lcm}(\ textrm {lcm}(a,b),c), d)\ $,等等。您可以执行\ $ 1 \ $查找,而不是对长度为\ $ L \ $的列表的lcm执行\ $ L-1 \ $查找。

从itertools :


组合以字典顺序排序。因此,如果对输入iterable进行了排序,则将按排序顺序生成组合元组。


假设您首先从最小的组合开始(当前是),则可以确保当前已删除最后一个元素的组合已在dict中。也就是说,确定\ $ \ textrm {lcm}(a,b,c,d)\ $时,我们总是可以从dict中获取\ $ \ textrm {lcm}(a,b,c)\ $。 />

组合逻辑

为了方便起见,通常将数学值赋予某些表达式。例如,\ $ 0!= 1 \ $。为您提供一个方便的值是定义\ $ \ textrm {lcm}(\ textrm {()))= 1 \ $,以便\ $ \ textrm {lcm}(\ textrm {()},a)= a \ $。然后,您可以将sum_divisors_fast中的两个外部循环合并为一个循环。

更新代码

原始版本

from itertools import combinations
from fractions import gcd

lcm_dict = {():1}

def lcm(a, b):
    return a * b // gcd(a, b)

def lcm_list(perm):
    l = lcm(lcm_dict[perm[:-1]], perm[-1])
    lcm_dict[perm] = l
    return l

def remove_multiples(divisors):
    # Directly from Joe Wallis's answer
    new_divisors = []
    divisors = sorted(set(divisors))
    for divisor in divisors:
        if not any(divisor % d == 0 for d in new_divisors):
            new_divisors.append(divisor)
    return new_divisors

def ceil_div(a, b):
    return -(-a // b)

def summation_from_zero(n):
    return n * (n+1) // 2

def sum_divisible_by_k(k, start, stop):
    sum_start = ceil_div(start, k)
    sum_stop = (stop - 1) // k
    return k * (summation_from_zero(sum_stop) - summation_from_zero(sum_start-1))

def sum_divisible_fast(div=[3,5], start=0, stop=100):
    divisors = remove_multiples(div)    
    total = 0

    j = 1
    for i in xrange(1, len(divisors)+1):
        for perm in combinations(divisors, i):
            product = lcm_list(perm)
            total += j * sum_divisible_by_k(product, start, stop)
        j = -j

    return total


def sum_divisible_naive(divisors=[3, 5], start=0, stop=100):
    count = 0
    for num in xrange(start, stop):
        for d in divisors:
            if num % d == 0:
                count += num
                break
    return count


if __name__ == '__main__':

    list_to_test = [2, 6, 9, 8]
    start = 2
    stop = 10**5
    print sum_divisible_naive(list_to_test, start, stop)
    print sum_divisible_fast(list_to_test, start, stop)


    print sum_divisible_naive()
    print sum_divisible_fast()



更新

timeit.timeit('orig.sum_divisible_naive([3,5,7,11,13,17,19,23,29,31,37,39,41], 0, 10**7)', 'from __main__ import orig', number=1)
5.059425115585327
timeit.timeit('orig.sum_divisible_fast([3,5,7,11,13,17,19,23,29,31,37,39,41], 0, 10**7)', 'from __main__ import orig', number=10)
0.2042551040649414
timeit.timeit('orig.sum_divisible_fast([3,5,7,11,13,17,19,23,29,31,37,39,41], 0, 10**7)', 'from __main__ import orig', number=100)
1.8143179416656494



/>

评论


\ $ \ begingroup \ $
我真的不认为幼稚的方法会像\ $ \ mathcal {O}(MN)\ $一样慢。这是因为我们使用了if循环,并在达到可除数时停止。因此,每个数字的一​​半都可被2整除,因此不再对这些数字进行测试。我认为必须查看函数必须经过的平均循环数才能找到确切的运行时间。除了那个梦幻般的答案!
\ $ \ endgroup \ $
– N3buchadnezzar
16年7月5日在9:11



\ $ \ begingroup \ $
@ N3buchadnezzar你是完全正确的。更糟糕的是,我说“当然”是它的\ $ \ mathcal {O}(MN)\ $。不幸的是,我现在不确定在不知道\ $ M \ $中除数的确切分布的情况下该算法的运行时间该如何说。
\ $ \ endgroup \ $
–twohundredping
2016年7月5日在12:46



\ $ \ begingroup \ $
我认为\ $ \ mathcal {O}(2 ^ M)\ $与\ $ \ mathcal {O}(N)\ $是一个比较。一个由除数的数量决定,而另一个则由间隔的长度决定。我认为这是最重要的部分,其余的都在挑剔^^
\ $ \ endgroup \ $
– N3buchadnezzar
16年7月5日在13:39



\ $ \ begingroup \ $
我更愿意说\ $ \ omega(\ textrm {max}(N,\ frac {NM} {logN}))\ $,但是我同意指定更好的界限是挑剔的,特别是对于代码审查。我将把脚架保存起来以使用更合适的网络。
\ $ \ endgroup \ $
–twohundredping
16年7月5日在14:40

#2 楼

您的数学比我的数学好得多,因此我无法对此发表评论。

但是,如果您以其他方式遇到问题,可以简化您的函数remove_multiples
通常与从旧列表中删除相比,将其添加到新列表中更容易。

这种方法的工作方式不是检查您添加的数字之后的所有数字是否都正确,而是检查是否可以通过查看前面的数字来确定数字。
那么您可以实现一个简单的功能:

说任何sorted(set(divisors))小于任何new_divisors
除非您将负数传递给它。
由此我们可以遍历divisors,如果它可以被任何divisor整除,则将其删除。 。

#3 楼

gcd_dict / gcd_list的东西可能更具表现力。

例如,由于您要使用备忘录,因此可以明确地说出来。可以完成这项工作的简单装饰器将比您的gcd_list占用更少的空间,并且可以重复使用。

看看这一点-当然,这对于一项任务来说是过大的,但是您会发现一些用处如果您以后继续执行Euler项目任务,则可以记住装饰者,因此将时间花在一个好的人身上会有所作为。


remove_multiples已经被另一个海报覆盖。我要补充一点,可以使用一些巧妙的reduce而不是迭代来缩短它,但是我不能说肯定会有所改进。


您有2条代码会增加/用total减少sum_divisible_by_k。当然,让读者理解第一部分的内容会更简单,但是无论如何,读者都必须理解第二部分。因此,可以将range(2, len(divisors)+1)替换为range(1, len(divisors)+1),并将初始k值设置为1-然后可以完全摆脱for divisor in divisors:循环。

此外,k的含义也不明显。如果对偶数大小的排列表示“ -1,对于奇数大小的排列表示1”,则应这样说。例如1 if (len(perm) % 2) else -1。不用担心,len()很快。

如果您找不到product可以吸引眼球,则可以将reduce移到一个单独的功能上,但这只是个人喜好。 >

#4 楼

我认为还有一个额外的速度。令N为停止-开始,k为除数。考虑k << << N,例如除数= {3,5},开始= 1,停止= 1百万的情况。

我们不需要执行\ $ O(N)\ $查找,我们可以在\ $ O(1)\ $中计算3的倍数之和。它是3 *(1 + 2 + 3 + 4 ... floor(1 Million / 3)),减少为众所周知的3 * n(n-1)/ 2个公式。

现在您的问题只需要\ $ O(k ^ 2)\ $而不是\ $ O(N)\ $,这可能要快得多。

评论


\ $ \ begingroup \ $
我已经做到了。我还对其进行了改进,以便可以在长度超过两位的列表上使用此技术。问题不是关于这个技巧,而是关于我的解决方案。
\ $ \ endgroup \ $
– N3buchadnezzar
16年7月1日在18:03