序列中最后 K 个元素的数值稳定和快速求和

计算科学 Python 算法 浮点 精确
2021-12-02 15:39:50

假设我有一个很长的,可能是无限的序列x:=[x1,x2,...],我想用它来计算另一个序列y:=[y1,y2,...]其中每个元素是输入序列的最后 K 个元素的总和。IE

yi=j=max(1,iK)ixj

天真、低效的方法(在 Python 中)是:

def sum_of_last_k(x: Sequence[float], k: int):
    buffer = [0. ] * k  # Initialize a buffer of k zeros
    for i, xi in enumerate(x):
        buffer[i % k] = xi  # Where % is modulo
        yield sum(buffer)

......哪个会有O(K)每次迭代的效率。但是,我想在线高效地进行操作。我们可以做到这一点O(1)通过保持流动总和:

def sum_of_last_k(x: Sequence[float], k: int):
   buffer = [0. ] * (k-1)  # Initialize a buffer of k-1 zeros
   running_sum = 0
   for i, xi in enumerate(x):
       ix_wrapped = i % (k-1)
       old_value = buffer[ix_wrapped]
       buffer[ix_wrapped] = xi
       running_sum = running_sum + xi - old_value
       yield running_sum

这有O(1)效率,但仍然存在一个问题:由于浮点精度误差,可能存在running_sum随时间累积的舍入误差。

现在我知道我可以通过running_sum每 M 次迭代从头开始重新计算来解决这个问题,其中 M 是一个很大的数字,并且平均运行时间为O((M1+K)/M)这可能非常接近 1 ......但我希望最坏情况下的运行时间是O(1), 不是O(K).

那么,有没有办法计算这个O(1)每次迭代的时间,同时仍保持数值稳定?

1个回答

非常有趣的问题!

LAPACK 启发的自适应策略

这让我想起了在 LAPACK 例程(揭示排名的 QR)中发现的与“降级”规范相关的错误:本质上,你得到了一个向量的范数v,你想在 O(1) 中的每次迭代中计算砍掉其初始条目后相同向量的范数:(v[1:], v[2:], ...在 Python 表示法中)。在这些和之间存在正交变换,这使得使用不同的策略变得困难。有关详细信息,请参阅http://www.netlib.org/lapack/lawnspdf/lawn176.pdf 。

据我了解,研究人员采用的解决方案是显式跟踪浮点误差的累积,并在其太大时重新计算总和。

这个 udea 的简单版本实现起来并不太复杂:例如,跟踪计算中出现的最大数,如果当前结果明显小于它,则从头开始重新计算总和。这种策略仍然是最坏情况的 O(K),但至少您仅在出于稳定性原因真正需要它时才支付 O(K),否则为 O(1)。

O(\sqrt{K}) 时间策略

无论如何,要确定地低于 O(K),一个可能的想法如下:

  • 认为K为简单起见,是一个完美的正方形。
  • 将元素循环划分为K桶:桶j包含所有x[i]带有i % sqrt(K) == j. 对于每个存储桶,将其对运行总和的贡献存储在一个临时变量中。
  • 在每一步,当你移动窗口时,所有的桶都没有改变,除了其中一个,其中一个元素被替换:从头开始重新计算这个桶的总和;然后计算总和。两种操作都需要O(K)时间。

请注意,如果您只能在线访问序列,则此策略仍需要 O(K) 空间,但在每次迭代中您只能访问O(K)其中。

Possibly a recursive version of this strategy with suitably-chosen bucket sizes could reachO(logK)对于足够大K.