鉴于i = 0到N-1的数字序列a [i],我试图计算以下总和:
a[0] * a[1] * a[2] +
a[1] * a[2] * a[3] +
a[2] * a[3] * a[4] +
...
a[N-4] * a[N-3] * a[N-2] +
a[N-3] * a[N-2] * a[N-1]
我想将一个相乘的组(在上面的示例中为3)的大小G设为可变参数。然后,可以使用简单的O(N * G)算法幼稚地获得结果,该算法可以用伪代码编写,如下所示:
sum = 0
for i from 0 to (N-G-1):
group_contribution = 1
for j from 0 to (G-1):
group_contribution *= a[i+j]
sum += group_contribution
但是,对于大G来说,显然该算法效率极低,尤其是假设序列a [i]的数量事先未知,并且必须在运行时进行昂贵的计算。
基于这个原因,我考虑使用以下复杂度为O(N + G)的算法,该算法通过计算滚动乘积来循环序列a [i]的值:
sum = 0
rolling_product = 1
for i from 0 to (G-1):
rolling_product *= a[i]
sum += rolling_product
for i from G to (N-1):
rolling_product /= a[i-G]
rolling_product *= a[i]
sum += rolling_product
但是我担心标准浮点表示中除法的数值稳定性。
我想知道是否存在一种稳定,更快的方法来计算此和。对我来说,这似乎是一项基本的数字任务,但现在我却茫然地如何高效地完成任务。
谢谢您的任何想法!
答案 0 :(得分:7)
是的,如果您仔细计算反向偏乘积,则无需除法。
def window_products(seq, g):
lst = list(seq)
reverse_products = lst[:]
for i in range(len(lst) - 2, -1, -1):
if i % g != len(lst) % g:
reverse_products[i] *= reverse_products[i + 1]
product = 1
for i in range(len(lst) - g + 1):
yield reverse_products[i] * product
if i % g == len(lst) % g:
product = 1
else:
product *= lst[i + g]
print(list(window_products(range(10), 1)))
print(list(window_products(range(10), 2)))
print(list(window_products(range(10), 3)))
答案 1 :(得分:3)
作为序言,您可以考虑在两种算法上运行一些测试用例并比较结果(例如,相对误差)。
接下来,如果您有额外的内存和时间,这是一种稳定的方法,可以节省O( N log 2 G )时间和记忆。它类似于在range minimum query问题的恒定时间线性空间中的方法。
二次计算乘方范围的乘积
让 B [ i ] [ j ]为2 j 的乘积 a 的sup>元素从位置 i 开始,所以
B [ i ] [ j ] = a [ i ] × a [ i + 1]×...× a [ i + 2 j -1]
我们对 B 中的 N log 2 G 值感兴趣,即0≤ j ≤log 2 G 。我们可以在O(1)中计算每个值,因为
B [ i ] [ j ] = B [ i ] [ j -1]× B [ i + 2 j -1 ]] [ j -1]
计算总和
要计算总和中的一项,我们将 G 分解为2的幂次方块。例如,如果G = 13,则第一项是
a [0]×...× a [12] =( a [0]×...× a [7])×( a [8]×...× a [11])× a [ 12] = B [0] [3]× B [8] [2]× B [12] [0]
每个O( N )项都可以在O(log 2 G )时间内计算,因此查找的总复杂度总和为O( N log 2 G )。
答案 2 :(得分:0)
您的滚动产品是个好主意,但正如您所说,它在稳定性方面存在问题。我可以这样解决:
a[i]
的滚动积,而是计算log(abs(a[i]))
的滚动 sum (不包括零)。然后,当您需要该产品时,它就是(num_zeros > 0 ? 0.0 : exp(log_sum)) * sign
。这样可以解决主要的不稳定因素。G
时,请用该数字覆盖滚动的llog_sum并将其重置为零。这样可以消除长期累积的舍入误差。答案 3 :(得分:0)
创建一个新序列b,其中b [0] = a [0] * a [1] * a [2] * ... a [G-1]等。
现在,您遇到了一个更简单的问题,即可以计算b值的总和,并且可以保持总计,每次添加b值都减去b [0]并添加新值并将它们全部向下滑动一个(使用循环缓冲,因此没有任何动作)。 [典型的滑动窗移动平均类型代码]
保留最后G a []个值的缓存并计算要添加到末尾的新值仅是O(G)运算,而您只计算一次a [i]。