在使用 Memoization 和 Recursion 时,如何改进计算第 N 行 Pascals 三角形的代码?

How can I improve my code for calculating the Nth row of Pascals Triangle while using Memoization and Recursion?

我一直在修改递归并决定用它来计算帕斯卡三角形的行。我已经成功地创建了一个函数来生成适用于 n <= 7 的 Pascals Triangle,但是它的效率非常低。我知道生成帕斯卡三角形的身份,但我对此并不感兴趣。我想要的是一些改进下面代码的指导。

在大约 n = 7 之后,计算需要很长时间,这让我觉得我的 memoization 实现有误。

count = 0 
def Pascal(n):
    global count
    count += 1 
    pasc_list = [] 
    i = 0 
    j = i+1
    dictionary = {0:[1],1:[1,1]}

    if n in dictionary:
        return dictionary[n]
    else:
        pasc_list.append(1)
        while j < len(Pascal(n-1)):
            pasc_list.append(Pascal(n-1)[i] + Pascal(n-1)[j])
            i += 1 
            j = i + 1 
        pasc_list.append(1)
        dictionary[n] = pasc_list
    return pasc_list
a = Pascal(5)
print(a)
print(count)

对于 n = 5,作用域的数量已经是 4694,当 n = 6 时,作用域的数量是 75105,这是一个显着的增加。因此,如果有人可以帮助我降低制作示波器的速度,我将不胜感激!

你在每次迭代中调用一个 Pascal 函数三次(而且,他们中的每一个都调用它越来越多......)。因此,您最终的复杂度是 O(n!)。只需存储每个帕斯卡结果的计算:

count = 0 
def Pascal(n):
    global count
    count += 1 

    pasc_list = [] 
    i = 0 
    j = i+1
    dictionary = {0:[1],1:[1,1]}

    if n in dictionary:
        return dictionary[n]

    else:
        pasc_list.append(1)
        p = Pascal(n-1)  # <-------------------- HERE!!
        while j < len(p):
            pasc_list.append(p[i] + p[j])
            i += 1 
            j = i+1 

        pasc_list.append(1)
        dictionary[n] = pasc_list

    return pasc_list

a = Pascal(7)
print(a)
print(count)

将打印:

[1, 7, 21, 35, 35, 21, 7, 1]
7

对递归要非常准确,不要在 loop-statements 中调用繁重的函数(比如 while j < len(Pascal(n-1)): <-- it is VERY bad idea to write code this way!)。为什么这么慢?

假设我们正在计算 Pascal(3):

在 P3 中我们在 while 中调用 P2。所以我们为 j[0]-index 调用 P2,在它内部调用 2 次,在下一次迭代(j[1]-index)中调用另外 3 次。在每个 P2 迭代中,我们将 P1 迭代再调用 3 次(手动 P1 迭代是第 4 次)。我们对 P4 重复整个过程 4 次,对 P5 重复 16 次,而且越来越多......就在你的代码如此缓慢的时候。

要在 Python 中正确使用记忆,请使用可变默认参数,通常命名为 memo:

count = 0 

def Pascal(n, memo={0:[1],1:[1,1]}):
    global count
    count += 1 

    pasc_list = [] 
    i = 0 
    j = i+1

    if n in memo:
        return memo[n]

    else:
        pasc_list.append(1)
        while j < len(Pascal(n-1)):
            pasc_list.append(Pascal(n-1)[i] + Pascal(n-1)[j])
            i += 1 
            j = i+1 

        pasc_list.append(1)
        memo[n] = pasc_list

    return pasc_list

a = Pascal(7)
print(a)
print(count)

输出:

c:\srv\tmp> python pascal.py
[1, 7, 21, 35, 35, 21, 7, 1]
70

你还应该把记忆 return 作为你的函数做的第一件事:

def Pascal(n, memo={0:[1],1:[1,1]}):
    if n in memo:
        return memo[n]

    global count
    count += 1 

    pasc_list = [] 
    i = 0 
    j = i+1

    pasc_list.append(1)
    while j < len(Pascal(n-1)):
        pasc_list.append(Pascal(n-1)[i] + Pascal(n-1)[j])
        i += 1 
        j = i+1 

    pasc_list.append(1)
    memo[n] = pasc_list

    return pasc_list

输出:

c:\srv\tmp> python pascal.py
[1, 7, 21, 35, 35, 21, 7, 1]
6

记忆无法弥补糟糕的算法。在这种情况下,它不能弥补任何东西。考虑删除记忆逻辑的清理代码版本:

count = 0

def Pascal(n):
    global count

    count += 1

    pasc_list = [1]

    if n > 0:

        i = 0
        j = i + 1

        previous = Pascal(n - 1)

        while j < len(previous):
            pasc_list.append(previous[i] + previous[j])
            i += 1
            j = i + 1

        pasc_list.append(1)

    return pasc_list

a = Pascal(10)

print(a)
print(count)

(此解决方案、@thebjorn 和 @vurmux 都无法使用默认的 Python 堆栈分配达到 Pascal(1000)。)如果我们计时 @vurmux 接受的答案,以及我上面的答案,循环遍历每个从 0 到 900 的行:

for n in range(900):
    print(Pascal(n))

结果相同:

> time python3 no-memoization.py > /dev/null
52.169u 0.120s 0:52.36 99.8%    0+0k 0+0io 0pf+0w
> time python3 memoization.py > /dev/null
52.031u 0.125s 0:52.23 99.8%    0+0k 0+0io 0pf+0w
>

如果我们采用上面的 no-memoization 解决方案,并简单地添加 Python 自己的 lru-cache 装饰器:

from functools import lru_cache

count = 0

@lru_cache()
def Pascal(n):
# ...

我们得到了显着 (100x) 的加速:

> time python3 lru_cache.py > /dev/null
0.556u 0.024s 0:00.59 96.6% 0+0k 0+0io 0pf+0w
>

正如我们对@thebjorn 的 (+1) 解决方案所做的那样,它正确地重用了字典缓存,而不是在每次调用时重新分配它。

重定向到文件时,所有输出都是相同的。 (很多时候我将 functools:lru-cache 添加到一个函数中只是为了发现它实际上减慢了它的速度——也就是说,不是这个优化的好候选者。)

专注于编写一个好的算法并尽可能地调整它。然后,研究记忆化等技术。但是做个计时看看是不是真的赢了。