Python 中的高效求和

Efficient summation in Python

我正在尝试有效地计算 Python 中求和的求和:

WolframAlpha is able to compute it too a high n value: sum of sum.

我有两种方法:for 循环方法和 np.sum 方法。我认为 np.sum 方法会更快。但是,它们在 n 变大之前是相同的,之后 np.sum 出现溢出错误并给出错误的结果。

我正在尝试找到计算此总和的最快方法。

import numpy as np
import time

def summation(start,end,func):
    sum=0
    for i in range(start,end+1):
        sum+=func(i)
    return sum

def x(y):
    return y

def x2(y):
    return y**2

def mysum(y):
    return x2(y)*summation(0, y, x)

n=100

# method #1
start=time.time()
summation(0,n,mysum)
print('Slow method:',time.time()-start)

# method #2
start=time.time()
w=np.arange(0,n+1)
(w**2*np.cumsum(w)).sum()
print('Fast method:',time.time()-start)

(最快的方法3和4在最后)

在快速 NumPy 方法中,您需要指定 dtype=np.object,以便 NumPy 不会将 Python int 转换为它自己的数据类型(np.int64 或其他)。它现在会给你正确的结果(最多检查 N=100000)。

# method #2
start=time.time()
w=np.arange(0, n+1, dtype=np.object)
result2 = (w**2*np.cumsum(w)).sum()
print('Fast method:', time.time()-start)

您的快速解决方案比慢速解决方案快得多。是的,对于大 N,但已经在 N=100 时快了 8 倍:

start=time.time()
for i in range(100):
    result1 = summation(0, n, mysum)
print('Slow method:', time.time()-start)

# method #2
start=time.time()
for i in range(100):
    w=np.arange(0, n+1, dtype=np.object)
    result2 = (w**2*np.cumsum(w)).sum()
print('Fast method:', time.time()-start)
Slow method: 0.06906533241271973
Fast method: 0.008007287979125977

编辑:更快的方法(由 KellyBundy,南瓜)是使用纯 python。事实证明 NumPy 在这里没有优势,因为它没有 np.objects.

的矢量化代码
# method #3
import itertools
start=time.time()
for i in range(100):
    result3 = sum(x*x * ysum for x, ysum in enumerate(itertools.accumulate(range(n+1))))
print('Faster, pure python:', (time.time()-start))
Faster, pure python: 0.0009944438934326172

EDIT2:Forss 注意到 numpy 快速方法可以通过使用 x*x 而不是 x**2 来优化。对于 N > 200 它比纯 Python 方法更快。对于 N < 200 它比纯 Python 方法慢(边界的确切值可能取决于机器,我的是 200,最好自己检查):

# method #4
start=time.time()
for i in range(100):
    w = np.arange(0, n+1, dtype=np.object)
    result2 = (w*w*np.cumsum(w)).sum()
print('Fast method x*x:', time.time()-start)

这是一个非常快速的方法:

result = ((((12 * n + 45) * n + 50) * n + 15) * n - 2) * n // 120

我是如何到达那里的:

  1. 将内和重写为众所周知的x*(x+1)//2。所以整个事情变成了 sum(x**2 * x*(x+1)//2 for x in range(n+1)).
  2. 重写为sum(x**4 + x**3 for x in range(n+1)) // 2
  3. formulas 中查找 sum(x**4)sum(x**3)
  4. Simplify 造成的混乱 (12*n**5 + 45*n**4 + 50*n**3 + 15*n**2 - 2*n) // 120.
  5. Horner吧。

如果在步骤 1. 和 2. 之后您知道它是 5 次多项式,则另一种推导方法:

  1. 用简单的实现计算六个值。
  2. 计算六个方程的多项式,六个未知数(多项式系数)。我做的与 this 类似,但我的矩阵 A 与之相比是左右镜像的,我称我的 y 向量为 b.

代码:

from fractions import Fraction
import math
from functools import reduce

def naive(n):
    return sum(x**2 * sum(range(x+1)) for x in range(n+1))

def lcm(ints):
    return reduce(lambda r, i: r * i // math.gcd(r, i), ints)

def polynomial(xys):
    xs, ys = zip(*xys)
    n = len(xs)
    A = [[Fraction(x**i) for i in range(n)] for x in xs]
    b = list(ys)
    for _ in range(2):
        for i0 in range(n):
            for i in range(i0 + 1, n):
                f = A[i][i0] / A[i0][i0]
                for j in range(i0, n):
                    A[i][j] -= f * A[i0][j]
                b[i] -= f * b[i0]
        A = [row[::-1] for row in A[::-1]]
        b.reverse()
    coeffs = [b[i] / A[i][i] for i in range(n)]
    denominator = lcm(c.denominator for c in coeffs)
    coeffs = [int(c * denominator) for c in coeffs]
    horner = str(coeffs[-1])
    for c in coeffs[-2::-1]:
        horner += ' * n'
        if c:
            horner = f"({horner} {'+' if c > 0 else '-'} {abs(c)})"
    return f'{horner} // {denominator}'

print(polynomial((x, naive(x)) for x in range(6)))

输出(Try it online!):

((((12 * n + 45) * n + 50) * n + 15) * n - 2) * n // 120

像这样将 Python 与 WolframAlpha 进行比较是不公平的,因为 Wolfram 会在计算之前简化方程式。

幸运的是,Python 生态系统没有限制,因此您可以使用 SymPy:

from sympy import summation
from sympy import symbols

n, x, y = symbols("n,x,y")
eq = summation(x ** 2 * summation(y, (y, 0, x)), (x, 0, n))
eq.evalf(subs={"n": 1000})

它将几乎立即计算出预期结果:100375416791650。这是因为 SymPy 为您简化了方程式,就像 Wolfram 所做的那样。查看eq的值:

很棒,但如果你像我一样使用计算器计算 2 + 2,那么你会爱上 SymPy ❤。如您所见,只需 3 行代码即可获得相同的结果,并且该解决方案也适用于其他更复杂的情况。

所有答案都使用数学来简化或实现 python 中的循环,试图达到 cpu 最优,但它们不是内存最优。

这是一个简单的实现,没有使用任何内存效率高的数学简化

def function5():
    inner_sum = float()
    result = float()

    for x in range(0, n + 1):
        inner_sum += x
        result += x ** 2 * inner_sum
        
    return result

相对于 dankal444 的其他解决方案,它相当慢:

method 2   | 31 µs ± 2.06 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
method 3   | 116 µs ± 538 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
method 4   | 91 µs ± 356 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
function 5 | 217 µs ± 1.14 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

顺便说一句,如果你用 numba 来 jit 函数(可能有更好的选择):

from numba import jit
function5 = jit(nopython=True)(function5)

你得到

59.8 ns ± 0.209 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

在评论中,您提到实际上是 f(x) 和 g(y) 而不是 x2 和 y。如果你只需要那个和的近似值,你可以假设和是中点黎曼和,这样你的和就可以用二重积分来近似 ∫-.5n+.5 f(x) ∫-.5x+.5 g(y) dy dx.

用你原来的 f(x)=x2 和 g(y)=y,这简化为 n5/10 +3n4/8+n3/2+5n2/16+3n/32+ 1/160,与正确结果相差n3/12+3n2/16+53n/480+1/160.

基于此,我怀疑 (actual-integral)/actual 会是 max(f'',g'')*O(n-2),但是我无法证明这一点。