【问题标题】:How to improve divide-and-conquer runtimes?如何改善分而治之的运行时间?
【发布时间】:2021-08-31 09:21:23
【问题描述】:

当分而治之的递归函数产生的运行时间不够低时,还可以进行哪些其他改进?

比如说,这个power函数取自here

def power(x, y):
    if (y == 0): return 1
    elif (int(y % 2) == 0):
        return (power(x, int(y / 2)) * power(x, int(y / 2)))
    else:
        return (x * power(x, int(y / 2)) * power(x, int(y / 2)))

由于它是递归性质,memoizing and tail recursion(不确定它是否可以与分而治之一起应用)可以提供很大帮助,但我不知道有任何其他调整。对于我的基准测试,我使用的是:

base = 1522605027922533360535618378132637429718068114961380688657908494580122963258952897654000350692006139
exponent = 1000000

301.140625s 结束,我仍然需要它能够处理更大的基数和指数...也许将问题分成两个以上的子问题?

正在使用的memoizer可以在here找到

【问题讨论】:

  • 使用递归还是使用迭代方法也可以吗?
  • @QuentinCoumes 都很好,尽管我对改进分而治之很感兴趣。我的意思是,改进递归函数。
  • 简单的改进是将多次调用 power(x, int(y / 2)) 替换为使用变量的唯一调用(即使 memoization 对此有所帮助),您也可以考虑使用位移来代替除法和模数。但不要忘记,纯 Python 并不是真正为计算密集型任务而设计的。

标签: python recursion divide-and-conquer


【解决方案1】:

您应该在这里使用的主要优化是公共子表达式消除。考虑你的第一段代码:

def power(x, y):
    if y == 0: 
        return 1
    elif y % 2 == 0:
        return (power(x, y // 2) * power(x, y // 2)
    else:
        return (x * power(x, y // 2) * power(x, y // 2)

我稍微清理了一下——因为yinty % 2 也将是int,因此不需要转换类型。编写int(y / 2) 的规范方式是y // 2。最后,在ifwhile 语句中,不需要在布尔子句周围加上括号,因为我们以分号结束子句(而在类似C 的语法中,可以有一个单行@ 987654330@ 声明,因此我们需要括号)。

这里的问题是您在elifelse 情况下都计算了两次power(x, y // 2)。您应该尝试只计算一次该值。

def power(x, y):
    if (y == 0): 
        return 1
    else:
        smaller_power = power(x, y // 2)
        if y % 2 == 1:
            return x * smaller_power * smaller_power
        else:
            return smaller_power * smaller_power

这会立即显着提高算法的效率。改进后的版本不是及时的O(y),而是及时的O(log y)(假设我们将单个*计为1次操作)。

稍微聪明点,我们可以想出一个稍微不同的算法:

def power(x, y):
    if y == 0:
        return 1
    else:
        smaller_power = power(x * x, y // 2)
        return x * smaller_power if y % 2 == 1 else smaller_power

可以转换为以下尾递归版本:

def power_tail_helper(x, y, acc):
    """
    returns acc * power(x, y)
    """
    if y == 0:
        return acc
    else:
        new_acc = acc * x if y % 2 == 1 else acc
        return power_tail_helper(x * x, y // 2, new_acc)

def power_tail(x, y):
    return power_tail_helper(x, y, 1)

又可以转化为以下迭代算法:

def power_iter(x, y):
    acc = 1
    while y != 0:
        (x, y, acc) = (x * x, y // 2, acc * x if y % 2 == 1 else acc)
    return acc

请注意,在惯用的 Python 中,我们会将其写为

def power_iter(x, y):
    acc = 1
    while y:
        x, y, acc = (x * x, y // 2, acc * x if y % 2 else acc)
    return acc

利用数字在适当的上下文中自动转换为布尔值的事实,0 为False,所有其他数字为True

您可以使用的最后一种方法是更数学的方法。您可以使用对数计算指数。这将需要一个高精度的对数库来获得准确的答案,因为base 是一个 320 位的数字,对于log 的普通 64 位双精度浮点算术版本来说太大了以至于不够精确。这可能是最佳方法,但您必须进行更多研究。

无论哪种方式,请记住,输出的大小将是一个需要超过 1 亿字节来存储的数字。所以无论你使用什么算法,都需要一段时间,因为即使是把答案存储在内存中并读出来的“算法”也需要一段时间。

【讨论】:

  • 您的解决方案非常棒,谢谢。我知道BigFloat,它是 MPFR 的包装器,但我在 Windows 上使用它时遇到了麻烦。我会同时采取你的解决方案。
  • @lok​​o 我忘了提到的是,当您处理大量数字(例如 1 亿字节数字)时,瓶颈可能是乘法算法。因此,如果您要在生产中使用它来处理大整数,您可能需要研究基于 FFT 的快速乘法算法。
猜你喜欢
  • 1970-01-01
  • 2014-12-18
  • 1970-01-01
  • 2013-11-09
  • 2017-05-19
  • 2013-01-31
  • 2012-03-04
  • 2013-02-02
  • 2023-03-14
相关资源
最近更新 更多